Itās hard to debug, and tricky for all sorts of reasons and on all sorts of levels. But ultimately creating a Grasshopper-native āDevopsā workflow eliminates a lot of tedium, and allows for very rapid builds and releases
The api calls you need are mostly just the Python versions of what you already have, e.g.
I canāt figure out how to modify the slider position and I havenāt tested connecting the GhPython inputs to a slider, but hopefully the code below will help you get there.
Itās a good idea in a self-modifying component, to make the adding input Params functionality execute as a one off, to avoid an infinite loop (adding or removing an input param can re-execute the component). I either set a boolean flag on the main component and test that in RunScript, then toggle it. Or below I also test if the Input Param is already there (change the mode to GhComponent SDK mode):
from ghpythonlib.componentbase import executingcomponent as component
import Grasshopper, GhPython
import System
import Rhino
import rhinoscriptsyntax as rs
input_list = ['input_1','input_2', 'any_name_input']
output_list = ['output_name_1','output_other']
def AddParam(name, IO):
assert IO in ('Output', 'Input')
params = [param.NickName for param in getattr(ghenv.Component.Params,IO)]
if name not in params:
param = Grasshopper.Kernel.Parameters.Param_GenericObject()
param.NickName = name
param.Name = name
param.Description = name
param.Access = Grasshopper.Kernel.GH_ParamAccess.list
param.Optional = True
index = getattr(ghenv.Component.Params, IO).Count
registers = dict(Input = 'RegisterInputParam'
,Output = 'RegisterOutputParam'
)
getattr(ghenv.Component.Params,registers[IO])(param, index)
ghenv.Component.Params.OnParametersChanged()
def add_slider():
slider = Grasshopper.Kernel.Special.GH_NumberSlider()
slider.SetSliderValue(0.7)
slider.CreateAttributes()
slider.Attributes.ExpireLayout();
GH_doc = ghdoc.Component.Attributes.Owner.OnPingDocument()
success = GH_doc.AddObject(docObject = slider, update = False)
return success
class MyComponent(component):
first = True
def RunScript(self, *args):
if self.first:
self.first = False
add_slider()
for input_ in input_list:
AddParam(input_, 'Input')
for output in output_list:
AddParam(output, 'Output')
a='OK'
return a
Do you mean removing the slider component, not removing the input it was connected to on the GhPython component (I never found a good way to do the latter, that wasnāt glitchy, unlike adding them)?
Iāve not tried it, but according to the API docs, but it should just be similar to the above, but get a reference to the component to be removed, and calling GH_doc.RemoveObject(..., True) on it
Thanks for the response! I wanted to dynamically add inputs based on an initial input, and then remove those inputs when that inital component was disconnected. I have solved that issue by using the ghenv.Component.Params.UnregisterInputParameter(param) method. I have to call ExpireSolution() to get the component to return to an original state with no ouptut. This is all working well.
My issue now is that when I restart Rhino/GH and open a saved file with the component, the connections to the inputs are removed. Maybe you could help with thisā¦
Is there a way to keep the current configuration of the component when closing Rhino/GH, or will it always recompute from an initial configuration?
Is there a place to save the parameters of the configuration internally, i.e. without creating an external config file?
If so, will the components on the canvas have the same guidās when reopening the file?
My thought is to store the input param ID and input component guid somewhere and when I open the file I can reconnect the two with that information.
Iām using the python script component in component mode.
hey I thought Iād just post my code in case it helps find a solution or in case anyone else finds it useful.
from ghpythonlib.componentbase import executingcomponent as component
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning
import Grasshopper
def AddParam(name, IO, list=True):
assert IO in ("Output", "Input")
params = [param.NickName for param in getattr(ghenv.Component.Params, IO)]
if name not in params:
param = Grasshopper.Kernel.Parameters.Param_GenericObject()
param.NickName = name
param.Name = name
param.Description = name
param.Access = Grasshopper.Kernel.GH_ParamAccess.list
param.Optional = True
index = getattr(ghenv.Component.Params, IO).Count
registers = dict(Input="RegisterInputParam", Output="RegisterOutputParam")
getattr(ghenv.Component.Params, registers[IO])(param, index)
ghenv.Component.Params.OnParametersChanged()
return param
def ClearParams(self):
while len(ghenv.Component.Params.Input) > 1:
ghenv.Component.Params.UnregisterInputParameter(
ghenv.Component.Params.Input[len(ghenv.Component.Params.Input) - 1]
)
ghenv.Component.Params.OnParametersChanged()
class dynamic_component(component):
def __init__(self):
self.input_type = None
def RunScript(self, original_input, *args):
if not original_input: # if no original_input is input
self.ClearParams()
self.input_type = None
return
if original_input.type != self.input_type: # if JointOptions changes
if len(original_input.input_names) != 2: # original_input is a class instance with attribute `input_names`
self.AddRuntimeMessage(Error, "Component currently only supports types with 2 inputs.")
self.ClearParams()
self.input_type = original_input.type
for name in original_input.input_names:
AddParam(name, "Input")
if len(ghenv.Component.Params.Input) != 3: #check that number of input params is correct
self.AddRuntimeMessage(Error, "Input parameter error.")
return
if len(args) < 2: #check if things connected to new inputs
self.AddRuntimeMessage(Warning, "Input parameters failed to collect data.")
return
for i in range(len(ghenv.Component.Params.Input) - 1): #do stuff
if not args[i]:
self.AddRuntimeMessage(
Warning, "Input parameter {} failed to collect data.".format(original_input.input_names[i])
)
else:
print(args[i])
I donāt quite understand the issue, if itās supposed to remove input params, and the saved version being loaded has those input params already removed? Or is the saved version missing some internal state, that caused the input params initially to be removed. You could try and detect the difference between a param disconnect, and any of: initial placement/ edit /load from save. But if the component is disconnected and removes a param, and user clicks āsaveā, Grasshopper just saves exactly what it sees at that moment.
Depends what you mean by āconfigurationā. The Input and Output params state is saved by Grasshopper (I think it just uses some sort of xml under the hood). But the internal Python state, e.g. from running __init__, and any instance variables, is not saved. I canāt quite remember, but I think __init__ will always run on first load from save (just the same as after an edit to its source code, or when the component was first placed). And even if __init__ doesnāt run (on load from save), Grasshopper does not save internal Python state - it is not going to automatically serialise arbitrary Python objects, and save the value of self.input_type to disk, and then restore from that on load.
I indexed the code I actually wanted to run (imported from a normal Python package) from .NickName, which is savedā¦ .Description is also editable I believe, but that will be visible in the mouse over bubble to the end user. Config files are easier than self-altering GhPython components to be honest, certainly easier to debug, especially if you use a helpful library that makes them more or less interchangeable with a nested dictionary of primitives, e.g ;-).: GitHub - JamesParrott/toml_tools: Tomli and Tomli-W for Python 2 and Iron Python . But a lot of what youāre attempting also causes me to wonder if a completely different design would avoid much of this difficulty, e.g. an approach that saves state in Grasshopper params outside of Python components, and uses simpler python components, and let Grasshopper persist/serialize the state the same as it does anything else in Grasshopperā¦
Itās best to assume as little as possible about uuids (beyond a regex). Especially about them persisting from one session to another. You have to work with them for versioning C# plug ins and in a packaged user objectās file. But everywhere else, treat UUIDs as a Grasshopper internal private variable, subject to change between sessions and implementations.
The issue is that I save a script where the created params are connected to input components, but when I open that script later, the params are disconnected. It is untenable to reconnect them every time a file with the component is opened.
The Input and Output params state is saved by Grasshopper (I think it just uses some sort of xml under the hood). But the internal Python state, e.g. from running __init__ , and any instance variables, is not saved.
So the doc presumably opens with the components connected, and I guess the issue is that the self.input_type reverts to None on opening the doc, because it is an internal state in the python script. This would cause the params to clear and re-register. Maybe instead of original_input.type, I can compare the param names to the input_names from the original_input
so that worked to compare the original_input.input_names to the param names. If they are different or dont exist, then they I remove the params and reinitialize them.