Generate inputs to Python components

Hi,

In C# grasshopper components I add inputs so that a user would not need to guess them.

How can I run a similar function for python non-compiled components (ghuser objects)?


        public override void AddedToDocument(GH_Document document)
        {
            base.AddedToDocument(document);

            //Add sliders
            double[] sliderValue = new double[] { 5, 2, 1, 0.5, 0.0 };
            double[] sliderMinValue = new double[] { 1, 1, 0.1, 0.01, -10 };
            double[] sliderMaxValue = new double[] { 10, 10, 10, 1, 10 };
            int[] sliderID = new int[] { 1, 2, 4, 5, 6 };

            for (int i = 0; i < sliderValue.Length; i++)
            {
                Grasshopper.Kernel.Parameters.Param_Number ni = Params.Input[sliderID[i]] as Grasshopper.Kernel.Parameters.Param_Number;
                if (ni == null || ni.SourceCount > 0 || ni.PersistentDataCount > 0) return;
                Attributes.PerformLayout();
                int x = (int)ni.Attributes.Pivot.X - 250;
                int y = (int)ni.Attributes.Pivot.Y - 10;
                Grasshopper.Kernel.Special.GH_NumberSlider slider = new Grasshopper.Kernel.Special.GH_NumberSlider();
                slider.SetInitCode(string.Format("{0}<{1}<{2}", sliderMinValue[i], sliderValue[i], sliderMaxValue[i]));
                slider.CreateAttributes();
                slider.Attributes.Pivot = new System.Drawing.PointF(x, y);
                slider.Attributes.ExpireLayout();
                document.AddObject(slider, false);
                ni.AddSource(slider);
            }
        }

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 :slight_smile:

The api calls you need are mostly just the Python versions of what you already have, e.g.

slider = Grasshopper.Kernel.Special.GH_NumberSlider()

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


Hey this is great!

How would I go about removing those inputs if for example the component was disconnected?

Thanks!

Thanks :slight_smile:

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

Hey James,

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.

Thanks for any help you can offer!

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])
3 Likes

Well done. Thanks for sharing your code.

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.

  1. 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.

  2. 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ā€¦

  3. 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

Iā€™ll give that a shot!

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.

Anyways, thank you so much for the tips!

1 Like