Simple Data Types (C#) |
This article discusses how Grasshopper deals with data items and types. It's a rather complicated topic as data is an integral part of the Grasshopper process and GUI. Grasshopper needs to be able to (de)serialize data, display data in tooltips, convert data to other types of data, prompt the user for persistent data, draw geometry preview data in viewports and bake geometric data. In this topic I'll only talk about non-geometric data, we'll get to previews and baking in a later topic.
Practically all native data types in Grasshopper are based either on a .NET Framework type or a RhinoCommon SDK type. For example System.Boolean, System.String, Rhino.Geometry.Point3d and Rhino.Geometry.Brep to name but a few. However the parameters in Grasshopper don't directly store Booleans, String, Points and Breps as these types can't handle themselves in the cauldron that is Grasshopper.
All data used in Grasshopper must implement the IGH_Goo interface. IGH_Goo defines the bare minimum of methods and properties for any kind of data before it is allowed to play ball.
In this section I'll briefly discuss all the methods and properties that are defined in IGH_Goo. What they're for, who uses them at what time, etc, etc.
Method | Purpose |
---|---|
C# bool IsValid { get{} } | Not all data types are valid all the time, and this property allows you to teint the current instance of your data. When data is invalid it will often be ignored by components. |
C# string TypeName { get{} } | This property must return a human-readable name for your data type. |
C# string TypeDescription { get{} } | This property must return a human-readable description of your data type. |
C# IGH_Goo Duplicate() | This function must return an exact duplicate of the data item. Data is typically shared amongst multiple Grasshopper parameters, so before data is changed, it first needs to copy itself. When data only contains ValueTypes and Primitives, making a copy of an instance is usually quite easy. |
C# string ToString() | This function is called whenever Grasshopper needs a human-readable version of your data. It is this function that populates the data panel in parameter tooltips. You don't need to create a String that is parsable, it only needs to be somewhat informative. |
C# IGH_GooProxy EmitProxy() | Data proxies are used in the Data Collection Manager. You can ignore this function (i.e. Return Nothing) without crippling your data type. |
C# bool CastTo<T>(out T target) | Data Casting is a core feature of Grasshopper data. It basically allows data types defined in Grasshopper add-ons to become an integral part of Grasshopper. Lets assume that we have a Component that operates on data type [A]. But instead of playing nice, we provide data of type [B]. Two conversion (casting) attempts will be made in order to change [B] into [A]. If [B] implements IGH_Goo, then it is asked if it knows how to convert itself into an instance of [A]. Failing that, if [A] implements IGH_Goo, it is asked whether or not it knows how to construct itself from an instance of [B]. The CastTo() function is responsible for step 1. The CastTo() method is a generic method, meaning the types on which it operates are not defined until the method is actually called. This allows the function to operate 'intelligently' on data types. It also unfortunately means you have to be 'intelligent' when implementing this function. |
C# bool CastFrom(object source) | The CastFrom() function is responsible for step 2 of data casting. Some kind of data is provided as a source object and if the local Type knows how to 'read' the source data it can perform the conversion. |
C# object ScriptVariable() | When data is fed into a VB or C# script component, it is usually stripped of IGH_Goo specific data and methods. The ScriptVariable() method allows a data type to provide a stripped down version of itself for use in a Script component. |
Although all data in Grasshopper must implement the IGH_Goo interface, it is not necessary to actually write a type from scratch. It is good practice to inherit from the abstract class GH_Goo<T>, as it takes care of some of the basic functionality. GH_Goo is a generic type (that's what the "<T>" bit means), where T is the actual type you're wrapping. GH_Goo<T> has several abstract methods and properties which must be implemented, but a lot of the other methods are already implemented with basic (though usually useless) functionality.
We'll now create a very simple custom type. This will introduce the basic concept of custom type development, without dealing with any of the baking and previewing logic yet. Our custom type will be a TriState flag, similar to boolean values but with an extra state called "Unknown". We'll represent these different states using integers:
Integer | TriState |
---|---|
Negative One | Unknown |
Zero | False |
Positive One | True |
We'll start with the general class layout, then drill down into each individual function. Create a new public class called TriStateType and inherit from GH_Goo<Integer>. Be sure to import the Grasshopper Kernel and Kernel.Types namespaces as we'll need them both:
using Grasshopper.Kernel; using Grasshopper.Kernel.Types; namespace MyTypes { public class TriStateType : GH_Goo<int> { } }
Unless a constructor is defined, .NET classes always have a default constructor which initializes all the fields of the class to their default values. This constructor does not require any inputs and when you develop custom types it is a good idea to always provide a default constructor. If there is no default constructor, then class instances cannot be created automatically which thwarts certain algorithms in Grasshopper.
In addition to a default constructor I also find it useful to supply so called copy constructors which create a new instance of the type class with a preset value.
// Default Constructor, sets the state to Unknown. public TriStateType() { this.Value = -1; } // Constructor with initial value public TriStateType(int tristateValue) { this.Value = tristateValue; } // Copy Constructor public TriStateType(TriStateType tristateSource) { this.Value = tristateSource.Value; } // Duplication method (technically not a constructor) public override IGH_Goo Duplicate() { return new TriStateType(this); }
Incidentally, the Value property which we are using to assign integers to our local instance is provided by the GH_Goo<T> base class. GH_Goo<T> defines a protected field of type T called m_value and also a public accessor property called Value which gets or sets the m_value field.
In this particular case, it actually makes sense to override the default Value property implementation, as the number of sensible values we can assign (-1, 0 and +1) is a subset of the total number values available through the Integer data type. It makes no sense to assign -62 for example. We could of course agree that all negative values indicate an "Unknown" state, but we should try to restrict ourselves to only three integer values:
// Override the Value property to strip non-sensical states. public override int Value { get { return base.Value; } set { if (value < -1) { value = -1; } if (value > +1) { value = +1; } base.Value = value; } }
Formatting data is primarily a User Interface task. Both the data type and the data state need to be presented in human-readable form every now and again. This mostly involves readonly properties as looking at data does not change its state:
// TriState instances are always valid public override bool IsValid { get { return true; } } // Return a string with the name of this Type. public override string TypeName { get { return "TriState"; } } // Return a string describing what this Type is about. public override string TypeDescription { get { return "A TriState Value (True, False or Unknown)"; } } // Return a string representation of the state (value) of this instance. public override string ToString() { if (this.Value == 0) { return "False"; } if (this.Value > 0) { return "True"; } return "Unknown"; }
Some data types can be stored as persistent data. Persistent data must be able to serialize and deserialize itself from a Grasshopper file. Most simple types support this feature (Booleans, Integers, Strings, Colours, Circles, Planes etc.), most complex geometry types cannot be stored as persistent data (Curves, Breps, Meshes). If possible, you should aim to provide robust (de)serialization for your data:
// Serialize this instance to a Grasshopper writer object. public override bool Write(GH_IO.Serialization.GH_IWriter writer) { writer.SetInt32("tri", this.Value); return true; } // Deserialize this instance from a Grasshopper reader object. public override bool Read(GH_IO.Serialization.GH_IReader reader) { this.Value = reader.GetInt32("tri"); return true; }
There are three casting methods on IGH_Goo; the CastFrom() and CastTo() methods that facilitate conversions between different types of data and the ScriptVariable() method which creates a safe instance of this data to be used inside untrusted code (such as VB or C# Script components).
// Return the Integer we use to represent the TriState flag. public override object ScriptVariable() { return this.Value; } // This function is called when Grasshopper needs to convert this // instance of TriStateType into some other type Q. public override bool CastTo<Q>(ref Q target) { //First, see if Q is similar to the Integer primitive. if (typeof(Q).IsAssignableFrom(typeof(int))) { object ptr = this.Value; target = (Q)ptr; return true; } //Then, see if Q is similar to the GH_Integer type. if (typeof(Q).IsAssignableFrom(typeof(GH_Integer))) { object ptr = new GH_Integer(this.Value); target = (Q)ptr; return true; } //We could choose to also handle casts to Boolean, GH_Boolean, //Double and GH_Number, but this is left as an exercise for the reader. return false; } // This function is called when Grasshopper needs to convert other data // into TriStateType. public override bool CastFrom(object source) { //Abort immediately on bogus data. if (source == null) { return false; } //Use the Grasshopper Integer converter. By specifying GH_Conversion.Both //we will get both exact and fuzzy results. You should always try to use the //methods available through GH_Convert as they are extensive and consistent. int val; if (GH_Convert.ToInt32(source, out val, GH_Conversion.Both)) { this.Value = val; return true; } //If the integer conversion failed, we can still try to parse Strings. //If possible, you should ensure that your data type can 'deserialize' itself //from the output of the ToString() method. string str = null; if (GH_Convert.ToString(source, out str, GH_Conversion.Both)) { switch (str.ToUpperInvariant()) { case "FALSE": case "F": case "NO": case "N": this.Value = 0; return true; case "TRUE": case "T": case "YES": case "Y": this.Value = +1; return true; case "UNKNOWN": case "UNSET": case "MAYBE": case "DUNNO": case "?": this.Value = -1; return true; } } //We've exhausted the possible conversions, it seems that source //cannot be converted into a TriStateType after all. return false; }