Python3 component RunScript() parameter names changing

hello, a couple questions here. We had some great components that would change the input parameters of the GH component based on selections in the context menu and a *args input in the RunScript method. This was working great in Rhino7 and we had basically based our entire workflow on this.

I am trying to update our code for Rhino8 and Python 3, and there are a few things I haven’t figured out.

  • First off, when I change an input name on the component, it automatically changes the RunScript() signature. Is there a way to turn this off so that the code can adapt to different input parameters? The previous method of using the arguments positionally was preferrable. I see the documentation about it here
  • After some testing, there is some funny behavior. When I add an input parameter via the script, it does not change the RunScript input parameters. This is what I want, but then I don’t see how to access the value from that input. If I put a *args as an input parameter in the RunScript method, it just creates a component input named *args.
  • The Python 3 script component inherits from GH_ScriptInstance, but I want to use some GH_Component functions, for example I wanted to try using the CollectData() toget the input data without going through the RunScript signature. can I inherit from GH_Component, or is that only when we develop proper plugins?
  • the AppendAdditionalMenuItems() override doesn’t seem to be implemented. Is this the case? are there plans to implement?

Thanks for any help you can offer!

@OBucklin

1 and 2: Would you mind sending me a good Rhino 7 example for your workflow so I can match the same behaviour in the new scripting component regarding RunScript signatures?

  • RE RH-84580 Support for optional values in RunScript signature

3: No. As in Rhino 7, scripts can only implement GH_ScriptInstance which is a controlled interface. However, you can access the script component using self.Component property of this class instance.

4: This is implemented but not yet documented in Rhino >= 8.13.

using SWF = System.Windows.Forms;

public class Script_Instance : GH_ScriptInstance
{
    private void RunScript()
    {
    }

    public override void AppendAdditionalMenuItems(SWF.ToolStripDropDown menu)
    {
        menu.Items.Add("Script Action", default, (s, e) => {
            // do things here
            this.Component.ExpireSolution(true);
        });
        menu.Items.Add(new SWF.ToolStripSeparator());
    }
}

Hey again!

Thanks for the information.

Here is the component script, it’s a bit long, since I left all the documentation in. I’ll upload a .gh file too in case it’s easier.

Currently I name the output with the _type which keeps the inputs connected to upstream components. On my wishlist is the ability to have the _type value persist between sessions in a more natural way.
Maybe that will be solved with the new python package compiler…?

from ghpythonlib.componentbase import executingcomponent as component
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning
import Grasshopper


class DirectJointRule(component):
    def __init__(self):
        super(DirectJointRule, self).__init__()

        if ghenv.Component.Params.Output[0].NickName == "out":
            self._type = None
        else:
            self._type = ghenv.Component.Params.Output[0].NickName

    def RunScript(self, *args):
        if not self._type:
            ghenv.Component.Message = "Select type from context menu (right click)"
            self.AddRuntimeMessage(Warning, "Select type from context menu (right click)")
            return None
        else:
            ghenv.Component.Message = self._type
            input_names = self.arg_names()[self._type]
            input_values = args
            string = "inputs are: {}".format(zip(input_names, input_values))

            return string

    def arg_names(self):
        return  {"a": ["arg_1"], "b": ["arg_1","arg_5"],"c": ["arg_1","arg_8","arg_9"]}

    def AppendAdditionalMenuItems(self, menu):
        for name in self.arg_names().keys():
            item = menu.Items.Add(name, None, self.on_item_click)
            if self._type and name == self._type:
                item.Checked = True

    def on_item_click(self, sender, event_info):
        self._type = str(sender)
        rename_gh_output(self._type, 0, ghenv)
        manage_dynamic_params(self.arg_names()[self._type], ghenv, rename_count=0, permanent_param_count=0)
        ghenv.Component.ExpireSolution(True)




def add_gh_param(
    name, io, ghenv, index=None
):  # we could also make beam_names a dict with more info e.g. NickName, Description, Access, hints, etc. this would be defined in joint_options components
    """Adds a parameter to the Grasshopper component.

    Parameters
    ----------
    name : str
        The name of the parameter.
    io : str
        The direction of the parameter. Either "Input" or "Output".
    ghenv : object
        The Grasshopper environment object.

    Returns
    -------
    None

    """
    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.item
        param.Optional = True
        if not index:
            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 clear_gh_params(ghenv, permanent_param_count=1):
    """Clears all input parameters from the component.

    Parameters
    ----------
    ghenv : object
        The Grasshopper environment object.
    permanent_param_count : int, optional
        The number of parameters that should not be deleted. Default is 1.

    Returns
    -------
    None

    """
    changed = False
    while len(ghenv.Component.Params.Input) > permanent_param_count:
        ghenv.Component.Params.UnregisterInputParameter(
            ghenv.Component.Params.Input[len(ghenv.Component.Params.Input) - 1], True
        )
        changed = True
    ghenv.Component.Params.OnParametersChanged()
    return changed


def rename_gh_input(input_name, index, ghenv):
    """Renames a parameter in the Grasshopper component.

    Parameters
    ----------
    ghenv : object
        The Grasshopper environment object.
    input_name : str
        The new name of the parameter.
    index : int
        The index of the parameter to rename.

    Returns
    -------
    None

    """
    param = ghenv.Component.Params.Input[index]
    param.NickName = input_name
    param.Name = input_name
    param.Description = input_name
    ghenv.Component.Params.OnParametersChanged()


def rename_gh_output(output_name, index, ghenv):
    """Renames a parameter in the Grasshopper component.

    Parameters
    ----------
    output_name : str
        The new name of the parameter.
    index : int
        The index of the parameter to rename.
    ghenv : object
        The Grasshopper environment object.

    Returns
    -------
    None

    """
    param = ghenv.Component.Params.Output[index]
    param.NickName = output_name
    param.Name = output_name
    param.Description = output_name
    ghenv.Component.Params.OnParametersChanged()


def manage_dynamic_params(input_names, ghenv, rename_count=0, permanent_param_count=1, keep_connections=True):
    """Clears all input parameters from the component.

    Parameters
    ----------
    input_names : list(str)
        The names of the input parameters.
    ghenv : object
        The Grasshopper environment object.
    permanent_param_count : int, optional
        The number of parameters that should not be deleted. Default is 1.

    Returns
    -------
    None

    """
    if not input_names:  # if no names are input
        clear_gh_params(ghenv, permanent_param_count)
        return
    else:
        if keep_connections:
            to_remove = []
            for param in ghenv.Component.Params.Input[permanent_param_count + rename_count :]:
                if param.Name not in input_names:
                    to_remove.append(param)
            for param in to_remove:
                param.IsolateObject()
                ghenv.Component.Params.UnregisterInputParameter(param, True)
            for i, name in enumerate(input_names):
                if i < rename_count:
                    rename_gh_input(name, i + permanent_param_count, ghenv)
                elif name not in [param.Name for param in ghenv.Component.Params.Input]:
                    add_gh_param(name, "Input", ghenv, index=i + permanent_param_count)

        else:
            register_params = False
            if (
                len(ghenv.Component.Params.Input) == len(input_names) + permanent_param_count
            ):  # if param count matches beam_names count
                for i, name in enumerate(input_names):
                    if (
                        ghenv.Component.Params.Input[i + permanent_param_count].Name != name
                    ):  # if param names don't match
                        register_params = True
                        break
            else:
                register_params = True
            if register_params:
                clear_gh_params(
                    ghenv, permanent_param_count + rename_count
                )  # we could consider renaming params if we don't want to disconnect GH component inputs
                for i, name in enumerate(input_names):
                    if i < permanent_param_count:
                        continue
                    elif i < rename_count:
                        rename_gh_input(name, i, ghenv)
                    else:
                        add_gh_param(name, "Input", ghenv)

InteractiveComponentDemo.gh (13.4 KB)

1 Like

Thanks a lot for sending this over. There are a few points here for me:

  1. First is allowing RunScript to include *args
    RH-84580
  2. Storing state on a parameter name, although works in this example, is not ideal IMHO. I would rather implement Write and Read methods on the script instance so your script can have persistent state (more than one string), stored in the gh file.
    RH-84667
  3. The script is replacing input and output parameters with Param_GenericObject instances. Script component uses special inputs and outputs that support the functionality it provides. I need to make modifications to make sure Script component can work with other parameter types as well.
    RH-84668
2 Likes

Ok great!

I’ll look forward to those updates.

Thanks!

1 Like

Hello Ehsan,

I was trying to access input params using Component as you suggested, but self.Component returns None. Any suggestions for how to access those parameters?

Thanks!