There are two basic ways plug-ins can store information in Rhino .3dm files, document user data and object user data.
For example, a rendering plug-in might save a scene descriptions as document user data and use object user data to attach rendering material information to individual surfaces.
To save document user data your plug-in must override three CRhinoPlugIn base class functions:
CRhinoPlugIn::CallWriteDocument CRhinoPlugIn::WriteDocument CRhinoPlugIn::ReadDocument
When Rhino writes a .3dm file, it goes through all the plug-ins that are currently loaded. First Rhino calls CallWriteDocument() to see if the plug-in wants to save document user data. If CallWriteDocument() returns true, Rhino saves information that identifies the plug-in and then calls WriteDocument() when it is time for the plug-in to save its "document" user data.
When Rhino reads a .3dm file and it encounters document user data, it uses the plug-in identification information to load the plug-in and then calls the plug-in's ReadDocument() to read the plug-in's "document" user data.
A Sample of this is provided at SdkDocumentData
Object user data can be attached to things like layers, materials, geometry objects, and object attributes. In fact object user data can be attached to any class derived from ON_Object. This user data is stored in a linked list on ON_Object and can be copied, transformed and saved along with the parent object. For example, you could attach object user data to a mesh. When the mesh is copied the object user data would be copied and attached to the copy. When the mesh is transformed, the transformation would be recored by the object user data. When the mesh is saved in a .3dm file, the object user data will be saved too.
All object user data is saved in a class you write that is derived from the class ON_UserData. The first example, CMyUserData1, shows how to make a simple piece of user data that is not save in .3dm files. The second example, CMyUserData2, shows how to make a piece of user data that is saved in .3dm files.
The source code for the next two the examples can be downloaded here.
class CMyUserData1 : public ON_UserData
{
public:
/*
Returns:
Uuid used to identify this type of user data.
This is the value saved in m_userdata_uuid and
passed to ON_Object::GetUserData().
*/
static ON_UUID Id();
CMyUserData1();
~CMyUserData1();
... download example for more details
ON_wString m_my_string;
};
The CMyUserData1 class must always be on the heap ( constructed by calling the C++ new operator ). The class's constuctor must initialize two ON_UserData fields, m_userdata_uuid, m_application_uuid. In addition, if you want the object user data to be copied whenever its parent object is copied, you have to set m_userdata_copycount to one.
ON_UUID CMyUserData1::Id()
{
// Use Microsoft's guidgen to get a unique id.
// {1129B130-B840-...}
static const ON_UUID id = { 0x1129b130, 0xb840, ... };
return id;
}
CMyUserData1::CMyUserData1()
{
m_userdata_uuid = CMyUserData1::Id();
m_application_uuid = MyPlugIn().PlugInID();
m_userdata_copycount = 1; // enable copying
}
... download example for more details
Attaching object user data to a parent object is a simple matter.
ON_Mesh* pMesh = ...;
CMyUserData1* pMyUserData1 = new CMyUserData1();
pMyUserData1->m_my_string = L"How, now, brown cow?";
if ( pMesh->AttachUserData(pMyUserData1) )
{
// It worked. The mesh class will manage the user
// data and delete it at the appropriate time.
RhinoApp().Print("User data attached.\n"):
}
else
{
// It didn't work. This usually means that
// the parent object already has this kind
// of user data attached.
RhinoApp().Print("User data not attached.\n");
delete pMyUserData;
}
pMyUserData = 0;
The user data's uuid is used to retrieve the user data from a parent object.
ON_Mesh* pMesh = ...;
ON_UserData1* pUserData = pMesh->GetUserData(CMyUserData1::Id());
CMyUserData1* pMyUserData1 = static_cast<CMyUserData1*>(pUserData);
if ( pMyUserData1 )
{
RhinoApp().Print("Got user data.\n"):
}
else
{
RhinoApp().Print("My user data is not on the mesh.\n"):
}
Make sure you completely understand the CMyUserData1 example before studying this example.
In order to have your user data saved in files, you have to add several IO related functions and add full opennurbs object support to your class.
class CMyUserData2 : public ON_UserData
{
// Opennurbs classes that are saved in .3dm files require
// an ON_OBJECT_DECLARE call in their declaration.
ON_OBJECT_DECLARE(CMyUserData2);
public:
static ON_UUID Id();
CMyUserData2();
~CMyUserData2();
// override virtual ON_UserData::Archive()
BOOL Archive() const;
// override virtual ON_UserData::Write()
BOOL Write(
ON_BinaryArchive& binary_archive
) const;
// override virtual ON_UserData::Read()
BOOL Read(
ON_BinaryArchive& binary_archive
);
... download example for more details
ON_wString m_my_string;
ON_3dPoint m_my_point;
};
Again, the CMyUserData2 class must always be on the heap ( constructed by calling the C++ new operator ). In order to support file IO, the definition of CMyUserData2 must contain the ON_OBJECT_DECLARE macro.
The cpp file where the class is implemented must contain the ON_OBJECT_IMPLEMENT macro shown below.
ON_OBJECT_IMPLEMENT(CMyUserData2,ON_UserData,"0D8FA7AB-F8A4-...");
The construction code for CMyUserData2 looks like.
ON_UUID CMyUserData2::Id()
{
return CMyUserData2::m_CMyUserData2_class_id.Uuid();
}
CMyUserData2::CMyUserData2()
{
m_userdata_uuid = CMyUserData2::Id();
m_application_uuid = MyPlugIn().PlugInID();
m_userdata_copycount = 1; // enable copying
// initialize your data members here
m_my_point.Set(0.0,0.0,0.0);
}
The CMyUserData2::Archive(), CMyUserData2::Read(), CMyUserData2::Write() functions look something like this. Download the example for more details.
BOOL CMyUserData2::Archive() const
{
// If false is returned, nothing will be saved in 3dm archives.
return true;
}
BOOL CMyUserData2::Write(
ON_BinaryArchive& binary_archive
) const
{
... download example for more details about adding version support
int minor_version = 0;
bool rc = binary_archive.BeginWrite3dmChunk(
TCODE_ANONYMOUS_CHUNK,
1,
minor_version
);
if (!rc)
return false;
// Write class members like this
for (;;)
{
// version 1.0 fields
rc = binary_archive.WriteString(m_my_string);
if (!rc) break;
rc = binary_archive.WritePoint(m_my_point);
if (!rc) break;
break;
}
if ( !binary_archive.EndWrite3dmChunk() )
rc = false;
return rc;
}
BOOL CMyUserData2::Read(
ON_BinaryArchive& binary_archive
)
{
... download example for more details about adding version support
int major_version = 0;
int minor_version = 0;
bool rc = binary_archive.BeginRead3dmChunk(
TCODE_ANONYMOUS_CHUNK,
&major_version,
&minor_version
);
if (!rc)
return false;
// Read class members like this
for (;;)
{
rc == ( 1 == major_version );
if (!rc) break;
// version 1.0 fields
rc = binary_archive.ReadString(m_my_string);
if (!rc) break;
rc = binary_archive.ReadPoint(m_my_point);
if (!rc) break;
break;
}
// If BeginRead3dmChunk() returns true,
// then EndRead3dmChunk() must be called,
// even if a read operation failed.
if ( !binary_archive.EndRead3dmChunk() )
rc = false;
return rc;
}
The Rhino geometry table is where the bulk of the Rhino model is stored. This is where the points, curves, meshes, surface, polysurfaces, annotation, render lights, and so on are stored. Each member of the geometry table is derived from the CRhinoObject class. There are three places you can attach object user data to a CRhinoObject: the CRhinoObject geometry class, the CRhinoObject attributes class, and the CRhinoObject itself. Object user data attached to the geometry and attributes class persists through copys, transformations, and file IO. Object user data attached directly on the CRhinoObject class is never saved in .3dm file.
In general, you do not need to explictly override the default copy constructor and operator= that C++ generates for you. Incorrectly implemented copy constructors and operator=s are a common source of bugs. The C++ defaults will work perfectly if every member of your class is a built-in data type (int, double, ...) or a class that properly handles copy construction and operator= (ON_NurbsCurve, ON_3dPoint, ON_Simple/ClassArray<>, ...).
The main reason you have to explicitly implement a copy constructor and operator= is to handle fields that involve explicit heap allocation and deallocation. In the example below, the field m_i_array is an array of integers that is on the heap.
class CMyUserData3 : public ON_UserData
{
ON_OBJECT_DECLARE(CMyUserData3);
public:
static ON_UUID Id();
static ON_UUID ApplicationId();
static CMyUserData3* Get(const ON_Object*);
CMyUserData3();
~CMyUserData3();
CMyUserData3(const CMyUserData3& src);
CMyUserData3& operator=(const CMyUserData3& src);
...
int m_i_count;
int* m_i_array;
};
ON_OBJECT_IMPLEMENT(CMyUserData3,
ON_UserData,
<<guidgen generated uuid here>>
);
ON_UUID CMyUserData3::Id()
{
return CMyUserData3::m_CMyUserData3_class_id.Uuid();
}
ON_UUID CMyUserData3::ApplicationId()
{
return <<your plug-in name>>().PlugInID();
}
CMyUserData3* CMyUserData3::Get(const ON_Object* object)
{
return CMyUserData3::Cast( (object != 0)
? object->GetUserData( CMyUserData3::Id() )
: 0
);
}
CMyUserData3::CMyUserData3()
{
m_userdata_uuid = CMyUserData3::Id();
m_application_uuid = CMyUserData3::ApplicationId();
m_userdata_copycount = 1;
// Initialize your class's fields
m_i_count = 0;
m_i_array = 0;
}
CMyUserData3::~CMyUserData3()
{
// virtual function
if ( 0 != m_i_array )
onfree(m_i_array);
}
When the copy constructor is called, the memory for "this" is not initialized. You must call the base class copy constructor, initialize all your class's fields, and then copy your class's fields.
CMyUserData3::CMyUserData3(const CMyUserData3& src )
: ON_UserData(src) // critical - must call base class copy constructor
{
m_userdata_uuid = CMyUserData3::Id();
m_application_uuid = CMyUserData3::ApplicationId();
m_transform_count = 0;
// DO NOT SET OTHER ON_UserData fields
// In particular, do not set m_userdata_copycount
// Copy you class's fields
m_i_count = 0;
m_i_array = 0;
if( src.m_i_count > 0 && src.m_i_array != 0 )
{
m_i_array = (int*)onmalloc(src.m_i_count*sizeof(m_i_array[0]));
if ( m_i_array != 0 )
{
memcpy( m_i_array,
src.m_i_array,
src.m_i_count*sizeof(m_i_array[0])
);
m_i_count = src.m_i_count;
}
}
}
When operator= is called, "this" has already been constructed and may already have been used. You must destroy any existing information, call the the base class operator=, and then copy your class's fields.
CMyUserData3& CMyUserData3::operator=(const CMyUserData3& src)
{
if ( this != &src )
{
// Destroy your class's existing information
// (Otherwise you will leak memory if "this"
// has been used before.)
m_i_count = 0;
if ( m_i_array != 0 )
{
onfree(m_i_array);
m_i_array = 0
}
// Use the base class operator=() to correctly
// copy all ON_UserData fields.
ON_UserData::operator=(src);
// Copy your class's fields
if( src.m_i_count > 0 && src.m_i_array != 0 )
{
m_i_array = (int*)onmalloc(src.m_i_count*sizeof(m_i_array[0]));
if ( m_i_array != 0 )
{
memcpy( m_i_array,
src.m_i_array,
src.m_i_count*sizeof(m_i_array[0])
);
m_i_count = src.m_i_count;
}
}
}
return *this;
}