Trying to set UserText to a grasshopper model object (GH_ModelObject)

Hello,
I have this CSharp Script that I want to code to convert a list of curves in a list of points, in order to later generate gCode. I want to add UserText text to each point, but I am struggling with the (lack of?) documentation.

In a first script, I derived the type of object that flows out of the new QueryModelObjects component of Grasshopper WIP : ‘Grasshopper.Rhinoceros.Model.GH_ModelObject’, using the .GetType() method. And I was able to read the UserText of my curves that I had manually set in rhino.

//#! csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;

using Rhino;
using Rhino.Geometry;
using Rhino.DocObjects;

using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using Grasshopper.Rhinoceros.Model;


public class Script_Instance : GH_ScriptInstance
{
  /* 
    Members:
      RhinoDoc RhinoDocument
      GH_Document GrasshopperDocument
      IGH_Component Component
      int Iteration

    Methods (Virtual & overridable):
      Print(string text)
      Print(string format, params object[] args)
      Reflect(object obj)
      Reflect(object obj, string method_name)
  */
  
  private void RunScript(RhinoCodePluginGH.Legacy.ProxyDocument ghdoc, RhinoCodePlatform.GH.ScriptEnv ghenv, System.Collections.Generic.IEnumerable<object> curves, Rhino.Geometry.Point3d entryPoint, object x)
{
    // Create a new list to store the GH_ModelObjects
    List<GH_ModelObject> list = new List<GH_ModelObject>();

        foreach (object o in curves)
        {
            Console.WriteLine("o.GetType()" + o.GetType());
            // if the object is a GH_ModelObject
            if (o is GH_ModelObject modelObject)
            {
                if (modelObject.UserText.ContainsKey("Case"))
                {
                    Console.WriteLine("Case : " + modelObject.UserText["Case"]);
                }
            }
        }
}
}

Now I want to set UserText to grasshopper Objects in a CSharp Script and I am failing (comments in the foreach loop) :

    // Grasshopper Script Instance
//#! csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;

using Rhino;
using Rhino.Geometry;
using Rhino.DocObjects;

using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using Grasshopper.Rhinoceros.Model;


public class Script_Instance : GH_ScriptInstance
{
  /* 
    Members:
      RhinoDoc RhinoDocument
      GH_Document GrasshopperDocument
      IGH_Component Component
      int Iteration

    Methods (Virtual & overridable):
      Print(string text)
      Print(string format, params object[] args)
      Reflect(object obj)
      Reflect(object obj, string method_name)
  */
  
  private void RunScript(RhinoCodePluginGH.Legacy.ProxyDocument ghdoc, RhinoCodePlatform.GH.ScriptEnv ghenv, System.Collections.Generic.IEnumerable<Rhino.Geometry.Curve> curves, Rhino.Geometry.Point3d entryPoint, object x, out object a)
{
    // Create a new list to store the GH_ModelObjects
    List<GH_ModelObject> list = new List<GH_ModelObject>();

    // Iterate over the curves
    foreach (Curve curve in curves)
    {
        // Divide the curve into 10 points with kink option set to true
        double[] parameters = curve.DivideByCount(10, true);

        // Create a new list to store the points
        List<Point3d> points = new List<Point3d>();

        // Add the divided points to the list
        foreach (double parameter in parameters)
        {
            GH_ModelObject modelObject = new GH_ModelObject();
            Point3d point = curve.PointAt(parameter);


            modelObject.Geometry = point; // modelObject.Geometry is read only how do I set it?
            modelObject.UserText.append("gCode", "G1"); // also failing

           // Add the modelObject to the list
            list.Add(modelObject);

        }
    }

    // Assign the list to the 'a' out parameter
    a = list;
}
}

What am I missing? Where can I find the Grasshopper.Rhinoceros.Model.GH_ModelObject documentation/code?
How can I set the geometry of a GH_ModelObject ? I did not find it in Grasshopper API - Redirect or rhinocommon.

Thank you,

Olivier

Anyone? Any advice? @AndyPayne?

Thank you !

Sorry for the delay. You can try something like this. Note, I’m using the newer script editor in Rhino 8 so that I could declare a top-level static class for an extension method. Also note, that while this will work today, we are working on modifying the class names, etc. of some of our classes to make them a little less verbose and conform to our other SDK standards. This work will hopefully be finished in the coming weeks, but this might alter some of the class names and break this specific script in the future… but once we settle on the final names, the SDK will be set and we won’t intentionally change any of the class names if we can help it.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.Linq;

using Rhino;
using Rhino.Geometry;

using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using Grasshopper.Rhinoceros;
using Grasshopper.Rhinoceros.Model;

public static class EnumerableExtensions
{
    public static IEnumerable<TResult> ZipOrLast<TFirst, TSecond, TResult>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector)
    {
      if (first is null) throw new ArgumentNullException(nameof(first));
      if (second is null) throw new ArgumentNullException(nameof(second));
      if (resultSelector is null) throw new ArgumentNullException(nameof(resultSelector));

      using (var e1 = first.GetEnumerator())
      using (var e2 = second.GetEnumerator())
      {
        var next1 = true;
        var next2 = true;
        var last1 = default(TFirst);
        var last2 = default(TSecond);
        while ((next1 && (next1 = e1.MoveNext())) | (next2 && (next2 = e2.MoveNext())))
        {
          yield return resultSelector(next1 ? (last1 = e1.Current) : last1, next2 ? (last2 = e2.Current) : last2);
        }
      }
    }
}

public class Script_Instance : GH_ScriptInstance
{
  private void RunScript(RhinoCodePluginGH.Legacy.ProxyDocument ghdoc, RhinoCodePlatform.GH.ScriptEnv ghenv, object pt, System.Collections.Generic.IEnumerable<string> utKeys, System.Collections.Generic.IEnumerable<string> utValues, out object a)
  {
    var keys= utKeys.ToArray();
    var values = utValues.ToArray();
    var modelObject = new GH_ModelObject.Attributes();
    {
        modelObject.Geometry = GH_Convert.ToGeometricGoo(pt);
        modelObject.UserText = new SortedList<string, string>();
    }

    foreach (var (key, value) in keys.ZipOrLast(values, (k, v) => (k, v)))
    {
        if (!string.IsNullOrWhiteSpace(key))
        {
            if (value is null)
                modelObject.UserText.Remove(key);
            else
                modelObject.UserText[key] = value;
        }
    }

    a = modelObject.ToGoo() as GH_ModelObject;
  }
}

AssignUserText.gh (12.0 KB)

Thank you very much, it works !
The thing is that I was declaring my modelObject as a GH_ModelObject

GH_ModelObject modelObject = new GH_ModelObject.Attributes();
    {
        modelObject.Geometry = GH_Convert.ToGeometricGoo(p);
        modelObject.UserText = new SortedList<string, string>();
    }

leads to :

Property or indexer 'GH_ModelObject.Geometry' cannot be assigned to -- it is read only

When you use ‘var’ instead of the actual type, it works !?

    var modelObject = new GH_ModelObject.Attributes();

I don’t think it’s the use of var in the declaration. Instead, I think you were declaring the type like this:

GH_ModelObject modelObject = new GH_ModelObject();

when it should have been this:

GH_ModelObject.Attributes modelObject = new GH_ModelObject.Attributes();

Of Course, Thank you !

Hello, I upgraded WIP to the last version today and my definition scripts are not working anymore here is one : ```

private void RunScript(System.Collections.Generic.IEnumerable<object> objectsList, string layerName, int maxGeoCount, int minGeoCount, out object a, out object info, out object error)
{
    //filterByLayer
    //METHOD that selects the document objects that are in a given layer, it also outputs errors

    //PARAMS
    //objectsList : an object list (ModelObject)
    //layerName : the layer Name that is used to select objects
    //maxGeoCount : the maximum number of selected objects, an error is outputed if the object count is over it
    //minGeoCount : the minimum number of selected objects, an error is outputed if the object count is under it

    //Console.WriteLine("objectsList.GetType()" + objectsList.GetType());

    // Create a new list
    List<GH_ModelObject.Attributes> list = new List<GH_ModelObject.Attributes>();
    List<String> infoMessages = new List<String>(); 
    List<String> errorMessages = new List<String>(); 

    try {
        if (objectsList != null) {
            if (objectsList.Count()>0) {
                //iterate over the objects
                foreach (GH_ModelObject.Attributes modelObject in objectsList.OfType<GH_ModelObject.Attributes>()) {
                    // we get the Case Id for error purposes
                    if (modelObject.Layer.FullName == layerName) {
                        list.Add(modelObject);
                        Console.WriteLine("Added modelObject " + modelObject);
                        Console.WriteLine("Added modelObject.Layer " + modelObject.Layer);
                    }
                }
                if (list.Count < minGeoCount) {
                    errorMessages.Add("filterByLayer : The number of objects found in layer " + layerName + " is too small, it should be over " + minGeoCount);
                }

                if (list.Count > maxGeoCount) {
                    errorMessages.Add("filterByLayer : The number of objects found in layer " + layerName + " is too big, it should be under " + maxGeoCount);
                }
            } else {
                Console.WriteLine("filterByLayer : objectsList empty list !!");
                errorMessages.Add("filterByLayer :  objectsList empty list !!");
            }
        } else {
            Console.WriteLine("filterByLayer :  objectsList is null !!");
            errorMessages.Add("filterByLayer : objectsList is null !!");
        }
    } catch (Exception e) {
        Console.WriteLine("filterByLayer :" + e.Message);
        errorMessages.Add("filterByLayer :" +e.Message);
    }
    // Assign the tree to the 'a' out parameter
    a = list;
    info = infoMessages;
    error = errorMessages;
}

}```

In the console I have this :

Added modelObject
Added modelObject.Layer Model Layer : Dessus_Initiale
Hidden = True
Locked = False
Display Colour = 0,98,255
Material = Nouveau matériau 005
Linetype = Continuous
Print Colour = 0,98,255
Print Width = 0
Added modelObject

And as a result of the script even though ModelObjects have been added to the result list, it is empty. Notice in the console modelObject prints nothing but modelObject.Layer is full of info

Before the update of WIP my code was working, I was using

foreach (GH_ModelObject modelObject in objectsList.OfType<GH_ModelObject>()) {
   if (modelObject.Layer.FullName == layerName) {
       list.Add(modelObject);
       Console.WriteLine("Added modelObject " + modelObject);
       Console.WriteLine("Added modelObject.Layer " + modelObject.Layer);
   }
}

and the output was

Added modelObject Model Brep : {20583af4-dc71-4b77-8ab6-97365f51dd16}
User Text:
Case = C1G
serialNumber = ERF20236_30168485
Added modelObject.Layer Model Layer : Dessus_Initiale
Hidden = True
Locked = False
Display Colour = 0,98,255
Material = Nouveau matériau 005
Linetype = Continuous
Print Colour = 0,98,255
Print Width = 0
Added modelObject Model Brep : {9600d115-c8b6-425d-99bd-3ce49f554769}

but in the script previous to this script was not working with GH_ModelObject but worked with GH_ModelObject.Attributes this i why I changed the type in this script also

If the goal is to filter objects based on different attributes… then there may now be some easier ways to do this. Try checking out the new Grouping, Sorting, and Filtering tools under the Content subpanel. I’m going to be writing up a more detailed explanation about how all of these components work, but in the meantime you might give those a poke.

1 Like

Awesome Andy, I was just about to create a group by user text script so this is very timely!

What defines a rule? I tried feeding in a text panel and searching for Rule but no results. Are rules booleans? Expressions? Something different?

Thanks for the insight and new components!

Hello Andy,
Thank you for your quick answer, and those new components !
Yes the goal is to filter geometry by layer, and also to log errors or infos about that process
e.g. : There are too many geo in a given layer.

Can you give us an example of how to filter with a layer name for example, I tried to set up a rule but did not succeed.

Thank you again !

Olivier

Hello Michael,

for what its worth, since it does not work in the latest version of WIP, I did write one that groups geometry in a tree reading a user text key:‘Case’, Value :‘A1’
`

private void RunScript(RhinoCodePluginGH.Legacy.ProxyDocument ghdoc, RhinoCodePlatform.GH.ScriptEnv ghenv, System.Collections.Generic.IEnumerable<object> objectsList, System.Collections.Generic.IEnumerable<string> groups, int minGeometries, out object a, out object slots, out object info, out object error)
{
    //groupGeoInTree
    //METHOD that groups document objects in branches in a dataTree

    //PARAMS
    //objects : all the geos of the document (ModelObject)
    //groups : list of groupNames, each model object of the document has a UserText with the Key "Case"
    // groups is the list of the possible values of "Case" 
    //it allows the grouping of the objects in branches and gives the order of the branches in the result tree


    // Create a new tree
    DataTree<GH_ModelObject> geoTree = new DataTree<GH_ModelObject>();
    DataTree<String> slotsTree = new DataTree<String>();
    List<String> infoMessages = new List<String>(); 
    List<String> errorMessages = new List<String>(); 


    // Iterate through the list of group names
    int i = 0;
    try {
            if (objectsList != null) {
                if (objectsList.Count()>0) {
                foreach (string groupName in groups)
                {
                    // Create a new branch for the current group
                    GH_Path path = new GH_Path(i);

                    // Count the number of objects belonging to the current group
                    int objectCount = objectsList.OfType<GH_ModelObject>()
                        .Count(modelObject => modelObject.UserText.ContainsKey("Case") &&
                                            (modelObject.UserText["Case"] == groupName ||
                                            modelObject.UserText["Case"] == groupName.Substring(0, groupName.Length - 1)));

                        Console.WriteLine("objectCount" + objectCount);                       

                    // Check if the object count meets the minimum geometries threshold
                    if (objectCount > minGeometries)
                    {
                        // Add objects to the tree for the current group
                        foreach (GH_ModelObject modelObject in objectsList.OfType<GH_ModelObject>())
                        {
                            if (modelObject.UserText.ContainsKey("Case") &&
                                (modelObject.UserText["Case"] == groupName ||
                                modelObject.UserText["Case"] == groupName.Substring(0, groupName.Length - 1)))
                            {
                                Console.WriteLine("in");
                                geoTree.Add(modelObject, path);
                                slotsTree.Add(groupName, path);
                                // // Check if the string already exists at the path in slotsTree
                                // bool stringExists = slotsTree.Branches[i].Contains(groupName);
                                // Console.WriteLine("slotsTree.Branches[i]" + slotsTree.Branches[i]);
                                // if (!stringExists)
                                // {

                                // }
                            }
                        }
                    } else {
                        Console.WriteLine("groupGeoInTree : objectCount < minGeometries one Case ignored : "+groupName);
                        infoMessages.Add("groupGeoInTree : objectCount < minGeometries one Case ignored : "+groupName);
                    }
                    i++;
                    }
                } else {
                Console.WriteLine("groupGeoInTree : objectsList empty list !!");
                errorMessages.Add("groupGeoInTree :  objectsList empty list !!");
                }
            } else {
                Console.WriteLine("groupGeoInTree :  objectsList is null !!");
                errorMessages.Add("groupGeoInTree : objectsList is null !!");
            }
    } catch (Exception e) {
        Console.WriteLine("groupGeoInTree : "+e.Message);
        errorMessages.Add("groupGeoInTree : "+e.Message);
    }
        // Assign the tree to the 'a' out parameter
        a = geoTree;
        slots = slotsTree;
        info = infoMessages;
        error = errorMessages;
}

`

1 Like

If the goal is to simply filter objects out by layer, then the easiest way to probably do that would be to use the Query Model Objects component and use the Layer input to specify the name of the layer that you want to filter for. Then, only the objects on that layer will be returned. If you right-click on that component and choose Show All Parameters, it will then also split the data out by type into different outputs. But, that’s not really the question here. So, let me explain how some of the other components work.

Let’s start with the simplest components (Group By Attribute, and Order By Attribute). In this example, I have the following model with lots of different types of objects and these are placed on different Layers (Layers 1-4).

We can use the Query Model Objects component to reference all of the Rhino geometry into Grasshopper. Then, we connect the Objects output to the Group By Attribute component. We can right-click on the Key input and look at the Tree view at all of the different attributes we can use as grouping criteria in the component. If we want to break objects out by Layer, we simply select the Layer root node in the tree view and the output will be broken out into different branches where each branch corresponds to a different Layer.

You’ll notice that the output isn’t sorted… but the branching does correspond to the list of values returned in the Values output. This output gives you all the unique values that it found for that grouping criteria. So, if you chose Layer, then you’ll get a list of unique Layers.

If you want to the branches to be sorted, then we simply need to add an Order By Attribute component between the Query Model Objects and the Group By Attribute components. Again, we’ll use the Layer as the sorting criteria and it will then return a list that is ordered according to the Layer name.

So, with these two components, you can begin to break down your data in really interesting ways. You can expand the Layer root node and select Display Color for example. Now, it will use the Layer color as the criteria to use for grouping on different branches. If you had several layers which had the color “Red” for example (255,0,0), then all of those objects would be placed on the same branch.

Or, let’s say you wanted all of the objects broken down by Material name. Simply expand the Material node and select the Name attribute, and now it will group objects together that have the same material name. Simple. Want to group objects based on their type (ie. Point, Curve, Brep, etc.)? Simply choose the Type Name as the search key and you’re done.

Now, let’s consider the concept of Filtering objects by a rule or criteria. I’ve now got a different model open. This one contains a bunch of boxes (on different layers), each of which have a number of different User Text entries associated with it. These keys include: Color, Cost, ID, Manufacturer, and Volume (which is derived from a formula). Let’s use some of these User Text values as filtering criteria.

The first thing we need to do is add a Filter By Rule component to our canvas and connect the Query Model Objects output to the Content input. Next, we need to tell the Filter By Rule component that we want to filter by a User Text entry instead of a data type attribute. To do this, right-click on the Filter By Rule component and choose By User Text Key in the menu. Note: you can do this same procedure in the Group By Attribute and Order By Attribute components if you want to use a User Key as the search criteria.

Now, right-click on the Key input and select one of the User Text keys to use as our filtering criteria. Let’s say we only want to return the “Orange” objects. So, we select the User Text key named Color as our filtering criteria.

Next we need to define a rule that describes how we want to filter the objects. In this case, we want a rule that says "Return any object which has a User Text value which is equal to “Orange”. So, we’re going to use the Equality rule component to achieve this. Notice that the Filter By Rule component only returns objects that match that rule.

Now, let’s say we wanted to filter the objects so that we only return the objects who have a volume larger than some value. We need to change the Key input so that it’s using the Volume numeric value as the filtering key (instead of color). And then we can define a new rule using the Larger Than Rule component. We can use a slider to dynamically control the filtering. In the image below, I’m selecting objects whose volume is greater than 630. The same works with the Smaller Than Rule component.

What if we wanted to define a compound rule where we only wanted objects whose volume was greater than 150 but also smaller than 400. We can use the Conditional And Operator to combine rules together. So, we first define the Larger Than and Smaller Than rules and combine them together so that our search criteria will only return the values we want.

Hopefully this helps explain the basics of how these components work… but there’s a lot you can do with these that I didn’t necessarily cover here. Feel free to ask any follow up questions if you have any.

3 Likes

This is HUGE, thank you @AndyPayne for the nodes and the detailed breakdown. Very helpful.

Here I’m filtering for objects with a flooring material and only returning results larger than 2000 sq ft and then selecting those in the Rhino document.

Am I correct in ordering the Filter By Rule node after the Group By Attribute node?

Or can I somehow combine the Keys as grafted list or an AND type method?

I’m thinking I can’t do this because the Filter By Rule node I am using is referencing Type Attributes and the Group By Attribute node is using User Text so I could graft the key values together and the rules but the Filter By Rule node will only allow User Text Key OR Type Attribute at the same time, is that correct?

1 Like

Yes. Right now it’s only processing a single key at a time. I’ll have to give it a think if there’s some way to combine keys into a single search criteria… Given your stated goal (filtering objects with a floor material and also floors more than 2000sf)… Then I think you could use a Group By component to filter by material, and then a Filter By using a Larger Than rule for filtering objects more than 2000sf. I think that should work.
Alternatively, I think you could swap the Group By component for a second Filter By component and use the Material Name in an Equality Rule to achieve the same thing… but it’s just a different way to achieve it and it’s basically still 2 separate components. I guess the only benefit would be that you wouldn’t need to do all the Data Tree Branch extraction stuff after the Group By component if you use two separate Filter By components.

1 Like

That makes sense, it would potentially be nice to be able to graft the inputs or leverage lists, similar to how User Text component works where you can provide an array of keys and matching values but for now I don’t mind stringing together a node or a few at at time to provide multiple filters. It at least keeps it very clear what you are filtering at each location along the line.

1 Like

To accompany my post in this thread, I’ve also published a little longer tutorial on Grouping, Sorting, and Filtering. Please have a look here: Grouping, Sorting, and Filtering

Thank you !

1 Like

Hello @AndyPayne, I upgraded WIP to the last version today (8.0.23255) and I cannot read the userText anymore

this used to display the userText I set in rhino :

        try {
            foreach (GH_ModelObject.Attributes modelObject in objectsList.OfType<GH_ModelObject.Attributes>())
            {
                if (modelObject.UserText.ContainsKey("Case"))
                {
                    Console.WriteLine("modelObject.UserText[Case] : " + modelObject.UserText["Case"]);
                }
            }
        }
         catch (Exception e) {
            Console.WriteLine("0groupGeoInTree : "+e.Message);
            errorMessages.Add("0groupGeoInTree : "+e.Message);
        }

the error I get :

  1. Error running script S
    Exception: Compile Error
    ‘ModelUserText?’ does not contain a definition for ‘ContainsKey’ and no accessible extension method ‘ContainsKey’ accepting a first argument of type ‘ModelUserText?’ could be found (are you missing a using directive or an assembly reference?) (Error CS1061) rhinocode:///grasshopper/1/2830dfd5-6d18-4040-b67f-28f85db43bc7/83e91521-8d2a-4ef8-9a76-675286e1a7d4:[95:42]
    Cannot apply indexing with to an expression of type ‘ModelUserText?’ (Error CS0021) rhinocode:///grasshopper/1/2830dfd5-6d18-4040-b67f-28f85db43bc7/83e91521-8d2a-4ef8-9a76-675286e1a7d4:[97:73]

Can you help me?
Thank you very much

Olivier

In the meantime, have you tried using auto complete to type a . or open parenthesis after GH_ModelObject.UserText to see what possible methods show up, and then sequentially drill down until you find the method names for user text?

Just a thought