Creating New Nodes

Hi All,

Just wondering if anyone has any experience creating icon/nodes from functions.

To explain what I am trying to do:

I have created a few functions using the basic method shown in the 2019_02 GC SIG "The GC SDK Digging_Deeper".

Using just a script in simple string form, as a easy way to get into creating Add-ins. 

That all works fine as and we can utilise the function from the function dialog or using a function call.

I would however like to create an icon/node that I can link to this function.

I have created an icon/node and placed it into a NodeTypePaletteCateory etc 

Just wondering what how I would go about calling the function I created? I assume I would need to create a funcitoncall as an input and populate it with the new function I created. 

We are trying to make the custom function more accessible to new users that are more familiar with node based workflows.

Any help would be appreciated.

Will post the results, maybe someone will find it useful.

Thanks

Wayne

  • Hi Wayne,

    I'm not familiar with that SIG, but I assume you're talking about a custom function and a custom node type that you've implemented in a GC add-in, which you created in Visual Studio.

    From a GC user's point of view, there should be no difference between your custom function and any other GC function. Likewise, there should be no difference between your custom node type and any other GC node type. In other words, the user shouldn't need to know -- or care -- where that function and node type came from.

    Just as in any other scenario, you should be able to call your custom function within an expression that you provide to an input to your custom node. Or, you should be able to create a FunctionCall node that calls your custom function, and then reference that FunctionCall node in an expression that you provide as an input to your custom node.

    Or am I off track from what you're asking?

    Jeff

  • Hi Jeff,

    Thanks for responding.

    Sorry my question was a bit vague on what I am exactly trying to do.

    As an example I wrote a new add-in by just using gcscript language in Visual Studio. I will attache an example below.

    This makes it easy as someone that is not familiar with Visual Studio to get an add-in for delivering function to a team.

    Basically found the handy line:

    GCScriptTools.ExecuteInItsEntirety(GCTools.UIGCSpace(), scriptwayne, ScriptFlags.UserAuthored, BreakpointAcknowledgement.Ignore);

    What i would like to do now is take that function and present it as a icon/node to the user in a new PaletteCateory.

    I managed to create an icon and a new Palette Category but I am struggling on how to now call the function in Visual Studio context.

    As an example I have included a bit of script from Visual Studio I was using for the icon.

    Hope that makes sense

    Thanks again 
    Wayne

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Bentley.GenerativeComponents;
    using Bentley.GenerativeComponents.AddInSupport;
    using Bentley.GenerativeComponents.Features;
    using Bentley.GenerativeComponents.Features.Specific;
    using Bentley.GenerativeComponents.GeneralPurpose;
    using Bentley.GenerativeComponents.GCScript;
    using Bentley.GenerativeComponents.GCScript.FundamentalValues;
    using Bentley.GenerativeComponents.GCScript.GCTypes;
    using Bentley.GenerativeComponents.GCScript.NameScopes;
    using Bentley.GenerativeComponents.MicroStation;
    using Bentley.Interop.MicroStationDGN;
    using Bentley.GeometryNET;
    using System.ComponentModel;
    using Bentley.GenerativeComponents.View;
    
    
    namespace SampleAddIn
    {
        [GCNamespace("User")]                                   // The GCNamespace attribute lets us specify where this SimpleLine node
                                                                // type will appear within GCScript's namespace tree (that is, the
                                                                // namespaces that are perceived by the GC user). This namespace has no
                                                                // relation to our C# namespace, which is (in this case) SampleAddIn.
    
        [NodeTypePaletteCategory("Wayne")]              // The NodeTypePaletteCategory attribute lets us specify where this
                                                                // SimpleLine node type will appear within GC's Node Types dialog.
                                                                // So, it will appear within a group named "Sample Add-In".
    
        [NodeTypeIcon("Resources/SimpleLineNode.png")]
    
        [Summary("Is a script i wrote")]
    
    
    
    
        public class ScriptFunctions
    
    
        //static internal class ScriptFunctions
        {
            static internal void Load()
            {
                // This method is called from within the constructor of the class, Initializer (within this project). So, this
                // method will be called automatically whenever the user loads this assembly, SampleAddIn, into GC.
    
                IGCEnvironment environment = UniversalGCEnvironment.TheOnlyInstance;
                NameCatalog nameCatalog = environment.TopLevelNameCatalog();
    
                // To add a new function to the GCScript processor, we call the method, nameCatalog.AddNamespaceLevelFunction.
                //
                // 1. The first argument is the name of the new function, as the GC user will see it. (It's our responsibility
                //    to ensure that the name doesn't conflict with another top-level name in GC.)
                //
                // 2. The second argument is the type (also known as the signature) of the new function, expressed in GCScript
                //    language.
                //
                // 3. The third argument is the name of the C# method that implements the new function. That method can have any
                //    name; however, we recommend that it have the same name as the function.
    
                
    
                DefineScriptFunctionsWrittenInScript();
            }
    
            // Each method that implements a script function may be prefaced by a 'Summary' attribute, which provides user
            // documentation for that function. The documentation text will appear in GC's Functions dialog. (The presence
            // or absence of a Summary attribute has no effect on how the function may be used in GC.)
    
           
    
    
    
            static void DefineScriptFunctionsWrittenInScript()
            {
                // The following script is copied from the contents of the script transaction that results if we define one or more
                // new script function in the Functions dialog, then commit that transaction.
    
                string scriptwayne = "global redeclare object WayneReverse(object List)\n" +
                                "{\n" +
                                "object NewList = Reverse(List);\n" +
                                "return NewList;\n" +
                                "}\n";
    
    
                // Now run the aforementioned script, which results in the script functions (in this case, the one function 'Angle360') being defined,
                // as though we were running a script transaction containing that script.
    
                GCScriptTools.ExecuteInItsEntirety(GCTools.UIGCSpace(), scriptwayne, ScriptFlags.UserAuthored, BreakpointAcknowledgement.Ignore);
                
            }
            
           
    
            
    
        }
    }
    

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using Bentley.GenerativeComponents;
    using Bentley.GenerativeComponents.AddInSupport;
    using Bentley.GenerativeComponents.Features;
    using Bentley.GenerativeComponents.GCScript;
    using Bentley.GenerativeComponents.GCScript.NameScopes;
    using Bentley.GenerativeComponents.GeneralPurpose;
    using Bentley.GenerativeComponents.MicroStation;
    using Bentley.GenerativeComponents.View;
    using Bentley.Interop.MicroStationDGN;
    using Bentley.GenerativeComponents.GCScript.FundamentalValues;
    using Bentley.GenerativeComponents.GCScript.GCTypes;
    using Bentley.GeometryNET;
    using Bentley.GenerativeComponents.UtilityNodes;
    
    namespace SampleAddIn
    {
        [GCNamespace("Waynetest")]                              // The GCNamespace attribute lets us specify where this SimpleLine node
                                                                // type will appear within GCScript's namespace tree (that is, the
                                                                // namespaces that are perceived by the GC user). This namespace has no
                                                                // relation to our C# namespace, which is (in this case) SampleAddIn.
    
        [NodeTypePaletteCategory("Wayne")]              // The NodeTypePaletteCategory attribute lets us specify where this
                                                        // SimpleLine node type will appear within GC's Node Types dialog.
                                                        // So, it will appear within a group named "Sample Add-In".
    
        [NodeTypeIcon("Resources/SimpleLineNode.png")]          // The NodeTypeIcon attribute lets us specify the graphical image (icon)
                                                                // that will appear on the SimpleLine node type's button within GC's Node
                                                                // Types dialog.
    
        [Summary("A line that connects two points in space.")]  // The Summary attribute lets us provide a brief description of this
                                                                // node's intended purposed. The text will be displayed when the user
                                                                // hovers over our node type in GC's Node Types dialog.
    
        public class WayneSimpleLine: UtilityNode
        {
    
           
        } // class
    
    } // namespace
    
    

  • Hi Wayne,

    I'm afraid your approach is a bit misguided. Regardless of how they've been implemented, GC functions are intended to be called only from within (other) script code, not from C# code.

    Alternatively, I recommend this approach for writing your add-in:

    1. Implement your custom function as a purely-C# method, to be called only from C# code. It will take appropriate C#-level arguments, and return an appropriate C#-level value.

    2. Within your custom node type, of course you can directly call that purely-C# method.

    3. To expose that functionality to GC as a script-callable function, you will write a C# wrapper method having the same general appearance as the sample method, ScriptFunctions.ConvertTemperature, which is in GC's sample add-in, VS2013.sln. Within the body of your wrapper method, you'll "unbox" the CallFrame arguments, then call your purely-C# method. Then you'll return the C# result to GC by calling callFrame.SetFunctionResult. (Again, this is all shown in that sample method.)

    Thus, the same underlying functionality will be shared by both your custom node type and your custom script-callable function.

    HTH

    Jeff

  • Hi Jeff,

    Thanks for the info, I have "headed back to school" on C# and learning a lot more.

    So I started with a goal of creating a node that will filter a list with another Boolean list.

    in C# I created something using my very basic C# skills which looked like this:

                static void Main(string[] args)
            {
    
                var mylist = new string[] {"a1","a2","a3","a4","a5","a6","a7","a8","a9" };
                var myfilter = new Boolean[] { true, false, true, true, true, false, false, true, true };
    
                List<string> myfilteredlist = new List<string>();
                
                for (int i = 0; i < mylist.Length; i++)
                {
                    if (myfilter[i])
                    {
                        myfilteredlist.Add(mylist[i]);
                    }
    
                }
                foreach (var myitems in myfilteredlist)
                {
                    Console.WriteLine(myitems);
                }
                
            }

    very basic but I appears to do the job.

    Now trying to implement that into a GC node by taking 2 inputs (mylist,myfilter) and output a new filtered list.

    reading and re-reading the Calculator example I am up to here so far but a bit stuck now as I am not getting anything appearing in GC yet. I know it will not work yet but I was expecting it to display the node icon even if it didn't do anything yet.

    If you have any time I would appreciate any tips.

    using System;
    using System.Collections.Generic;
    using Bentley.GenerativeComponents;
    using Bentley.GenerativeComponents.GCScript;
    using Bentley.GenerativeComponents.GCScript.GCTypes;
    using Bentley.GenerativeComponents.GCScript.NameScopes;
    using Bentley.GenerativeComponents.GCScript.ReflectedNativeTypeSupport;
    using Bentley.GenerativeComponents.GeneralPurpose;
    using Bentley.GenerativeComponents.UtilityNodes;
    using Bentley.GenerativeComponents.View;
    
    
    
    namespace FilterList
    {
        [GCNamespace("User")]
        [NodeTypePaletteCategory("wayne")]
        [NodeTypeIcon("Resource/wayne.png")]
        // The name of the Node Pallet and the icon
    
        class Program: UtilityNode // setting it as a Utility Node rather than a node node.
        {
            // below are required lines (I think)
            static readonly NodeGCType s_gcTypeOfAllInstances = (NodeGCType)GCTypeTools.GetGCType(UniversalGCEnvironment.TheOnlyInstance, typeof(Program));
    
            static public NodeGCType GCTypeOfAllInstances
            {
                get { return s_gcTypeOfAllInstances; }
            }
            static void AddAdditionalMembersToGCType(IGCEnvironment environment, GCType gcType, NativeNamespaceTranslator namespaceTranslator)
            {
            //setup the parameters and technique as default
            
                UtilityNodeTechnique technique1 = gcType.AddDefaultNodeTechnique("Default", DefaultTechnique);
                technique1.AddParameterDefinition(environment, "InputList", typeof(string[]), "",
                                                 Ls.Literal("The list to be filtered"));
    
                technique1.AddParameterDefinition(environment, "FilterList", typeof(Boolean[]), null,
                                                  Ls.Literal("The filter list (Boolean list)"));
    
                technique1.AddParameterDefinition(environment, "ResultList", typeof(string[]), "",
                                      Ls.Literal("The filtered list."), NodePortRole.TechniqueOutputOnly);
            }
            static NodeUpdateResult DefaultTechnique(UtilityNode node, NodeUpdateContext updateContext)
            {
            // this is just filler till i can work out this part
                Program myfilter = (Program) node;
                return NodeUpdateResult.Success;
            }
                static void Main(string[] args)
            {
    
                var mylist = new string[] {"a1","a2","a3","a4","a5","a6","a7","a8","a9" };
                var myfilter = new Boolean[] { true, false, true, true, true, false, false, true, true };
    
                List<string> myfilteredlist = new List<string>();
                
                for (int i = 0; i < mylist.Length; i++)
                {
                    if (myfilter[i])
                    {
                        myfilteredlist.Add(mylist[i]);
                    }
    
                }
                foreach (var myitems in myfilteredlist)
                {
                    Console.WriteLine(myitems);
                }
                
            }
        }
    }
    

    Thanks

    Wayne

  • Hi Wayne,

    Here's a working version of your new node type. (I copied and modified the 'Calculator' node from the sample add-in.)

    HTH

    Jeff

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using Bentley.GenerativeComponents;
    using Bentley.GenerativeComponents.GCScript;
    using Bentley.GenerativeComponents.GCScript.GCTypes;
    using Bentley.GenerativeComponents.GCScript.NameScopes;
    using Bentley.GenerativeComponents.GCScript.ReflectedNativeTypeSupport;
    using Bentley.GenerativeComponents.GeneralPurpose;
    using Bentley.GenerativeComponents.UtilityNodes;
    using Bentley.GenerativeComponents.View;
    
    namespace SampleAddIn
    {
        [GCNamespace("User")]
        [NodeTypePaletteCategory("wayne")]
        // [NodeTypeIcon("Resources/wayne.png")]
        public class ListFilter: UtilityNode
        {
            // Following are the names of all of the possible inputs and outputs to this ListFilter node type. Since we will be
            // referencing these names more than once herein, it's a good idea to define them as constants.
    
            internal const string NameOfInputListProperty  = "InputList";
            internal const string NameOfFilterListProperty = "FilterList";
            internal const string NameOfResultListProperty = "ResultList";
    
            static readonly NodeGCType s_gcTypeOfAllInstances = (NodeGCType) GCTypeTools.GetGCType(UniversalGCEnvironment.TheOnlyInstance, typeof(ListFilter));
    
            static public NodeGCType GCTypeOfAllInstances
            {
                get { return s_gcTypeOfAllInstances; }
            }
    
            static void AddAdditionalMembersToGCType(IGCEnvironment environment, GCType gcType, NativeNamespaceTranslator namespaceTranslator)
            {
                UtilityNodeTechnique technique1 = gcType.AddDefaultNodeTechnique("Default", DefaultTechnique);
    
                technique1.AddParameterDefinition(environment, NameOfInputListProperty, typeof(string[]), "",
                                                 Ls.Literal("The list to be filtered"));
    
                technique1.AddParameterDefinition(environment, NameOfFilterListProperty, typeof(Boolean[]), null,
                                                  Ls.Literal("The filter list (Boolean list)"));
    
                technique1.AddParameterDefinition(environment, NameOfResultListProperty, typeof(string[]), "",
                                      Ls.Literal("The filtered list."), NodePortRole.TechniqueOutputOnly);
            }
    
            static NodeUpdateResult DefaultTechnique(UtilityNode node, NodeUpdateContext updateContext)
            {
                ListFilter listFilter = (ListFilter) node;  // It's safe to assume that the given node is of this class type
                                                            // (ListFilter).
    
                string[]     mylist         = listFilter.InputList;
                bool[]       myfilter       = listFilter.FilterList;
                List<string> myfilteredlist = new List<string>();
    
                for (int i = 0; i < mylist.Length; i++)
                {
                    if (myfilter[i])
                    {
                        myfilteredlist.Add(mylist[i]);
                    }
                }
    
                listFilter.Result = myfilteredlist.ToArray();
    
                return NodeUpdateResult.Success;
            }
    
            // Beginning of instance members.
    
            // The following two methods -- ActiveNodeState and GetInitialNodeState -- are simply "must be defined"
            // methods that all Node-based node classes must have. These can simply be copied-and-pasted
            // into your own Node-based class (changing the name of the constructor, of course).
    
            internal new NodeState State
            {
                get { return (NodeState) base.State; }
            }
    
            protected override UtilityNode.NodeState GetInitialState(NodeTechniqueDetermination initialActiveTechniqueDetermination)
            {
                return new NodeState(this, initialActiveTechniqueDetermination);
            }
    
            public string[] InputList
            {
                get { return State.InputListProperty.GetNativeValue<string[]>(); }
                set { State.InputListProperty.SetNativeValueAndInputExpression(value); }
            }
    
            public bool[] FilterList
            {
                get { return State.FilterListProperty.GetNativeValue<bool[]>(); }
                set { State.FilterListProperty.SetNativeValueAndInputExpression(value); }
            }
    
            public string[] Result
            {
                get { return State.ResultListProperty.GetNativeValue<string[]>(); }
                set { State.ResultListProperty.SetNativeValueAndInputExpression(value); }
            }
    
            // Following is the embedded NodeState class that every Node-based class must have.
            //
            // This embedded class can be copied-and-pasted from this class to your own node type class, then you can replace the
            // list of properties with the inputs and outputs that are specific to your own class.
    
            public new class NodeState: UtilityNode.NodeState
            {
                // There must be one NodeProperty field for each unique input and output of your node type, regardless of which
                // technique(s) each input and output belongs to. The order of these fields is irrelevant; we suggest listing them
                // alphabetically.
    
                internal readonly UtilityNodeProperty InputListProperty;
                internal readonly UtilityNodeProperty FilterListProperty;
                internal readonly UtilityNodeProperty ResultListProperty;
    
                internal protected NodeState(ListFilter parentNode, NodeTechniqueDetermination initialActiveTechniqueDetermination):
                                             base(parentNode, initialActiveTechniqueDetermination)
                {
                    // IMPORTANT: This constructor calls 'AddProperty' to get each property field, whereas the following
                    // constructor calls 'GetProperty'.
    
                    InputListProperty  = AddProperty(NameOfInputListProperty);
                    FilterListProperty = AddProperty(NameOfFilterListProperty);
                    ResultListProperty = AddProperty(NameOfResultListProperty);
                }
    
                protected NodeState(NodeState source): base(source)
                {
                    // IMPORTANT: This constructor calls 'GetProperty' to get each property field, whereas the preceding
                    // constructor calls 'AddProperty'.
    
                    InputListProperty  = GetProperty(NameOfInputListProperty);
                    FilterListProperty = GetProperty(NameOfFilterListProperty);
                    ResultListProperty = GetProperty(NameOfResultListProperty);
                }
    
                protected new ListFilter UtilityNode
                {
                    get { return (ListFilter) base.UtilityNode(); }
                }
    
                public override UtilityNode.NodeState Clone()
                {
                    return new NodeState(this);
                }
    
                public override bool TryGetDefaultOutputProperty(out INodeProperty property)
                {
                    property = ResultListProperty;
                    return true;
                }
            }
        }
    }