What happens to Custom Objects on Save?

Hello i have another question regarding Custom Objects.

When a 3dm file containing custom objects gets saved, all of them get converted in some way (CustomBrepObject becomes BrepObject, CustomCurveObject becomes CurveObject etc.) Can someone explain how this process is happening and what can be changed about it?

For example in my current case i have UserData attached to my CustomObjects and while this userdata survives saving and reopening on “normal” objects, the custom object gets stripped of the userdata. I am following this example which works up to the point where custom objects come into play.

Maybe there is something to override or an event to subscribe to, so i can attach my userdata to those automatic replacement objects and get it saved in the doc.

Thanks for any help

Hi @lando.schumpich,

Custom objects are saved as their inherited types. For example, a CustomCurveObject is saved as a CurveObject. Any extra data on your derived object should be saved as user data.

When Rhino opens a document, you should watch for objects with your user data and convert them back to your custom object.

– Dale


I finally got it to work, the problem was i was adding my UserData to the customObject itself when i should have been doing something like:


Subscribing to RhinoDoc.EndOpenDocument now gives me the ability to re-create all my custom objects after saving and reopening.

Thanks a lot!

Hi @lando.schumpich,

Would it be possible for you to provide one quick example demonstrating how to re-create the custom objects after saving and reopening?
Thanks in advance! :slight_smile:

Hi @jeffoulet,

For re-creation i typically do this inside of the plugIn class:

  1. Subscribe to the EndOpenDocument event
        protected override LoadReturnCode OnLoad(ref string errorMessage)
            // Add an event handler so we know when documents are opened
            RhinoDoc.EndOpenDocument += RhinoDoc_EndOpenDocument;

            return LoadReturnCode.Success;
  1. Iterate over all objects in the doc and search for your user data, re-create if found.
        private void RhinoDoc_EndOpenDocument(object sender, DocumentOpenEventArgs e)
            RhinoDoc doc = e.Document;

            foreach (var docObject in doc.Objects)
                // try to find our user data on a given doc object
                var data = docObject.Attributes.UserData.Find(typeof(MyCustomUserData)) as MyCustomUserData;
                if (data is null) continue;

                // Update data to restore auto implemented properties

                // create a shiny new custom Object from the stored data
                var customObject = data.CreateCustomObject();

                // replace original object in doc
                doc.Objects.Replace(new ObjRef(docObject), customObject);


In this example I defined a method inside of MyCustomUserData to create a custom object.

You can also take a look at this Repository Where i implemented Read Write for the RectangleData class as an example


Hi Lando,

Many thanks for your help!
By using the sample you provided here: Custom object for storing internal meta-information

And with the help of your Octopus example, I eventually get the CustomObjects to be re-created on document load time. :slight_smile:
I’m not sure to have fully/deeply understood the events/handlers stories yet, but at least, it works. I’ve now to wrap my mind around all this!

Hi @lando.schumpich,

How to elegantly re-create the custom objects at document opening when you have different kind of CustomObjects with different types of associated CustomUserData?

In your example, we loop through all objects from document, and look if some custom user data is attached to the object. If yes, we recreate the object based on the user data attached to it.

Let say we have these guys in our document:

  • ‘MyCustomBrep1’ associated with ‘MyCustomBrepUserData1’
  • ‘MyCustomBrep2’ associated with ‘MyCustomBrepUserData2’
  • ‘MyCustomCurve’ associated with ‘MyCustomCurveUserData’

Do we also need to loop 3x times through the document objects, each time looking for a special type of user data, first time doing:

docObject.Attributes.UserData.Find(typeof(MyCustomBrepUserData1)) as MyCustomBrepUserData1

a 2nd time:

docObject.Attributes.UserData.Find(typeof(MyCustomBrepUserData2)) as MyCustomBrepUserData2

and so on…

Or is there a way to ‘filter’ and cast the user data type relatively to its type?

Thanks in advance!

One immediate optimization that comes to mind, is doing all your filtering inside of one loop:

// try get all different user data types
foreach obj in doc.Objects {
    var myCbrep1 = obj.Attributes.UserData.Find(typeof(MyCustomBrepUserData1)) as MyCustomBrepUserData1;
    var myCbrep2 = obj.Attributes.UserData.Find(typeof(MyCustomBrepUserData2)) as MyCustomBrepUserData2;
    var myCcurve = obj.Attributes.UserData.Find(typeof(MyCustomCurveUserData)) as MyCustomCurveUserData;

    // at max only one of your variables won't be null
    if(!(myCbrep1 is null)){ // do stuff}
    if(!(myCbrep2 is null)){ // do stuff}
    if(!(myCcurve is null)){ // do stuff}

This is just pseudo-code, I’m not sure right now if the as - casting will throw if no userdata is found

I have a better solution which takes advantage of the fact, that all UserData derives from DataBase ins the Octopus project, but it is not implemented as of yet and sadly I don’t really know when I will find the time to do that. I can keep you updated :slight_smile:


Sorry to jump in here, but my problem is related to this topic.
I have no problem replacing the scene objects with my custom objects after document has been loaded.
(I can see great examples for this in this topic.)

But what if my custom object is part of a block object. And it is used in the scene only as part of the block object (or block objects).
In this case my object is not in the doc.Objects list. I can find it from the InstanceDefinition (GetObjects) but because it is not in doc.Objects, doc.Objects.Replace doesn’t work.
What is the correct solution for this problem?

maybe this function could help:
bool ReplaceInstanceObject(ObjRef objref, int instanceDefinitionIndex);

But I don’t understand the exact parameters.


Again necrobumping… Dear @dale would you mind to point me what i can do possibly wrong as in my case seems protected override bool Write is never called. My UserData derived class have public override bool ShouldWrite => true; and it is attached to an object:

It’s my custom object which derives from CustomPointObject in constructor I’m adding UserData like

public PivotPoint(Point3d at) : base(new Rhino.Geometry.Point(at))
            var pivotPointUserData = new PivotPointUserData();
            pivotPointUserData.Plane = new Plane(at, Vector3d.ZAxis);

While editing document I can easily get this UserData from this object via UserData.Find. But on Read nothing happens so I investigated and set a breakpoint inside Write override but it never hits it. What is missing? I’ve run out of ideas to be honest.

Hi @D-W,

the first thing that comes to mind, is custom UserData needs to have an annotated GUID, see the definition of my RectangleData class which implements read/write:

    public class RectangleData : DataBase
        public double Width { get; set; } = 1; // X
        public double Height { get; set; } = 1; // Y
    // code ommitted ...

(In this example DataBase inherits from UserData, you can look at the full code: https://github.com/DerLando/Octopus/blob/master/Octopus/Core/Objects/RectangleObject.cs)

I found that without the GUID, Read and Write never get called.

Hope to help,

1 Like

@lando.schumpich Bingo! Thanks for that now works as expected!

I’m curious is there any guide explaining which classes should be decorated with Guid ?

In the example it is also annotated:

The info given is this:

  // You must define a Guid attribute for your user data derived class
  // in order to support serialization. Every custom user data class
  // needs a custom Guid

But I also forgot to annotate many times, it is quite annoying that no error is thrown in that case, would save some time debugging everytime it happens

1 Like