Drawings SDK Developer Guide > Working with .dwg Files > Working with Entities > Creating a Custom Entity > Implementing Save and Load Functions
Implementing Save and Load Functions

To implement a custom class derived from OdDbEntity, the following input/output virtual function must be overridden:

  • dwgInFields();
  • dwgOutFields();
  • dxfInFields();
  • dxfOutFields();

In addition to the required I/O function, the following additional functions should be implemented if needed:

  • composeForLoad();
  • decomposeForSave();

The custom object calls the dwgInFields() or dxfInFields() function when a custom entity is being loaded from a .dwg or .dxf file respectively. The dwgOutFields() or dxfOutFields() function is called when a custom entity is being saved in a .dwg or a .dxf file respectively. They are also used for other purposes, such as cloning.


virtual OdResult OdDbEntity::dwgInFields( OdDbDwgFiler* pFiler );
virtual void OdDbEntity::dwgOutFields( OdDbDwgFiler* pFiler ) const;
virtual OdResult OdDbEntity::dxfInFields( OdDbDxfFiler* filer );
virtual void OdDbEntity::dxfOutFields( OdDbDxfFiler* filer ) const;

Each I/O function takes a pointer to a filer as its primary argument. The custom entity writes data to and reads data from a filer. Reading data is performed by 'rd'-functions of the filer object. Writing data is performed by 'wr'-functions of the filer object. These functions subdivide by types of data. The following list describes some of the functions that allow you to work with various types of data:


rdBool() / wrBool() - read / write a boolean value
rdInt8() / wrInt8() - read / write a byte
rdInt16() / wrInt16() - read / write a integer value of int type
rdInt32() / wrInt32() - read / write a longer value of long type
rdDouble() / wrDouble() - read / write a real value of double type
rdString() / wrString() - read / write a string value
rdPoint3d() / wrPoint3d() - read / write a 3d point of OdGePoint3d type
rdVector3d() / wrVector3d() - read / write a 3d vector of OdGeVector3d type
rdScale3d() / wrScale3d() - read / write a 3d scale of OdGeScale3d type

Data must be loaded in the same order in which it was saved. Before loading or saving custom entity data, the corresponding method of the base class must be called to take care of base class data. For each custom entity it is recommended that you keep a version number.

Implementing the dwgInFields()/dwgOutFields() functions

Upon implementing the dwgInFields() function, it is necessary to check whether the object is open for writing before reading from the filer object. Use the assertWriteEnabled() function, which throws an exception if this object is not open for writing, and it controls automatic undo and notification of modifications.

Upon implementing the dwgOutFields() function, it is necessary to check whether the object is open for reading before writing to the filer object. Use the assertReadEnabled() function, which throws an exception if this object is not open for reading. 'In'-functions should return the eOk if the result of reading is successful.

For example, the smiley entity implements the following:


const double kCurrentVersionNumber = 1.0;

void AsdkSmiley::dwgOutFields(OdDbDwgFiler* filer) const
{
   assertReadEnabled();
   // Method of base class must be performed first
   OdDbEntity::dwgOutFields( filer );              
   filer->wrDouble( kCurrentVersionNumber );
   filer->wrPoint3d( center() );
   filer->wrDouble( radius() );
   filer->wrVector3d( normal() );
   filer->wrDouble( eyesApart() );
   filer->wrDouble( eyesHeight() );
   filer->wrDouble( eyeSize() );
   filer->wrPoint3d( mouthLeft() );
   filer->wrPoint3d( mouthBottom() );
   filer->wrPoint3d( mouthRight() );
   filer->wrInt32( backColor().color() );
}

OdResult AsdkSmiley::dwgInFields(OdDbDwgFiler* filer)
{
   assertWriteEnabled();
   // Method of base class must be performed first
   OdResult res = OdDbEntity::dwgInFields( filer );               
   if(res != eOk) return res;

   // Read version number. If this is a new unknown version, load the entity as a Proxy object.
   if( filer->rdDouble() > kCurrentVersionNumber ) return eMakeMeProxy;
   
   // Read face data
   setCenter( filer->rdPoint3d() );
   setRadius( filer->rdDouble() );
   setNormal( filer->rdVector3d() );
   
   // Read eyes data
   setEyesApart( filer->rdDouble() );
   setEyesHeight( filer->rdDouble() );
   setEyeSize( filer->rdDouble() );
   
   // Read mouth data
   OdGePoint3d mouthleftpt=filer->rdPoint3d(),
               mouthbottompt=filer->rdPoint3d(),
               mouthrightpt=filer->rdPoint3d();
   setMouth( mouthleftpt, mouthbottompt, mouthrightpt );
   
   // Read color data
   mbackcolor.setColor( filer->rdInt32() );

   return eOk;
}

dwgInFields() return codes

  • eOk — Self descriptive.
  • eMakeMeProxy — Object will be loaded as a proxy. For example, if the object's version is unknown, all data will be preserved; and if the saved file is loaded by a higher version application, the object will be restored.

If any other code is returned, a warning will be issued. The exact return code effects only the warning message. If the object is invalid, it can be erased in dwgInFields(). This is valid if erasing the particular object during readFile() does not break drawing integrity and does not require auditing of other objects.

dwgInFields() exceptions

If an exception is thrown from dwgInFields():

  • readFile() will throw an exception and refuse to load the file.
  • recoverFile() will erase the object and report it to AuditInfo.

Note: During recoverFile(), audit() is called for all objects.

For example:


OdResult res = pImpl->dwgInFields(pFiler);
switch (res)
{
  case eOk:
  case eMakeMeProxy:
    break;
  default:
    {
      OdDbAuditInfo* pAuditInfo = database()->auditInfo();
      if (pAuditInfo)
      { // File is being loaded with recoverFile()
        // Exception will be handled, object erased and the error reported to AuditInfo
        throw (OdError(res));
      }
      else
      {  // File is being loaded with readFile(), warning will be issued
         erase(); // If necessary
      }
    }
}
return res;

Implementing the dxfInFields()/dxfOutFields() functions

The dxfInFields() and dxfOutFields() functions are implemented similarly to dwgInFields() and dwgOutFields() respectively. Note that the DXF format stores data in text format which can be located inconsistently in the file. For example, a .dxf file obtained from another application may not save all of the data. A function loading data from a .dxf file may not be able to read the data as it was saved in the .dxf file. Additionally, the DXF format allows you to skip a line of data.

In the DXF format, each data burst has a group code, which identifies data (see also Working with Tagged Data). Writing functions ('wr'-functions) set a group code for each data burst and write it in the file. Reading functions ('rd'-functions) do not know what data burst they read from the file. To identify a data burst at the time of reading, you must use the nextItem() function of the OdDbDxfFiler object, which returns a group code of data. The received group code can be checked using a switch-case operator. The end of the entity's data is defined by the atEOF() function of the OdDbDxfFiler object, which returns true if this filer object is at the end of an object's data. It occurs if the filer is at any of the following:

  • a subclass data marker,
  • the start of an object's xdata,
  • the start of another object's data,
  • the end of a file.

Missing data must be initialized in the dxfInFields() function at reading or it can be set by default before reading.

To identify a custom entity in a .dxf file, it is necessary to save an entity's class name, which can be determined using the desc()->name() function. It is used in the dxfOutFields() function to write the entity's class name as a marker of an object and in the dxfInFields() function to compare the read name with an entity's class name. You can compare the names using the atSubclassData() function, which returns true if a filer object is a subclass data marker with the specified argument. It is recommended that you store a version number of the entity after its class name and before its data.

For example, the smiley entity implements the following:


void AsdkSmiley::dxfOutFields(OdDbDxfFiler *filer) const
{
   assertReadEnabled();
   OdDbEntity::dxfOutFields( filer );              // Method of base class must be performed first

   filer->wrSubclassMarker( desc()->name() );
   filer->wrDouble( OdResBuf::kDxfReal, kCurrentVersionNumber );
   filer->wrPoint3d( OdResBuf::kDxfXCoord, center() );
   filer->wrDouble( OdResBuf::kDxfReal+1, radius() );
   if( filer->includesDefaultValues() || normal() != OdGeVector3d( 0, 0, 1 ))
   {
      filer->wrVector3d( OdResBuf::kDxfNormalX, normal() );
   }
   filer->wrDouble( OdResBuf::kDxfReal+2, eyesApart() );
   filer->wrDouble( OdResBuf::kDxfReal+3, eyesHeight() );
   filer->wrDouble( OdResBuf::kDxfReal+4, eyeSize() );
   filer->wrPoint3d( OdResBuf::kDxfXCoord+1, mouthLeft() );
   filer->wrPoint3d( OdResBuf::kDxfXCoord+2, mouthBottom() );
   filer->wrPoint3d( OdResBuf::kDxfXCoord+3, mouthRight() );
}

OdResult AsdkSmiley::dxfInFields(OdDbDxfFiler *filer)
{
   assertWriteEnabled();
   OdResult es;
   OdGePoint3d center = center(),
               mouthleftpt = mouthLeft(),
               mouthbottompt = mouthBottom(),
               mouthrightpt = mouthRight();

   if( eOk != (es = OdDbEntity::dxfInFields( filer )) ) return es;
   
   // Check that we are at the correct subclass data
   if( !filer->atSubclassData( desc()->name() )) return eBadDxfSequence;
   
   // Read version number (must be first)
   if( filer->nextItem() != OdResBuf::kDxfReal ) return eMakeMeProxy;
   if( filer->rdDouble() > kCurrentVersionNumber ) return eMakeMeProxy;
   
   // Read of smiley data
   while( !filer->atEOF() )
   {
      switch( filer->nextItem() )
      {
         case OdResBuf::kDxfXCoord:
              filer->rdPoint3d( center );
              setCenter( center );
              break;

         case OdResBuf::kDxfReal+1:
              setRadius( filer->rdDouble() );
              break;

         case OdResBuf::kDxfNormalX:
              filer->rdVector3d( mnormal );
              break;

         case OdResBuf::kDxfReal+2:
              setEyesApart( filer->rdDouble() );
              break;

         case OdResBuf::kDxfReal+3:
              setEyesHeight( filer->rdDouble() );
              break;

         case OdResBuf::kDxfReal+4:
              setEyeSize( filer->rdDouble() );
              break;

         case OdResBuf::kDxfXCoord+1:
              filer->rdPoint3d( mouthleftpt );
              break;

         case OdResBuf::kDxfXCoord+2:
              filer->rdPoint3d( mouthbottompt );
              break;

         case OdResBuf::kDxfXCoord+3:
              filer->rdPoint3d( mouthrightpt );
              break;
      }
   }
   setMouth( mouthleftpt, mouthbottompt, mouthrightpt );

   return eOk;
}

Important remarks for implementing I/O functions

The dwgInFields()/dxfInFields() input function in general should not access any other database objects:

  • For dwgInFields(), accessing other database objects is not safe in multi-threaded load mode. Even in single-threaded mode it may happen that another object is not loaded yet and an attempt to open it would invoke its loading. So for related objects, it may happen that loading of the first object is not finished yet and an attempt to open another one starts its loading. If the second object being loaded attempts to access the first one, it appears that loading of the first one is not finished yet and it's not in a valid state.
  • For dxfInFields(), it may happen that the object you want to access is not loaded yet. For example, while loading an entity (derived from OdDbEntity), Linetype Table records are already loaded and you can access them, searching by name. But OdDbObject inheritors residing in the objects section are not loaded yet, so you can't access the Extension Dictionary.

Most (or better, all) actions requiring access to other objects should be performed in the special composeForLoad() function.

The dwgOutFields()/dxfOutFields() output function must not modify the database (modify or create new objects). All required actions must be performed in the special decomposeForSave() function.

Additional composeForLoad() and decomposeForSave() functions

In addition to the required I/O function, the following additional functions should be implemented if needed:

  • composeForLoad() is called for each object after dwgInFields()/dxfInFields() are called for all objects while the file is being loaded. Its usual purpose is to process round-trip data saved to minor file versions. This function is called always including loading of the most recent version. This allows adding new data fields to an object at run-time without changing its format in the file. New data can be saved as XData or in an XRecord.
  • decomposeForSave() is called for each object before dwgOutFields()/dxfOutFields() is called for any object before file saving. It can take care of converting objects to previous versions or even replace objects with another object, save round-trip data, etc. All actions performed in decomposeForSave() while file saving will be undone automatically after the file save operation is finished.

Important: composeForLoad() / decomposeForSave() are called only during file I/O operations. But dwg/dxfIn/OutFields() are called in many other cases:

  • dxfIn/OutFields() — Used by entGet/entMod/entMake functionality.
  • dwgIn/OutFields() — Used while cloning, undo/redo, collecting references, etc.

Object versioning

An object's data stored in a file may change. It's possible to detect an object's version by the Filer version, but it does not provide enough flexibility. A good practice is to store the object's version in a file. During file loading, all objects are converted to the current version. If the object's version stored in the file is greater than the current (known by the application), the object should be loaded as a proxy. The application doesn't know how to load it and converting to a proxy would guarantee that information is not lost.

Serious problems can arise if an object's version is not saved. Detecting how the object should be read involves checking the file version, maintenance release version and XData which is used to store the object's version.

It is strongly recommended to follow the rules below:

  • Object version should be saved to the file for each object instance (the same file may contain objects of the current version and objects of higher versions which were loaded as proxies and saved back.
  • There is no need to store object version at run-time inside the object. A static function returning the current object's version is sufficient. If an object of a known version is loaded, it converts to the current version. Objects of unknown versions are loaded as proxies (see AsdkSmiley::dwgInFields() implementation above).
  • dwgInFields()/dxfInFields() should have different paths to serve object versions less than or equal to the current version and return the eMakeMeProxy code if the object version in the file is greater than the current version.
  • dwgOutFields()/dxfOutFields() may save objects to different versions depending on a Filer version. In this case, decomposeForSave() should take care of saving round-trip data to not loose information.

Avoid commonly encountered mistakes in I/O functions

It's often assumed that dwgInFields() and dxfInFields() are called only for a newly created object and only are used while file loading. However, dwgInFields() may be called for Undo/Redo operations also, and dxfInFields() may be called for the entMode() operation.

Another common mistake is that arrays contained in an object are not cleared at the beginning of the functions, and later push_back() operations double the array size at each step, such as when using Undo/Redo.

One more common mistake for DXF: default for missing DXF data is different from the value initialized in the constructor. For example, the default is Standard style Id and in the constructor it is initialized as Null.

Optimizations for OdDbDwgFiler

Depending on Filer type, the following optimizations are possible:

  • kIdFiler, kIdXlateFiler, kPurgeFiler — Involved only in Id-related operations: read/write HardOwnershipId, SoftOwnershipId, HardPointerId, SoftPointerId. If there is a large amount of data that does not contain Ids, such as huge strings representing ACIS data or huge arrays representing vertex data (OdGePoint3d), you can skip the output for this kind of filer. Of course this requires symmetrical handling of the input for the same filer type.
  • kFileFiler — Is a special case and consists of more than one stream: Ids and other data such as Ints, Doubles, etc. Starting with .dwg file format 2010, string data is also stored in a separate stream.

    The following cases work exactly the same for kFileFiler:

    
    pFiler->wrOdInt32(arr.size())	    // Goes to "Data" stream
    for(int i = 0; i < arr.size(); i++)
    {
      pFiler->wrSoftPointerId(arr[i]);  // Goes to Id stream
    }
    

    and

    
    for(int i = 0; i < arr.size(); i++)
    {
      pFiler->wrSoftPointerId(arr[i]);  // Goes to Id stream
    }
    pFiler->wrOdInt32(arr.size());      // Goes to "Data" stream
    

    This is extremely useful if you want to skip saving Null or erased elements to file and avoid extra looping through the array to calculate non-Null elements.

See Also

Creating a Custom Entity

Copyright © 2002 – 2021. Open Design Alliance. All rights reserved.