Vortex Studio SDK - Adding User Controls


Introduction

A common task among developers that use the Vortex Studio SDK is the development of a user interface to control their application and thus the simulation. An example of such an interface can be seen in the Vortex Studio Player application.
Some of its main features are:

  • Load content
  • Start/stop/pause the simulation
  • Record sessions which can then be replayed at a later time
  • Shows info, warning and error messages generated by the application.

This section of the guide will show you how you can develop equivalent functionality inside your own application using the Vortex Studio SDK.

Adding an Operator Console

There is basically two ways to allow an external user control over a Vortex application. Insert our user controls inside the application or make a separate application that communicate with Vortex through an external channel.
Here we will focus ourselves with the former method, which is both simpler and gives a more direct access to Vortex through your user interface.

When you launch the Vortex Studio Player, you might have the impression that there is two distinct applications: the simulation application with its blue background and the user control application containing all widgets.
Actually those two windows are part of the same Vortex application. The user control portion of the application is simply a plugin (see Vortex Studio SDK - Customizing Vortex - About Plugins) containing extensions (see Vortex Studio SDK - Customizing Vortex - Extensions) which handles the user interface and its logic. As with any extension they must be handled by a module. Each extensions will also typically implement an interface specific to your user interface. That interface can be checked against on each extension received by your module's onExtensionAdded method.

Most of Vortex Studio's graphical applications uses the Qt GUI toolkit to develop native C++ graphical application. This is in no way a restriction on which technology you may use for your own application. Since you will be making your own module to handle your UI extensions, you can use whatever UI technology is most appropriate for the task at hand.

Going back to the example of the Vortex Studio Player, each of its pages are classes deriving from VxSim::IExtension and also VxSim::UI::IQtPage. The VxQtModule which handles extensions implementing the VxSim::UI::IQtPage is added to the application setup document (.vxc). All UI extensions are also added to the setup document. When launching the application, the module will be automatically added to the application and it will manage all extensions it accepts in its onExtensionAdded method. It is also at that moment, when the extension is added to the module that you would signal the extension to start the creation of its user control elements.

To ensure that operations done by your user interface do not negatively impact the performance of your simulation, be sure to separate both process into different threads of execution. In other words, when your extension gets added to your module in its onExtensionAdded method do not start creating widgets in the simulation thread. Instead signal your extension in another thread that it can now create its content and handle UI related events.

For example, the IQtPage interface offers the createPage virtual method that is implemented by each specific extension and will be called from the UI thread.

Here is a small code example that demonstrates in C++ what was explained above using a custom IUIPage tagging interface which our module will use to distinguish extensions it is interested in managing.

#include <VxSim/IExtension.h>
#include <VxSim/ISimulatorModule.h>
#include <VxSim/VxSmartInterface.h>

#include "IUIPage.h"

// Tagging interface for our module
class IUIPage
{
	// Pure virtual function to implement in each derived class.
	virtual void createPage() = 0;
};

class ContentBrowser : public VxSim::IExtension, public IUIPage
{
public:
	// Methods overloaded from VxSim::IExtension
    virtual ~ContentBrowser();

	ContentBrowser(VxSim::VxExtension *proxy);

	...

	// Method overloaded from IUIPage
	virtual void createPage()
	{
		// Create widgets in UI thread.
		...
	}

	...

	void setApplication(VxSim::VxApplication* application)
	{
		mApplication = application;
	}

private:
	VxSim::VxApplication* mApplication;	
};

class UIModule : public VxSim::ISimulatorModule
{
	...

	virtual void onExtensionAdded(VxSim::VxExtension* extension)
	{
		VxSim::VxSmartInterface<IUIPage> uiPage = extension;
		if (uiPage.valid())
		{
			_addManagedExtension(extension);
			uiPage->setApplication(getApplication());
			uiPage->createPage();
		}
	}

	...
};

The following sections will detail common functionalities that developers typically want to expose in their Vortex application.

Loading Content

As described in Integrating the Application, you can use the class VxSim::VxSimulationFileManager of your VxSim::VxApplication to load content on all network nodes. Sometimes this is enough but there are also other cases where the class VxSim::VxSimulationFileManagerFacade is more appropriate. The class VxSimulationFileManagerFacade has the added advantage of not requiring direct access to the VxApplication and of being thread safe. You can call the loadObject() method from your UI thread and it will dispatch an event to the VxApplication in the simulation thread to load your object. Additionally you can register instances of VxSimulationFileManagerFacade::Listener to the VxSimulationFileManagerFacade to be notified about the state of your load/unload request.

Available callbacks are:

  • onRequestedObjectLoaded / onRequestedObjectUnloaded
  • onObjectAboutToLoad / onObjectAboutToUnload
  • onObjectLoaded / onObjectUnloaded

These callbacks give you all the flexibility needed to adjust your user interface based on the state of the content.

Start, Pause, Stop

As explained in Integrating the Application, there are three application modes. You initially load content in Editing mode and when you are ready to start the simulation, you switch to Simulating mode. During the simulation you can easily pause, resume or even make it run for just one simulation update (i.e. simulating step by step).

The class VxSim::VxApplication contains many methods to perform each of these tasks. The methods which will trigger a change in the state of the application will dispatch an event which will perform the actual task on the simulation thread. They can thus safely be called from the UI thread.
Here is an excerpt from the VxApplication's class header. Some comments have been shortened, for the full documentation please refer to the SDK's doxygen documentation or the file VxApplication.h.

// Changes the application mode asynchronously. The application mode will change when possible.
// Note that the current update will stay at the same mode.
bool setApplicationMode(eApplicationMode mode);        

// Determines whether the paused flag is set on the simulation.
bool isPaused();

// Negation of isPaused() when the mode is VxSim::kModeSimulating.
bool isSimulationRunning();

// Pauses the simulation.
// It will take effect at the next application update.
void pause(bool pause = true);

// Resumes the simulation.
// It will take effect at the next application update.
void resume();

// Makes the simulation do one step and then pause.
void stepOnce();

Going back to our example ContentBrowser class, here's how it may expose some of these functionalities.

void ContentBrowser::simulate()
{
	mApplication->setApplicationMode(VxSim::kModeSimulating);
    mApplication->resume();
}
void ContentBrowser::pause()
{
    mApplication->pause();
}
void ContentBrowser::step()
{
    mApplication->setApplicationMode(VxSim::kModeSimulating);
    mApplication->stepOnce();
}
void ContentBrowser::stop()
{
    mApplication->setApplicationMode(VxSim::kModeEditing);
}

Record and Playback

When you want to capture what happens visually during a simulation for further perusal, the best tool at your disposal is the recorder. A recording is not a complete sequence of the state of all objects in the simulation at all time. A recording contains only the kinematic data of the object during the simulation. In other words, to keep the recording light, simple and performant, we save only the position of objects during the recorded interval. These kinematic data can be replayed (playback) afterwards resulting in a visual "replay" of the recorded simulation. You cannot, for example, pause a playback mid-way and start simulating from that point (re-starting simulation from a specific point in time is the role of the key frame explained in  another section of this guide). But it is the perfect tool if, for example, you want to save a student performance in a particular scenario and review it later.

Setup

Use the Vortex Editor to add the recorder module to your application setup. In order to work properly, the Recorder module must be set on the Master node only. If you are using the Vortex Player UI page, the Record Playback Page can also be added to one of your node. The Vortex Player Console is using that page, see Record and Playback Tab.

Sending commands to the Recorder Module.

To control the recorder from inside your application, Vortex Studio SDK provides a convenient interface to its internal record and playback facilities called VxSim::KinematicRecorder, which is available from the ApplicationContext. Most methods of this class are asynchronous, ensuring no slow-down of your user interface, and all dispatch events to the internal objects running inside the simulation thread. Thus the KinematicRecorder can safely be called directly from UI callbacks such as a click of a button. To perform a recording, a file is needed. The KinematicRecorder API does not provide file management, it is up to the user to delete the files if needed. 

Using the KinematicRecorder to playback a recording changes the application mode to VxSim::kModePlayingBack. This is done automatically and you do not need to manually call VxApplication::setApplicationMode(), Your modules will get the application mode change callback as usual. . See Integrating the Application for details about the application mode.

The KinematicRecorder API

  • open() is both used for previously recorded files you want to play() and to create new empty files to which you will record(). The Recorder module always works with one file. Opening a file will stop any playback or recording and close the current file before opening a new file.  When recording, the file will always be appended. The result of recording multiple simulation into the same file, of playing back a recording on a different scene, is undefined. 
  • close() closes the previously opened file. It will perform a stop if needed.
  • The record() method is used to record the current simulation.
  • The play() method is used to play the recording. It starts playing at the current frame index and play 1 frame per update. This sets the application mode to kModePlayingBack and keeps a key frame of the simulation. The key frame will be restored when leaving playback.
  • The pause() method is used to suspend the current recording or playback. While recording, it actually stops and wait for a new instruction. In playback, the application mode remains kModePlayingBack.
  • The setPlaySpeedMultiplier() method is used to to play a recording at a faster pace than it was recorded. It can be called during a recording, it will only affect the next playback.
  • The stop() method stops the current recording or playback. If the application is in playback mode, the mode before entring playback is restored and the saved key frame is restored to put back the simulation to where it was when the playback was started.
  • The setCurrentFrameByIndex() is used to set the current playback frame by using the recorded frame index. Like  play(), it changes the application mode to kModePlayingBack.
  • The setCurrentFrameByTimestamp() is used to set the current playback frame by using the timestamp. If the given timestamp is not valid, it uses the nearest recorded timestamp.
  • The getStatus() is used to get the recorder status. Since the KinematicRecorder behavior is asynchronous, the status of the recorder is not guaranteed to be updated immediately after an operation returns. The status contains the following information
    1. filename - The recorded resource filename.
    2. recorderMode - Indicates if the recording is playing, paused, recording or idle. If there is no module, the mode will be not ready.
    3. openingMode - Indicates if the file is open for reading or writing. It always append when writing. A file that is readonly cannot be written into and a record will fail. 
    4. lastErrorCode - The last error code, see enum KinematicRecorder::eErrorCode for details.
    5. playMultiplier - playback multiplier as set with setPlaySpeedMultiplier().
    6. first/current/lastFrameIndex - Indexes of the frames  of the recording.
    7. first/current/lastFrameTimestamp- Timestamps of the frames of the recording.
    8. first/current/lastFrameSessionTime- VxApplication's Simulation times of the frames of the recording.

Stopping a playback or closing a file in playback which will set the application mode back to the value it used to be upon entering the VxSim::kModePlayingBack mode and restore the simulation as it was before entering playback.

When you are done using the KinematicRecorder's functionalities, you must call the close() method.


See tutorial MechanismViewer for an example of a Keyboard extension using this API.

Logging

Logging information for users is a task that many applications must perform. Vortex Studio isn't different and there is a wide range of informative messages that are logged by Vortex. These messages can easily be re-routed to your application and you can also use Vortex's logging system to add your own messages. All those functionalities are exposed in C++ through the functions in the VxMessage.h header file.

You can log messages to Vortex using a set of increasing levels based on the severity of the message you wish to convey. The levels are represented in the following enum.

    /// The log level defines which messages will be taken into consideration by the logging system.
    enum eLogLevel 
    { 
        /// Turn the logging Off.
        kOff,

		/// Fatal Error. The application cannot further proceed.
        kFatal,
		
		/// Error throws an exception giving a chance to the application to continue.
        kError,

		/// Warn message to the end user : reports an issue that requires an user action.
        /// Expect these to be immediately visible on a status console. 
        kWarn,

		/// Information message to the end user : describes application level operations.
        /// These might be visible on a console, so be conservative and keep to a minimum. 
        kInfo,

		/// Debug message to report potential issues.
        /// Expect these to be written to logs only. 
        kDebug,

		/// Debug message to report finer-grained informational events than kDebug
        /// Expect these to be written to logs only. 
        kTrace,

		/// Turn all messages logging On.
        kAll
    };

By default there is no way to recover from a fatal error. It will halt your application. An error throws an exception which allows for a softer termination or even a recovery depending on the circumstances. If you implement your error message handler (see below) be sure to follow the guidelines listed in the eLogLevel enum as this what Vortex expects.

You can use the function LogSetLevel() to set the minimum level of messages you are interested in receiving. For example, if you call LogSetLevel(kWarn), you will only receive warnings, errors and fatal errors; info, debug and trace messages will be silently dropped.

To register your own message handler instead of the default one, use the function LogSetHandler() which takes a callback function of type void (*LogHandler)(eLogLevel level, const char *const format, va_list ap) as its sole argument. Your handler will be called for each new message from that point on with the message's log level, its format string and a variadic list of arguments for the string. The LogSetHandler()function returns the previous handler in case you would want to put it back at some point.

To log your own messages, simply use the appropriate function for the log level of the message (LogFatal()LogError()LogWarning(), etc...). All of those functions use a format string with a variadic argument list, exactly like the C printf() function.

If you want to log to a file, use the setLogFile() function with the path to the file as a string. To disable file logging, simply pass and empty string to the function.