Programmatically creating new C# & Python script components

Hi McNeel and other experts

Do you know the best way to programmatically create a RhinoCodePluginGH component (Rhino 8 C# and Python script components)?

I have these aspects in mind:

  • creating a new script component instance
  • setting the inputs & outputs parameters, with type hints and persistent data
  • setting the source code & ensuring that the RunScript method signature matches the inputs & outputs

In my experimentation so far, for Rhino 7, this works well:

  • a new instance is created through Activator.CreateInstance
  • set up inputs & outputs by
    • creating Param_ScriptVariable objects
    • assigning objects to Param_ScriptVariable.TypeHint to set the type, and
    • adding them via Params.RegisterInputParam and Params.RegisterOutputParam.
  • to set the source code using,
    • for C#: the ScriptSource.ScriptCode, ScriptSource.AdditionalCode and ScriptSource.UsingCode properties,
    • and for Python, the Code property

However for Rhino 8:

  • I could actually set the inputs and outputs the same way using the legacy registration, but somehow it ends up with the RunScript signature seemingly not respecting the type hints, not reflecting these inputs/outputs, so it doesn’t work. Intriguingly, when I look at the XML for the component instance created this way, the ConverterData chunk (needed for Rhino 8 it seems) isn’t there
  • there seems to be no source code property, although I find that serialising it to XML, using XDocument to parse and find the Script chunk, encoding the code to base64 and assigning it to the Text item before deserialising it into a component instance, seems to work - except for the RunScript signature of course - but this does feel a little ‘hacky’

Is there a better way to do it? There might be no public API for it but if anyone can share the next best thing, the next most reliable mechanism, I’d much appreciate it. Is there a plan at McNeel to offer a sanctioned way to do this at some point?

I did actually fid one way that seems to work for Rhino 8, fulfilling all my requirements: I have a template script component (that I load from file or have stored as static content) which has one of every type of input, and I just clone the parameters and add them to a newly created instance, before setting the source code using the XML method. The types are then respected & reflected in the RunScript signature, and it works.

Any thoughts on this?

1 Like

Sorry for the late reply, I’m interested to know more about your approach (I’ve only just come across your post while searching for a way to change the font size in code).

Full support is on Ehsan’s to do list: https://mcneel.myjetbrains.com/youtrack/issue/RH-81754
Rhino 8 Python Grasshopper Component.Code Property - #18 by eirannejad

The Compas-dev team produced some great tools. I’m not sure if they compile the scripts, and I’ve not had chance to test them. compas-actions.ghpython_components/componentize_cpy.py at main · compas-dev/compas-actions.ghpython_components · GitHub

The approach I used in Rhino 7 to create ghuser components, was to create a basic bare GH Python component, with no inputs and outputs whatsoever, but with its Name and NickName set (possibly the Description too for the tooltip).

I found it troublesome to add Params on a component from an external location, even when its disabled. Instead, each component knows its own name, so works out the required input and output args it needs from a common imported Python package or the plug-in.

It then adds its Input and Output Params itself, the first time RunScript runs. I’ve not tried it to build CPython components yet, but it built the old GH Python ones in Rhino 8 last year.

It’s by no means super efficient. But by starting from none whatsoever, it doesn’t care if any extra Params are added, so can supports the zoomable interface, and the user can add on whatever extra Params they like.

I added a ticket to cleanup the script component creation API. We do have a method to create but the parameters on script components are custom so adding parameters is more complicated

RH-86266 Provide API to create script components programmatically

2 Likes

I actually found the Rhino 7 components (ScriptComponents.Component_CSNET_Script and GhPython.Component.ZuiPythonComponent) easier compared to the Rhino 8 ones (in the RhinoCodePluginGH.Components namespace, since the Rhino 7 ones have settable properties for source code, and there is the standard GH_Component.Params.RegisterInputParam and ..RegisterOutputParam methods.

The Rhino 8 ones don’t have a settable code property, and in fact if you look at the XML representation, they have a whole new Script chunk which contains a Text item to store a base64-encoded string of the script source code as well as a LanguageSpec chunk, too.

Currently my approach for all script types isto:

  1. Create component object

creating a new orphan object using Activator.CreateInstance and passing in the type object of what I want to create, e.g.:

var typeScript = Type.GetType("RhinoCodePluginGH.Components.CSharpComponent, RhinoCodePluginGH");
var scriptComponentObject= (GH_Component)Activator.CreateInstance(typeScript);
scriptComponentObject.CreateAttributes();
  1. Create and add Params objects

For Rhino 7, new inputs and outputs can be created just by new Param_ScriptVariable() and adding them to the object using the register/unregister methods.
For Rhino 8, every time I try something like this, the type hints don’t seem to be recognised so I’ve manually prepared XML archive that contains one of each Rhino 8 script component, with each having one of each possible input type. So I just open that file up as an archive, extract the template script component and the relevant template input and output objects from there, clone them (using Activator.CreateInstance again) and assign them via the register/unregister methods.

  1. Set source code

Rhino 7 has exposed properties for this, like the following for C# (if you excuse my use of dynamic to avoid direct references to the libraries):

((dynamic)scriptComponentObject).ScriptSource.ScriptCode = scriptCode;
((dynamic)scriptComponentObject).ScriptSource.AdditionalCode = additionalCode;
((dynamic)scriptComponentObject).ScriptSource.UsingCode = usingsCode;

For Rhino 8 though the only way I found to work so far is to serialise this new script object to XML, then parse it using XDocument to find the appropriate Text item inside the Script chunk and set it it there.

  1. Manual (perhaps slightly hacky) ‘state reset’ for Rhino 8 components

After setting source code via XML and deserialising back into the object, it needs one more serialise & deserialise for the object to be in a state that seems to match the result of doing all of this manually in the UI.

@James_Parrott When you say

I found it troublesome to add Params on a component from an external location, even when its disabled

Did you mean adding Params using the register/unregister methods? If not, what other method of adding did you have in mind? Could you share any source code for that, out of curiosity?

Could you comment on the approach in my answer to James above, @eirannejad ? It’s unconventional, sure, but in your opinion will it be fairly reliable until you update the functionality with public methods to accomplish the same thing?

Just to clarify, my aim is to programmatically do all of the following:

  • create scripting objects
  • set inputs and outputs of any available type
  • set persistent data to any input
  • set source code
  • end up with the component being in the same state as what is achieved through manual setting up of a script component in the UI

On that last point, I did have it in Rhino 8 where, after setting the source code via the XML ‘detour’, the object on my canvas did appear to have the updated source code and all the correct inputs & outputs but something about the state wasn’t quite finalised, as evidenced by the fact that opening the script viewer manually and closing it (without having changed anything) caused it to still ask whether I wanted to save the changes.

This was only seemingly resolved once I did another quick XML ‘detour’ but this time just serialise and deserialise (without any changes in between).

I’m still finding a similar symptom for just the Rhino 8 Python 3 components in SDK mode, specifically.

If you really don’t recommend any of these actions, or you have a better interim way of doing any of these things I’ve mentioned, I’d really appreciate some extra guidance/tips. Thanks!

Thanks for that. Really interesting, and good to know if it might need those activators and working archives. It’d be great to have a cleaner method for Rhino 8’s new components, but I think I’ll stick with the old ones for now.

Did you mean adding Params using the register/unregister methods?

Yes, but there’s a more too it. I might have misremembered where the trouble occurred - I initially tried to build everything from a builder component, and ended up dropping the idea. But I wanted every component to manage itself anyway, to allow it to dynamically change its tools and Params, and to let the user update the plug-in version without having to place a new component instead of each existing one. But I never really got deleting params to work (e.g. with the Params.UnregisterInputParameter), so I just designed a system where I dind’t need to anyway (this avoids deleting user added params too). The full code is a lot more complicated, but the essence of updating Params from code is:

  • create an SDK mode GHPython component
  • make its RunScript(self, *args): method run some other code suite or method idempotently, once (and only once) when the condition to update the params is met, e.g. when the component’s NickName changes.
  • at the end of this code suite or back in RunScript, do something that ensures the params change condition is False the next time RunScript runs for the idempotency (avoiding retriggering it and an infinite cycle), then return early, instead of doing the normal stuff.
  • RunScript will get re-run afterwards by the param update, but then the idempotent code should be skipped (as long as there was no second external trigger):
        Params = self.Params # in RunScript(self, *args) on a ghpythonlib.componentbase.executingcomponent subclass called MyComponent

        ParamsSyncObj = Params.EmitSyncObject()

        for name, nick_name, description in [("Param_name", "P", "An example Parameter"),]:


            made_param = Param_ScriptVariable()

            made_param.TypeHint = GhPython.Component.GhDocGuidHint()

            for k, v in dict(NickName = nick_name,
                             Name = name,
                             Description = description,
                             Access = Grasshopper.Kernel.GH_ParamAccess.list,  # or .item or .tree 
                             Optional = True, #I can't remember trying False
                             ).items():
                setattr(made_param, k, v)

            Params.RegisterInputParameter(made_param) 
            Params.OnParametersChanged()

        Params.Sync(ParamsSyncObj)
        Params.RepairParamAssociations()

When returning from RunScript too, the same number of args need to be returned (even if they’re all None) as the length of self.Params.Output.

Great info. Moving this to 8.x and will update soon

Hi,
first off thank you @nicolaasburgers for bringing this up. I have been looking for a way to specifically access the source code of already existing components programmatically for a while.

In that sense - exposing the source code of component objects via a property again would help loads in development! Reason in my case is saving source code of all scripts in the document separately as files. Worked like a charm in Rhino 7 but almost not doable with the new components since the source code is not exposed (yes, the (De)-Serialize hack works but I think it’s bit too hacky to use on a day by day basis).

Any work on this highly appreciated! Best wishes
Max

Working on this at the moment. Creating a decent api to create script components with support for overriding scripts, parameters, type hints, etc.

2 Likes

Going to put this in the next Rhino. I would appreciate your feedback and improvement ideas:

# r "RhinoCodePluginGH.rhp"                 // this is necessary to access script component types
using System;
using Grasshopper;
using Grasshopper.Kernel;

// namespaces to access script components and variable parameter
using RhinoCodePluginGH.Components;
using RhinoCodePluginGH.Parameters;

// this creates a python 3 component.
// could also be:
// CSharpComponent.Create
// IronPython2Component.Create
Python3Component c = Python3Component.Create("MyScript", @"
# here is your custom python script going
# into the component
");

var p = new ScriptVariableParam("first")    // this is the variable name for script
{
    PrettyName = "First Input",             // pretty name is human-readable
    ToolTip = "This is the first input",
    Optional = true,
    Access = GH_ParamAccess.list,
    AllowTreeAccess = true,
};
p.TypeHints.Select(typeof(double));         // .TypeHints is new helper to set type hints
p.CreateAttributes();
c.Params.RegisterInputParam(p);

p = new ScriptVariableParam("second")
{
    PrettyName = "Second Input",
    ToolTip = "This is the second input",
    Optional = true,
    AllowTreeAccess = true,
};
p.TypeHints.Select("Point3dList");          // .TypeHints helper can set hints by name as well
p.CreateAttributes();
c.Params.RegisterInputParam(p);

p = new ScriptVariableParam("output")
{
    Hidden = true,
};
p.CreateAttributes();
c.Params.RegisterOutputParam(p);

c.VariableParameterMaintenance();           // necessary step after changing component parameters


// get and set script on a component
c.TryGetSource(out string source);
c.SetSource("# new source code to be set into component");

// special parameters can be accessed and enabled if necessary
c.UsingStandardOutputParam = true;
c.GraftStandardOutputLines = true;

// marshalling settings can be changed as well
c.MarshGuids = false;

// parameters can also be applied or collected from the source
c.SetParametersToScript()        // this updates RunScript signature to match component params
c.SetParametersFromScript()      // this updates the component parameters from RunScript signature
1 Like

Hi @eirannejad great to read you’re actioning this and in general it looks good to me but I’ll share some minor/pedantic and also expansive thoughts/questions I have so far:

  • Could you set the index (position) for any new input or output parameter and ensure that any sources/recipients connected to the other existing input/output parameters remain unaffected?
  • I’m guessing for Python you’d need to use strings like "float" and "str" to specify some types rather than typeof()?
  • TypeHints - there is only one type hint per parameter so should this be TypeHint?
  • Would you set Grafted/Flattened/Simplified using the DataMapping property and persistent data using the SetPersistentData method?
  • Will the be a property that indicates whether the source code assigned to the script component has been recognised as SDK mode or plain? Are you planning on having something like an IsSDKMode property?
  • Which methods/property assignments you showed in your code example will trigger events like ObjectChanged?
  • Could there be a way to control refreshes of the UI in any way so that I could add remove input/output parameters, change the source code, set/remove persistent data that would visually appear as one instant set of changes to a script component?
  • Tangential question: for my particular purpose (DefinitionLibrary) I’m seeking a property in these these RhinoCodePluginGH commponents I can use to store a value which is (at least relatively) hidden from view and which persists when a component is copy-pasted - I’m using Tooltip for this now but I could alternatively use the Description property for this but currently it seems prone to being wiped when a file is re-loaded; could that be fixed so that its value is retained?

@nicolaasburgers

  • Could you set the index (position) for any new input or output parameter and ensure that any sources/recipients connected to the other existing input/output parameters remain unaffected?

You should be able to use the Component.Param.Input or .Output Lists to swap input or output parameters. The connection info is stored on the parameter itself so it moves with it.

  • I’m guessing for Python you’d need to use strings like "float" and "str" to specify some types rather than typeof()?

No the component is smart enough to convert typeof(double) to float for python and same for strings.

  • TypeHints - there is only one type hint per parameter so should this be TypeHint?

.TypeHints gives access to the collection of type hints that are available on a parameter and is iterable. .Select() methods set one of those on the parameter. Let me know if this api does not make sense.

  • Would you set Grafted/Flattened/Simplified using the DataMapping property and persistent data using the SetPersistentData method?

Yeah ScriptVariableParam is just like any other Grasshopper param so any non-scripting stuff should work as usual.

  • Will the be a property that indicates whether the source code assigned to the script component has been recognised as SDK mode or plain? Are you planning on having something like an IsSDKMode property?

Good call! I forgot about this. I will add methods to check IsSDKMode and also to convert and add preview override methods.

  • Which methods/property assignments you showed in your code example will trigger events like ObjectChanged?

Not sure what you mean or why this matters. Expand a bit please.

  • Could there be a way to control refreshes of the UI in any way so that I could add remove input/output parameters, change the source code, set/remove persistent data that would visually appear as one instant set of changes to a script component?

Grasshopper should not be redrawing the UI while your code is making changes to the component.

  • Tangential question: for my particular purpose (DefinitionLibrary) I’m seeking a property in these these RhinoCodePluginGH commponents I can use to store a value which is (at least relatively) hidden from view and which persists when a component is copy-pasted - I’m using Tooltip for this now but I could alternatively use the Description property for this but currently it seems prone to being wiped when a file is re-loaded; could that be fixed so that its value is retained?

I have a ticket to provide Persistent storage support to scripts:
RH-84667 Support persistent state for script components

Added SDK Mode test and conversion as well

Python3Component c = Python3Component.Create("MyScript");

c.IsSDKMode
c.ConvertToSDKMode(addSolve: false, addPreview: false);

This ticket is also related
RH-84668 Support any GH param on script component

.TypeHints gives access to the collection of type hints that are available on a parameter and is iterable. .Select() methods set one of those on the parameter. Let me know if this api does not make sense.

Thanks, I get the TypeHints idea now.

And for ObjectChanged:

Not sure what you mean or why this matters. Expand a bit please.

I’m writing some code which should display something when the source code of a script component changes, so I’m generally curious about when events will fire so that I can write code for this efficiently.

When a person manually adds new inputs/outputs or changes the code then those are usually singular events and my code can run every time, but if code like your example runs would cause events to fire every time then that’s something I’d need to keep in mind.

Grasshopper should not be redrawing the UI while your code is making changes to the component.

So I’m guessing it just naturally redraw when the user does something on the canvas or through a call to Grasshopper.Instances.ActiveCanvas.Refresh()?

Cheers for this, it will make things much easer.

RH-86266 is fixed in Rhino 8 Service Release 18 Release Candidate

1 Like

This is nuts! Thank you so much, I just discovered the TryGetSource() by chance, made my day! :blush:

1 Like