Customizing HSF Objects

In addition to writing and reading a standard HOOPS Stream File, the HOOPS/Stream Toolkit provides support for storing and retrieving user-defined data in the HSF file. This data could be associated with HSF objects, or it could simply be custom data which is convenient to store inside an HSF. The toolkit also supports tagging of objects in the HSF file, which allows for association of HSF objects with user data. The #TKE_Start_User_Data opcode is used to represent user data.

This section reviews the process of creating customized versions of default HSF objects. This is achieved by replacing the BStreamFileTookit class’ default handler for a particular opcode with a custom opcode handler which is derived from the default handler class. The custom opcode handler would provide support for writing and reading additional user-data.

  • During file writing, you must first access the graphical and user data that you wish to export and initialize the opcode’s data structures; we refer to this as ‘interpretation’. This work could be done in the opcode’s constructor, or you could perform the work in the BBaseOpcodeHandler::Interpret method of each opcode handler and call that method prior to exporting the opcode to the file. After ‘interpretation’ is complete, you must call the BBaseOpcodeHandler::Write method of the opcode handler until writing of the current opcode is complete. This exports the opcode data to an accumulation buffer initially passed to the toolkit. This buffer could then be exported to an HSF file or utilized directly. (This process was previously reviewed here).

  • During file reading (which is initiated by calling the TK_Read_Stream_File function, reviewed in the file reading section), the ParseBuffer method of the BStreamFileToolkit object automatically reads the opcode at the start of each piece of binary information and continually calls the BBaseOpcodeHandler::Read method of the associated opcode handler. After the opcode handler reports that reading is complete, BStreamFileToolkit::ParseBuffer calls the BBaseOpcodeHandler::Execute method of the opcode handler. The data which has been read and parsed would typically be mapped to your custom application/graphical data structures within the BBaseOpcodeHandler::Execute method of each opcode handler. However, this work could also be performed in the BBaseOpcodeHandler::Read method; doing it in the BBaseOpcodeHandler::Read method is optional.

  • Your custom opcode handler should implement the virtual method called BBaseOpcodeHandler::Clone. This method needs to make a new instance of the opcode handler.

For example, let’s say we wanted to write out an extra piece of user-data at the end of each piece of ‘shell’ geometry, (and of course retrieve it during reading) that represents a temperature value for each of the vertices in the shell’s points array. Given that the shell primitive is denoted by the #TKE_Shell opcode, and handled by the TK_Shell opcode-handler, this would involve the following steps:

1. Define a new class derived from ``TK_Shell`` that overloads the ``Write`` and ``Read`` methods to process the export and import of extra user data.

As previously mentioned, query/retrieval of the user data from custom data structures during the writing process would typically occur within the Interpret method of the opcode handler. Similarly, mapping of the imported user data to custom application data structures would typically occur in the Execute method. However, this work can be performed in the Write and Read methods as well, as the example indicates.

The following sample header expands upon the sample My_TK_Shell object reviewed in the section Writing an HSF, by also overloading the Read and Write methods:

#include "BOpcodeShell.h"

class My_TK_Shell : public TK_Shell
{
        protected:
                int                             my_stage;   // denotes the current processing stage

        public:
                My_TK_Shell()   { my_stage  = 0; }

                TK_Status               Execute (BStreamFileToolkit & tk) alter;
                TK_Status               Interpret (BStreamFileToolkit & tk, HC_KEY key, int lod=-1) alter;
                TK_Status               Read (BStreamFileToolkit & tk) alter;
                TK_Status               Write (BStreamFileToolkit  & tk) alter;

                TK_Status               Clone (BStreamFileToolkit & tk, BBaseOpcodeHandler **) const;
                void                    Reset () alter;
};

2. Implement the custom Write function.

This is done in stages, each of which correspond to the discrete pieces of data that need to be written out for the custom shell. We use different versions of the BBaseOpcodeHandler::PutData method to output the user data, and we return from the writing function during each stage if the attempt to output the data failed (This could happen due to an error or because the user-supplied buffer is full). At this point, review the process of Formatting User Data.

The following lists in detail the 5 writing stages for our custom shell opcode-handler:

Stage 0: Output the default TK_Shell object by calling the base class’ Write function TK_Shell::Write

Stage 1-4: These stages write out the custom data (the temperature array) as well as formatting information required to denote a block of user data.

  1. Output the #TKE_Start_User_Data opcode to identify the beginning of the user data

  2. Output the # of bytes of user data.

  3. Output the user data itself.

  4. Output the #TKE_Start_User_Data opcode to identify the end of the user data

Example:

TK_Status My_TK_Shell::Write (BStreamFileToolkit & tk)
{
        TK_Status       status;

        switch (m_stage)
        {
                // call the base class' Write function to output the default
                // TK_Shell object
                case 0:
                {
                        if ((status = TK_Shell::Write(tk)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;

                // output the TKE_Start_User_Data    opcode
                case 1:
                {
                        if ((status = PutData (tk, (unsigned char)TKE_Start_User_Data)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;

                // output the amount of user data in bytes; we're writing out one
                // float for each vertex value, so we have 4*m_num_values
                case 2:
                {
                        if ((status = PutData (tk, 4*m_num_values)) != TK_Normal)
                                return status;

                        m_progress  = 0;
                        my_stage++;

                } nobreak;

                // output our custom  data, which in this example is an array of
                // temperature values which are stored  in an application
                // data structure called 'temperature_values'
                // since the temperature values array  might always be larger
                // than the buffer, we can't just "try again" so always generate
                // piecemeal, with m_progress the number of values done so far
                case 3:
                {
                        if ((status = PutData (tk, temperature_values, m_num_values)) != TK_Normal)
                                my_stage++;

                } nobreak;

                // output the TKE_Stop_User_Data opcode which denotes the end of user data
                case 4:
                {
                        if ((status = PutData (tk, (unsigned char)TKE_Stop_User_Data)) != TK_Normal)
                                return status;

                           my_stage = -1;

                } break;

                default:
                        return TK_Error;
        }

        return TK_Normal;
}

3. Implement the custom Read function

This is also done in stages, each of which correspond to the discrete pieces of data that need to be read in for the custom shell. We use different versions of the BBaseOpcodeHandler::GetData method to retreive data, and we return from the reading function during each stage if the attempt to retreive the data failed. Otherwise, the stage counter is incremented and we move on to the next stage.

The stages during the reading process are analogous to the stages during the writing process outline above, with one exception. The #TKE_Start_User_Data opcode would still be read during ‘Stage 1’, but rather than blindly attempting to read our custom data, we need to handle the case where there isn’t any user data attached to this shell object. Perhaps the file isn’t a custom file, or it was a custom file and this particular shell object simply didn’t have any user data appended to it.

It is also appropriate at this time to bring up the issue of versioning and user data; it is also possible that there is user data following this shell object, but it is not ‘our’ user data. Meaning, it is not temperature data that was written out by our custom shell object, and therefore it is data that we don’t understand; as a result, we could attempt to read to much or too little data. If custom versioning information was written at the beginning of our custom file, and this versioning information was used to verify that this was a file written out by our custom logic, then it is generally safe to proceed with processing user data since we ‘know’ what it is. The versioning issue, including details on how to write custom versioning information in the file, is discussed in more detail in the next section, Versioning and storing additional user data.

Note that to check if there is any user data, we first call BBaseOpcodeHandler::LookatData to simply look at (but not get) the next byte and verify that it is indeed a #TKE_Start_User_Data opcode. If not, we return.

Example:

TK_Status My_TK_Shell::Read (BStreamFileToolkit & tk)
{
        TK_Status       status;

        switch (my_stage)
        {
                case 0:
                {
                        if ((status = TK_Shell::Read (tk)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;
                case 1:
                {
                        unsigned    char temp;

                        // look at the next byte since it may not be the TKE_Start_User_Data opcode
                        if ((status = LookatData(tk, temp)) != TK_Normal)
                                return status;

                        if (temp != TKE_Start_User_Data)
                                return TK_Normal;   // there isn't any user data, so return!

                        // get the opcode from the buffer
                        if ((status = GetData (tk, temp)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;
                case 2:
                {
                        int     length;

                        // get the integer denoting the amount of user data
                        if ((status = GetData (tk, length)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;
                case 3:
                {
                        // get the  temperature value array; this assumes we've
                        // already determined the length of the array and identified
                        // it using m_num_values
                        if ((status = GetData (tk, temperature_values, m_num_values)) != TK_Normal)
                                return status;

                        my_stage++;

                } nobreak;
                case 4:
                {
                        unsigned        char temp;

                        // get the TKE_Stop_User_Data opcode which denotes the end of user data
                        if ((status = GetData (tk, temp)) != TK_Normal)
                                return status;

                        if (temp != TKE_Stop_User_Data)
                                return TK_Error;

                        my_stage = -1;

                } break;
                default:
                        return TK_Error;
        }

        return TK_Normal;

}

4. Implement the custom BBaseOpcodeHandler::Reset Function The toolkit will call the opcode handler’s Reset function after it has finished processing the opcode. This method should reinitialize any opcode handler variables, free up temporary data and then call the base class implementation.

Example:

void My_TK_Shell::Reset()
{
        my_stage = 0;
        TK_Shell::Reset();
}

5. Implement the custom BBaseOpcodeHandler::Clone function

Example:

TK_Status My_TK_Shell::Clone (BStreamFileToolkit & tk, BBaseOpcodeHandler **newhandler) const
{
        *newhandler = new My_TK_Shell();

        if (*newhandler != null)
                return TK_Normal;
        else
                return tk.Error();
}

6. Instruct the toolkit to use our custom shell opcode handler in place of the default handler by calling BStreamFileToolkit::SetOpcodeHandler.

We specify the type of opcode that we want to replace, and pass in a pointer to the new opcode handler object:

tk->SetOpcodeHandler (TKE_Shell, new My_TK_Shell);

This will also cause the toolkit to delete it’s default handler object for the TKE_Shell opcode. Note: As the HOOPS/Stream Reference Manual points out, all opcode handler objects stored in the BStreamFileToolkit object will be deleted when the BStreamFileTookit object is deleted. Therefore, we would not delete the My_TK_Shell object created in the above example.