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's 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 in Section 2.1).
- During file reading (which is initiated by calling the ::TK_Read_Stream_File function, reviewed in Section 2.2), 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.
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.
- Output the #TKE_Start_User_Data opcode to identify the beginning of the user data
- Output the # of bytes of user data.
- Output the user data itself.
- Output the #TKE_Start_User_Data opcode to identify the end of the user data
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.
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.
5. Implement the custom BBaseOpcodeHandler::Clone function
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.
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.