From 438d6e3dddb856de2d110c0c92abba419db558df Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Mon, 21 Oct 2024 15:06:40 +0200 Subject: [PATCH] SessionRecording and KeyframeRecording redesign (#3399) --- include/openspace/engine/globals.h | 8 +- ...recording.h => keyframerecordinghandler.h} | 48 +- .../openspace/interaction/sessionrecording.h | 885 +---- .../interaction/sessionrecording.inl | 72 - .../interaction/sessionrecordinghandler.h | 288 ++ .../tasks/convertrecfileversiontask.h | 55 - .../interaction/tasks/convertrecformattask.h | 16 +- .../openspace/navigation/keyframenavigator.h | 1 + include/openspace/scene/scene.h | 2 +- include/openspace/scripting/scriptscheduler.h | 15 - modules/globebrowsing/src/renderableglobe.cpp | 6 +- modules/imgui/imguimodule.cpp | 4 +- .../include/topics/sessionrecordingtopic.h | 6 +- .../src/topics/sessionrecordingtopic.cpp | 21 +- modules/video/src/videoplayer.cpp | 6 +- modules/volume/linearlrucache.inl | 2 +- src/CMakeLists.txt | 13 +- src/documentation/core_registration.cpp | 8 +- src/engine/globals.cpp | 34 +- src/engine/openspaceengine.cpp | 20 +- src/interaction/keyframerecording.cpp | 430 --- src/interaction/keyframerecording_lua.inl | 190 - src/interaction/keyframerecordinghandler.cpp | 166 + .../keyframerecordinghandler_lua.inl | 108 + src/interaction/sessionrecording.cpp | 3233 ++++------------- src/interaction/sessionrecording_lua.inl | 212 -- src/interaction/sessionrecordinghandler.cpp | 786 ++++ .../sessionrecordinghandler_lua.inl | 121 + .../tasks/convertrecfileversiontask.cpp | 117 - .../tasks/convertrecformattask.cpp | 303 +- src/navigation/keyframenavigator.cpp | 2 +- src/scene/scene.cpp | 8 +- src/scene/scene_lua.inl | 14 +- src/scripting/scriptengine.cpp | 9 +- src/scripting/scriptscheduler.cpp | 19 - src/scripting/scriptscheduler_lua.inl | 24 - src/util/time.cpp | 2 +- src/util/time_lua.inl | 4 +- src/util/timemanager.cpp | 6 +- tests/CMakeLists.txt | 1 + .../0100_ascii_linux.osrectxt | 7 + .../0100_ascii_windows.osrectxt | 7 + .../sessionrecording/0100_binary_linux.osrec | Bin 0 -> 599 bytes .../0100_binary_windows.osrec | Bin 0 -> 600 bytes .../0200_ascii_linux.osrectxt | 7 + .../0200_ascii_windows.osrectxt | 7 + .../sessionrecording/0200_binary_linux.osrec | Bin 0 -> 531 bytes .../0200_binary_windows.osrec | Bin 0 -> 456 bytes tests/test_sessionrecording.cpp | 854 +++++ 49 files changed, 3176 insertions(+), 4971 deletions(-) rename include/openspace/interaction/{keyframerecording.h => keyframerecordinghandler.h} (70%) delete mode 100644 include/openspace/interaction/sessionrecording.inl create mode 100644 include/openspace/interaction/sessionrecordinghandler.h delete mode 100644 include/openspace/interaction/tasks/convertrecfileversiontask.h delete mode 100644 src/interaction/keyframerecording.cpp delete mode 100644 src/interaction/keyframerecording_lua.inl create mode 100644 src/interaction/keyframerecordinghandler.cpp create mode 100644 src/interaction/keyframerecordinghandler_lua.inl delete mode 100644 src/interaction/sessionrecording_lua.inl create mode 100644 src/interaction/sessionrecordinghandler.cpp create mode 100644 src/interaction/sessionrecordinghandler_lua.inl delete mode 100644 src/interaction/tasks/convertrecfileversiontask.cpp create mode 100644 tests/sessionrecording/0100_ascii_linux.osrectxt create mode 100644 tests/sessionrecording/0100_ascii_windows.osrectxt create mode 100644 tests/sessionrecording/0100_binary_linux.osrec create mode 100644 tests/sessionrecording/0100_binary_windows.osrec create mode 100644 tests/sessionrecording/0200_ascii_linux.osrectxt create mode 100644 tests/sessionrecording/0200_ascii_windows.osrectxt create mode 100644 tests/sessionrecording/0200_binary_linux.osrec create mode 100644 tests/sessionrecording/0200_binary_windows.osrec create mode 100644 tests/test_sessionrecording.cpp diff --git a/include/openspace/engine/globals.h b/include/openspace/engine/globals.h index df8fa1f578..2e7b671373 100644 --- a/include/openspace/engine/globals.h +++ b/include/openspace/engine/globals.h @@ -57,9 +57,9 @@ namespace interaction { class ActionManager; class InteractionMonitor; class KeybindingManager; - class KeyframeRecording; + class KeyframeRecordingHandler; class NavigationHandler; - class SessionRecording; + class SessionRecordingHandler; } // namespace interaction namespace properties { class PropertyOwner; } namespace scripting { @@ -94,9 +94,9 @@ inline interaction::InteractionMonitor* interactionMonitor; inline interaction::JoystickInputStates* joystickInputStates; inline interaction::WebsocketInputStates* websocketInputStates; inline interaction::KeybindingManager* keybindingManager; -inline interaction::KeyframeRecording* keyframeRecording; +inline interaction::KeyframeRecordingHandler* keyframeRecording; inline interaction::NavigationHandler* navigationHandler; -inline interaction::SessionRecording* sessionRecording; +inline interaction::SessionRecordingHandler* sessionRecordingHandler; inline properties::PropertyOwner* rootPropertyOwner; inline properties::PropertyOwner* screenSpaceRootPropertyOwner; inline properties::PropertyOwner* userPropertyOwner; diff --git a/include/openspace/interaction/keyframerecording.h b/include/openspace/interaction/keyframerecordinghandler.h similarity index 70% rename from include/openspace/interaction/keyframerecording.h rename to include/openspace/interaction/keyframerecordinghandler.h index 48adef6542..c8e814cc3a 100644 --- a/include/openspace/interaction/keyframerecording.h +++ b/include/openspace/interaction/keyframerecordinghandler.h @@ -22,63 +22,41 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ -#ifndef __OPENSPACE_CORE___KEYFRAMERECORDING___H__ -#define __OPENSPACE_CORE___KEYFRAMERECORDING___H__ +#ifndef __OPENSPACE_CORE___KEYFRAMERECORDINGHANDLER___H__ +#define __OPENSPACE_CORE___KEYFRAMERECORDINGHANDLER___H__ -#include #include + +#include #include -#include +#include #include #include namespace openspace::interaction { -class KeyframeRecording : public properties::PropertyOwner { +class KeyframeRecordingHandler : public properties::PropertyOwner { public: - struct Keyframe { - struct TimeStamp { - double application; - double sequenceTime; - double simulation; - }; - - KeyframeNavigator::CameraPose camera; - TimeStamp timestamp; - }; - - KeyframeRecording(); + KeyframeRecordingHandler(); void newSequence(); - void addKeyframe(double sequenceTime); + void addCameraKeyframe(double sequenceTime); + void addScriptKeyframe(double sequenceTime, std::string script); void removeKeyframe(int index); void updateKeyframe(int index); void moveKeyframe(int index, double sequenceTime); - bool saveSequence(std::optional filename); - void loadSequence(std::string filename); - void preSynchronization(double dt); + void saveSequence(std::filesystem::path filename); + void loadSequence(std::filesystem::path filename); void play(); - void pause(); - void setSequenceTime(double sequenceTime); - void jumpToKeyframe(int index); bool hasKeyframeRecording() const; std::vector keyframes() const; static openspace::scripting::LuaLibrary luaLibrary(); private: - void sortKeyframes(); - - Keyframe newKeyframe(double sequenceTime); - bool isInRange(int index) const; - - std::vector _keyframes; - std::string _filename; - bool _isPlaying = false; - bool _hasStateChanged = false; - double _sequenceTime = 0.0; + SessionRecording _timeline; }; } // namespace openspace -#endif // __OPENSPACE_CORE___KEYFRAMERECORDING___H__ +#endif // __OPENSPACE_CORE___KEYFRAMERECORDINGHANDLER___H__ diff --git a/include/openspace/interaction/sessionrecording.h b/include/openspace/interaction/sessionrecording.h index 1adfbce109..1af1bf9e39 100644 --- a/include/openspace/interaction/sessionrecording.h +++ b/include/openspace/interaction/sessionrecording.h @@ -25,863 +25,62 @@ #ifndef __OPENSPACE_CORE___SESSIONRECORDING___H__ #define __OPENSPACE_CORE___SESSIONRECORDING___H__ -#include - #include -#include -#include +#include +#include +#include +#include +#include #include -#include namespace openspace::interaction { -struct ConversionError : public ghoul::RuntimeError { - explicit ConversionError(std::string msg); +enum class DataMode { + Ascii = 0, + Binary }; -class SessionRecording : public properties::PropertyOwner { -public: +struct SessionRecording { + struct Entry { + auto operator<=>(const SessionRecording::Entry&) const = default; - inline static const std::string FileHeaderTitle = "OpenSpace_record/playback"; - inline static const std::string HeaderCameraAscii = "camera"; - inline static const std::string HeaderTimeAscii = "time"; - inline static const std::string HeaderScriptAscii = "script"; - inline static const std::string HeaderCommentAscii = "#"; - inline static const char HeaderCameraBinary = 'c'; - inline static const char HeaderTimeBinary = 't'; - inline static const char HeaderScriptBinary = 's'; - inline static const std::string FileExtensionBinary = ".osrec"; - inline static const std::string FileExtensionAscii = ".osrectxt"; + using Camera = KeyframeNavigator::CameraPose; + using Script = std::string; - enum class DataMode { - Ascii = 0, - Binary, - Unknown + double timestamp = 0.0; + double simulationTime = 0.0; + std::variant value; }; - enum class SessionState { - Idle = 0, - Recording, - Playback, - PlaybackPaused - }; - - struct Timestamps { - double timeOs; - double timeRec; - double timeSim; - }; - - /** - * Struct for storing a script substring that, if found in a saved script, will be - * replaced by its substringReplacement counterpart. - */ - struct ScriptSubstringReplace { - std::string substringFound; - std::string substringReplacement; - ScriptSubstringReplace(std::string found, std::string replace) - : substringFound(found) - , substringReplacement(replace) {} - }; - - static const size_t FileHeaderVersionLength = 5; - char FileHeaderVersion[FileHeaderVersionLength+1] = "01.00"; - char TargetConvertVersion[FileHeaderVersionLength+1] = "01.00"; - static const char DataFormatAsciiTag = 'A'; - static const char DataFormatBinaryTag = 'B'; - static const size_t keyframeHeaderSize_bytes = 33; - static const size_t saveBufferCameraSize_min = 82; - static const size_t saveBufferStringSize_max = 2000; - static const size_t _saveBufferMaxSize_bytes = keyframeHeaderSize_bytes + - + saveBufferCameraSize_min + saveBufferStringSize_max; - - using CallbackHandle = int; - using StateChangeCallback = std::function; - - SessionRecording(); - SessionRecording(bool isGlobal); - - ~SessionRecording() override = default; - - /** - * Used to de-initialize the session recording feature. Any recording or playback - * in progress will be stopped, files closed, and keyframes in memory deleted. - */ - void deinitialize(); - - /** - * This is called with every rendered frame. If in recording state, the camera state - * will be saved to the recording file (if its state has changed since last). If in - * playback state, the next keyframe will be used (if it is time to do so). - */ - void preSynchronization(); - - /** - * If enabled, calling this function will render information about the session - * recording that is currently taking place to the screen. - */ - void render(); - - /** - * Current time based on playback mode. - */ - double currentTime() const; - - /** - * Fixed delta time set by user for use during saving of frame during playback mode. - */ - double fixedDeltaTimeDuringFrameOutput() const; - - /** - * Returns the number of microseconds that have elapsed since playback started, if - * playback is set to be in the mode where a screenshot is captured with every - * rendered frame (enableTakeScreenShotDuringPlayback() is used to enable this mode). - * At the start of playback, this timer is set to the current steady_clock value. - * However, during playback it is incremented by the fixed framerate of the playback - * rather than the actual clock value (as in normal operation). - * - * \return Number of microseconds elapsed since playback started in terms of the - * number of rendered frames multiplied by the fixed time increment per frame - */ - std::chrono::steady_clock::time_point currentPlaybackInterpolationTime() const; - - /** - * Returns the simulated application time. This simulated application time is only - * used when playback is set to be in the mode where a screenshot is captured with - * every rendered frame (enableTakeScreenShotDuringPlayback() is used to enable this - * mode). At the start of playback, this timer is set to the value of the current - * applicationTime function provided by the window delegate (used during normal mode - * or playback). However, during playback it is incremented by the fixed framerate of - * the playback rather than the actual clock value. - * - * \return Application time in seconds, for use in playback-with-frames mode - */ - double currentApplicationInterpolationTime() const; - - /** - * Starts a recording session, which will save data to the provided filename according - * to the data format specified, and will continue until recording is stopped using - * stopRecording() method. - * - * \param filename File saved with recorded keyframes - * \return `true` if recording to file starts without errors - */ - bool startRecording(const std::string& filename); - - /** - * Starts a recording session, which will save data to the provided filename in ASCII - * data format until recording is stopped using stopRecording() method. - * - * \param dataMode The format in which the session recording is stored - */ - void setRecordDataFormat(DataMode dataMode); - - /** - * Used to stop a recording in progress. If open, the recording file will be closed, - * and all keyframes deleted from memory. - */ - void stopRecording(); - - /** - * Used to check if a session recording is in progress. - * - * \return `true` if recording is in progress - */ - bool isRecording() const; - - /** - * Starts a playback session, which can run in one of three different time modes. - * - * \param filename File containing recorded keyframes to play back. The file path is - * relative to the base recordings directory specified in the config - * file by the RECORDINGS variable - * \param timeMode Which of the 3 time modes to use for time reference during - * \param forceSimTimeAtStart If true simulation time is forced to that of playback - * playback: recorded time, application time, or simulation time. See the - * LuaLibrary entry for SessionRecording for details on these time modes - * \param loop If true then the file will playback in loop mode, continuously looping - * back to the beginning until it is manually stopped - * \param shouldWaitForFinishedTiles If true, the playback will wait for tiles to be - * finished before progressing to the next frame. This value is only used when - * `enableTakeScreenShotDuringPlayback` was called before. Otherwise this value - * will be ignored - * - * \return `true` if recording to file starts without errors - */ - bool startPlayback(std::string& filename, KeyframeTimeRef timeMode, - bool forceSimTimeAtStart, bool loop, bool shouldWaitForFinishedTiles); - - /** - * Used to stop a playback in progress. If open, the playback file will be closed, and - * all keyframes deleted from memory. - */ - void stopPlayback(); - - /** - * Returns playback pause status. - * - * \return `true` if playback is paused - */ - bool isPlaybackPaused(); - - /** - * Pauses a playback session. This does both the normal pause functionality of setting - * simulation delta time to zero, and pausing the progression through the timeline. - * - * \param pause If `true`, then will set playback timeline progression to zero - */ - void setPlaybackPause(bool pause); - - /** - * Enables that rendered frames should be saved during playback. - * - * \param fps Number of frames per second. - */ - void enableTakeScreenShotDuringPlayback(int fps); - - /** - * Used to disable that renderings are saved during playback. - */ - void disableTakeScreenShotDuringPlayback(); - - /** - * Used to check if a session playback is in progress. - * - * \return `true` if playback is in progress - */ - bool isPlayingBack() const; - - /** - * Is saving frames during playback. - */ - bool isSavingFramesDuringPlayback() const; - - bool shouldWaitForTileLoading() const; - - /** - * Used to obtain the state of idle/recording/playback. - * - * \return int value of state as defined by struct SessionState - */ - SessionState state() const; - - /** - * Used to trigger a save of the camera states (position, rotation, focus node, - * whether it is following the rotation of a node, and timestamp). The data will be - * saved to the recording file only if a recording is currently in progress. - */ - void saveCameraKeyframeToTimeline(); - - /** - * Used to trigger a save of the current timing states. The data will be saved to the - * recording file only if a recording is currently in progress. - */ - void saveTimeKeyframeToTimeline(); - - /** - * Used to trigger a save of a script to the recording file, but only if a recording - * is currently in progress. - * - * \param script String of the Lua command to be saved - */ - void saveScriptKeyframeToTimeline(std::string script); - - /** - * \return The Lua library that contains all Lua functions available to affect the - * interaction - */ - static openspace::scripting::LuaLibrary luaLibrary(); - - /** - * Used to request a callback for notification of playback state change. - * - * \param cb Function handle for callback - * \return CallbackHandle value of callback number - */ - CallbackHandle addStateChangeCallback(StateChangeCallback cb); - - /** - * Removes the callback for notification of playback state change. - * - * \param callback Function handle for the callback - */ - void removeStateChangeCallback(CallbackHandle handle); - - /** - * Provides list of available playback files. - * - * \return Vector of filenames in recordings dir - */ - std::vector playbackList() const; - - /** - * Reads a camera keyframe from a binary format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a camera keyframe which contains camera details - * \param file An ifstream reference to the playback file being read - * \param lineN Keyframe number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readCameraKeyframeBinary(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, std::ifstream& file, int lineN); - - /** - * Reads a camera keyframe from an ascii format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a camera keyframe which contains camera details - * \param currentParsingLine String containing the most current line that was read - * \param lineN Line number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readCameraKeyframeAscii(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, const std::string& currentParsingLine, - int lineN); - - /** - * Reads a time keyframe from a binary format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a time keyframe which contains time details - * \param file An ifstream reference to the playback file being read - * \param lineN Keyframe number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readTimeKeyframeBinary(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, std::ifstream& file, int lineN); - - /** - * Reads a time keyframe from an ascii format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a time keyframe which contains time details - * \param currentParsingLine String containing the most current line that was read - * \param lineN Line number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readTimeKeyframeAscii(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, const std::string& currentParsingLine, - int lineN); - - /** - * Reads a script keyframe from a binary format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a script keyframe which contains the size of the script (in - * chars) and the text itself - * \param file An ifstream reference to the playback file being read - * \param lineN Keyframe number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readScriptKeyframeBinary(Timestamps& times, - datamessagestructures::ScriptMessage& kf, std::ifstream& file, int lineN); - - /** - * Reads a script keyframe from an ascii format playback file, and populates input - * references with the parameters of the keyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a script keyframe which contains the size of the script (in - * chars) and the text itself - * \param currentParsingLine String containing the most current line that was read - * \param lineN Line number in playback file where this keyframe resides - * \return `true` if data read has no errors - */ - bool readScriptKeyframeAscii(Timestamps& times, - datamessagestructures::ScriptMessage& kf, const std::string& currentParsingLine, - int lineN); - - /** - * Writes a camera keyframe to a binary format recording file using a CameraKeyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a camera keyframe which contains the camera details - * \param kfBuffer A buffer temporarily used for preparing data to be written - * \param file An ofstream reference to the recording file being written-to - */ - void saveCameraKeyframeBinary(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, unsigned char* kfBuffer, - std::ofstream& file); - - /** - * Writes a camera keyframe to an ascii format recording file using a CameraKeyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a camera keyframe which contains the camera details - * \param file An ofstream reference to the recording file being written-to - */ - void saveCameraKeyframeAscii(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, std::ofstream& file); - - /** - * Writes a time keyframe to a binary format recording file using a TimeKeyframe - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a time keyframe which contains the time details - * \param kfBuffer A buffer temporarily used for preparing data to be written - * \param file An ofstream reference to the recording file being written-to - */ - void saveTimeKeyframeBinary(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, unsigned char* kfBuffer, - std::ofstream& file); - - /** - * Writes a time keyframe to an ascii format recording file using a TimeKeyframe. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param kf Reference to a time keyframe which contains the time details - * \param file An ofstream reference to the recording file being written-to - */ - void saveTimeKeyframeAscii(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, std::ofstream& file); - - /** - * Writes a script keyframe to a binary format recording file using a ScriptMessage. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param sm Reference to a ScriptMessage object which contains the script details - * \param smBuffer A buffer temporarily used for preparing data to be written - * \param file An ofstream reference to the recording file being written-to - */ - void saveScriptKeyframeBinary(Timestamps& times, - datamessagestructures::ScriptMessage& sm, unsigned char* smBuffer, - std::ofstream& file); - - /** - * Writes a script keyframe to an ascii format recording file using a ScriptMessage. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param sm Reference to a ScriptMessage which contains the script details - * \param file An ofstream reference to the recording file being written-to - */ - void saveScriptKeyframeAscii(Timestamps& times, - datamessagestructures::ScriptMessage& sm, std::ofstream& file); - - /** - * Since session recordings only record changes, the initial conditions aren't - * preserved when a playback starts. This function is called whenever a property value - * is set and a recording is in progress. Before the set happens, this function will - * read the current value of the property and store it so that when the recording is - * finished, the initial state will be added as a set property command at the - * beginning of the recording file, to be applied when playback starts. - * - * \param prop The property being set - */ - void savePropertyBaseline(properties::Property& prop); - - /** - * Reads header information from a session recording file. - * - * \param stream Reference to ifstream that contains the session recording file data - * \param readLen_chars Number of characters to be read, which may be the expected - * length of the header line, or an arbitrary number of characters within it - */ - static std::string readHeaderElement(std::ifstream& stream, size_t readLenChars); - - /** - * Reads header information from a session recording file. - * - * \param stream Reference to ifstream that contains the session recording file data - * \param readLen_chars Number of characters to be read, which may be the expected - * length of the header line, or an arbitrary number of characters within it - */ - static std::string readHeaderElement(std::stringstream& stream, size_t readLenChars); - - /** - * Writes a header to a binary recording file buffer. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param type Single character signifying the keyframe type - * \param kfBuffer The char buffer holding the recording info to be written - * \param idx Index into write buffer (this is updated with the num of chars written) - */ - static void saveHeaderBinary(Timestamps& times, char type, unsigned char* kfBuffer, - size_t& idx); - - /** - * Writes a header to an ASCII recording file buffer. - * - * \param times Reference to a timestamps structure which contains recorded times - * \param type String signifying the keyframe type - * \param line The stringstream buffer being written to - */ - static void saveHeaderAscii(Timestamps& times, const std::string& type, - std::stringstream& line); - - /** - * Saves a keyframe to an ASCII recording file. - * - * \param entry The ASCII string version of the keyframe (any type) - * \param file `std::ofstream` object to write to - */ - static void saveKeyframeToFile(const std::string& entry, std::ofstream& file); - - /** - * Checks if a specified recording file ends with a particular file extension. - * - * \param filename The name of the file to record to - * \param extension The file extension to check for - */ - static bool hasFileExtension(const std::string& filename, - const std::string& extension); - - /** - * Converts file format of a session recording file to the current format version - * (will determine the file format conversion to convert from based on the file's - * header version number). - * - * \param filename Name of the file to convert - * \param depth iteration number to prevent runaway recursion (init call with zero) - * \return String containing the filename of the previous conversion step. This is - * used if there are multiple conversion steps where each step has to use - * the output of the previous. - */ - std::string convertFile(std::string filename, int depth = 0); - - /** - * Converts file format of a session recording file to the current format version - * (will determine the file format conversion to convert from based on the file's - * header version number). Accepts a relative path (currently from task runner dir) - * rather than a path assumed to be relative to `${RECORDINGS}`. - * - * \param filenameRelative name of the file to convert - */ - void convertFileRelativePath(std::string filenameRelative); - - /** - * Goes to legacy session recording inherited class, and calls its #convertFile - * method, and then returns the resulting conversion filename. - * - * \param filename Name of the file to convert - * \param depth Iteration number to prevent runaway recursion (init call with zero) - * \return string containing the filename of the conversion - */ - virtual std::string getLegacyConversionResult(std::string filename, int depth); - - /** - * Version string for file format version currently supported by this class. - * - * \return string of the file format version this class supports - */ - virtual std::string fileFormatVersion(); - - /** - * Version string for file format version that a conversion operation will convert to - * (e.g. upgrades to this version). This is only relevant for inherited classes that - * support conversion from legacy versions (if called in the base class, it will - * return its own current version). - * - * \return string of the file format version this class supports - */ - virtual std::string targetFileFormatVersion(); - - /** - * Determines a filename for the conversion result based on the original filename and - the file format version number. - * - * \param filename source filename to be converted - * \param mode Whether the file is binary or text-based - * - * \return pathname of the converted version of the file - */ - std::string determineConversionOutFilename(const std::string& filename, - DataMode mode); - -protected: - properties::BoolProperty _renderPlaybackInformation; - properties::BoolProperty _ignoreRecordedScale; - properties::BoolProperty _addModelMatrixinAscii; - - enum class RecordedType { - Camera = 0, - Time, - Script, - Invalid - }; - struct TimelineEntry { - RecordedType keyframeType; - unsigned int idxIntoKeyframeTypeArray; - Timestamps t3stamps; - }; - double _timestampRecordStarted = 0.0; - Timestamps _timestamps3RecordStarted{ 0.0, 0.0, 0.0 }; - double _timestampPlaybackStarted_application = 0.0; - double _timestampPlaybackStarted_simulation = 0.0; - double _timestampApplicationStarted_simulation = 0.0; - bool hasCameraChangedFromPrev(const datamessagestructures::CameraKeyframe& kfNew); - double appropriateTimestamp(Timestamps t3stamps); - double equivalentSimulationTime(double timeOs, double timeRec, double timeSim); - double equivalentApplicationTime(double timeOs, double timeRec, double timeSim); - void recordCurrentTimePauseState(); - void recordCurrentTimeRate(); - bool handleRecordingFile(std::string filenameIn); - static bool isPath(std::string& filename); - void removeTrailingPathSlashes(std::string& filename) const; - bool playbackCamera(); - bool playbackTimeChange(); - bool playbackScript(); - bool playbackAddEntriesToTimeline(); - void signalPlaybackFinishedForComponent(RecordedType type); - void handlePlaybackEnd(); - - bool findFirstCameraKeyframeInTimeline(); - Timestamps generateCurrentTimestamp3(double keyframeTime) const; - static void saveStringToFile(const std::string& s, unsigned char* kfBuffer, - size_t& idx, std::ofstream& file); - static void saveKeyframeToFileBinary(unsigned char* buffer, size_t size, - std::ofstream& file); - - bool addKeyframe(Timestamps t3stamps, - interaction::KeyframeNavigator::CameraPose keyframe, int lineNum); - bool addKeyframe(Timestamps t3stamps, - datamessagestructures::TimeKeyframe keyframe, int lineNum); - bool addKeyframe(Timestamps t3stamps, - std::string scriptToQueue, int lineNum); - bool addKeyframeToTimeline(std::vector& timeline, RecordedType type, - size_t indexIntoTypeKeyframes, Timestamps t3stamps, int lineNum); - - void initializePlayback_time(double now); - void initializePlayback_modeFlags(); - bool initializePlayback_timeline(); - void initializePlayback_triggerStart(); - void moveAheadInTime(); - void lookForNonCameraKeyframesThatHaveComeDue(double currTime); - void updateCameraWithOrWithoutNewKeyframes(double currTime); - bool isTimeToHandleNextNonCameraKeyframe(double currTime); - bool processNextNonCameraKeyframeAheadInTime(); - bool findNextFutureCameraIndex(double currTime); - bool processCameraKeyframe(double now); - bool processScriptKeyframe(); - bool readSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, - Timestamps& times, DataMode mode, std::ifstream& file, - std::string& inLine, const int lineNum); - void saveSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, - Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer); - bool readSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, - Timestamps& times, DataMode mode, std::ifstream& file, std::string& inLine, - const int lineNum); - void saveSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, - Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer); - bool readSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, - Timestamps& times, DataMode mode, std::ifstream& file, std::string& inLine, - const int lineNum); - void saveSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, - Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer); - void saveScriptKeyframeToPropertiesBaseline(std::string script); - bool isPropertyAllowedForBaseline(const std::string& propString); - unsigned int findIndexOfLastCameraKeyframeInTimeline(); - bool doesTimelineEntryContainCamera(unsigned int index) const; - void trimCommandsFromScriptIfFound(std::string& script); - void replaceCommandsFromScriptIfFound(std::string& script); - - RecordedType getNextKeyframeType(); - RecordedType getPrevKeyframeType(); - double getNextTimestamp(); - double getPrevTimestamp(); - void cleanUpPlayback(); - void cleanUpRecording(); - void cleanUpTimelinesAndKeyframes(); - bool convertEntries(std::string& inFilename, std::stringstream& inStream, - DataMode mode, int lineNum, std::ofstream& outFile); - virtual bool convertCamera(std::stringstream& inStream, DataMode mode, int lineNum, - std::string& inputLine, std::ofstream& outFile, unsigned char* buffer); - virtual bool convertTimeChange(std::stringstream& inStream, DataMode mode, - int lineNum, std::string& inputLine, std::ofstream& outFile, - unsigned char* buffer); - virtual bool convertScript(std::stringstream& inStream, DataMode mode, int lineNum, - std::string& inputLine, std::ofstream& outFile, unsigned char* buffer); - DataMode readModeFromHeader(const std::string& filename); - void readPlaybackHeader_stream(std::stringstream& conversionInStream, - std::string& version, DataMode& mode); - void populateListofLoadedSceneGraphNodes(); - - void checkIfScriptUsesScenegraphNode(std::string s); - bool checkForScenegraphNodeAccessScene(const std::string& s); - bool checkForScenegraphNodeAccessNav(std::string& navTerm); - std::string extractScenegraphNodeFromScene(const std::string& s); - bool checkIfInitialFocusNodeIsLoaded(unsigned int firstCamIndex); - std::string isolateTermFromQuotes(std::string s); - void eraseSpacesFromString(std::string& s); - std::string getNameFromSurroundingQuotes(std::string& s); - - static void writeToFileBuffer(unsigned char* buf, size_t& idx, double src); - static void writeToFileBuffer(unsigned char* buf, size_t& idx, std::vector& cv); - static void writeToFileBuffer(unsigned char* buf, size_t& idx, unsigned char c); - static void writeToFileBuffer(unsigned char* buf, size_t& idx, bool b); - void readFileIntoStringStream(std::string filename, - std::ifstream& inputFstream, std::stringstream& stream); - - DataMode _recordingDataMode = DataMode::Binary; - SessionState _state = SessionState::Idle; - SessionState _lastState = SessionState::Idle; - std::string _playbackFilename; - std::ifstream _playbackFile; - std::string _playbackLineParsing; - std::ofstream _recordFile; - int _playbackLineNum = 1; - int _recordingEntryNum = 1; - KeyframeTimeRef _playbackTimeReferenceMode; - datamessagestructures::CameraKeyframe _prevRecordedCameraKeyframe; - bool _playbackActive_camera = false; - bool _playbackActive_time = false; - bool _playbackActive_script = false; - bool _hasHitEndOfCameraKeyframes = false; - bool _playbackPausedWithinDeltaTimePause = false; - bool _playbackLoopMode = false; - bool _playbackForceSimTimeAtStart = false; - double _playbackPauseOffset = 0.0; - double _previousTime = 0.0; - - bool _saveRenderingDuringPlayback = false; - double _saveRenderingDeltaTime = 1.0 / 30.0; - double _saveRenderingCurrentRecordedTime = 0.0; - bool _shouldWaitForFinishLoadingWhenPlayback = false; - std::chrono::steady_clock::duration _saveRenderingDeltaTime_interpolation_usec; - std::chrono::steady_clock::time_point _saveRenderingCurrentRecordedTime_interpolation; - double _saveRenderingCurrentApplicationTime_interpolation = 0.0; - long long _saveRenderingClockInterpolation_countsPerSec = 1; - bool _saveRendering_isFirstFrame = true; - - unsigned char _keyframeBuffer[_saveBufferMaxSize_bytes]; - - bool _cleanupNeededRecording = false; - bool _cleanupNeededPlayback = false; - const std::string scriptReturnPrefix = "return "; - - std::vector _keyframesCamera; - std::vector _keyframesTime; - std::vector _keyframesScript; - std::vector _timeline; - - std::vector _keyframesSavePropertiesBaseline_scripts; - std::vector _keyframesSavePropertiesBaseline_timeline; - std::vector _propertyBaselinesSaved; - const std::vector _propertyBaselineRejects = { - "NavigationHandler.OrbitalNavigator.Anchor", - "NavigationHandler.OrbitalNavigator.Aim", - "NavigationHandler.OrbitalNavigator.RetargetAnchor", - "NavigationHandler.OrbitalNavigator.RetargetAim" - }; - - //A script that begins with an exact match of any of the strings contained in - // _scriptRejects will not be recorded - const std::vector _scriptRejects = { - "openspace.sessionRecording.enableTakeScreenShotDuringPlayback", - "openspace.sessionRecording.startPlayback", - "openspace.sessionRecording.stopPlayback", - "openspace.sessionRecording.startRecording", - "openspace.sessionRecording.stopRecording", - "openspace.scriptScheduler.clear" - }; - const std::vector _navScriptsUsingNodes = { - "RetargetAnchor", - "Anchor", - "Aim" - }; - - //Any script snippet included in this vector will be trimmed from any script - // from the script manager, before it is recorded in the session recording file. - // The remainder of the script will be retained. - const std::vector _scriptsToBeTrimmed = { - "openspace.sessionRecording.togglePlaybackPause" - }; - - //Any script snippet included in this vector will be trimmed from any script - // from the script manager, before it is recorded in the session recording file. - // The remainder of the script will be retained. - const std::vector _scriptsToBeReplaced = { - { - "openspace.time.pauseToggleViaKeyboard", - "openspace.time.interpolateTogglePause" - } - }; - std::vector _loadedNodes; - - unsigned int _idxTimeline_nonCamera = 0; - unsigned int _idxTime = 0; - unsigned int _idxScript = 0; - - unsigned int _idxTimeline_cameraPtrNext = 0; - unsigned int _idxTimeline_cameraPtrPrev = 0; - - unsigned int _idxTimeline_cameraFirstInTimeline = 0; - double _cameraFirstInTimeline_timestamp = 0; - - int _nextCallbackHandle = 0; - std::vector> _stateChangeCallbacks; - - DataMode _conversionDataMode = DataMode::Binary; - int _conversionLineNum = 1; - const int _maximumRecursionDepth = 50; -}; - -// Instructions for bumping the file format version with new changes: -// -// 1. Create a new subclass with the current version # in its name, such as: -// SessionRecording_legacy_####, which inherits from SessionRecording -// 2. Override any method that changes in the new version. This includes both -// methods in SessionRecording class and structs in -// openspace::datamessagestructure that do data read/writes. Make the modified -// method/struct virtual, and override it in the new legacy subclass. This -// override will contain the code as it is before the new changes. This will -// need to be done in every legacy subclass/struct that exists, but only if that -// subclass does NOT already contain an override of that method/struct. -// 3. Override FileHeaderVersion with the version # of the new subclass (which is -// the version being replaced by the new changes). -// 4. Override TargetConvertVersion with the version # with the new changes. This -// is now the version that this legacy subclass converts up to. -// 5. Override getLegacyConversionResult method so that it creates an instance of -// the new version subclass. This is how the current version looks back to the -// legacy version that preceded it. -// 6. The convert method for frame types that changed will need to be changed -// (for example SessionRecording_legacy_0085::convertScript uses its own -// override of script keyframe for the conversion functionality). - -class SessionRecording_legacy_0085 : public SessionRecording { -public: - SessionRecording_legacy_0085() : SessionRecording() {} - ~SessionRecording_legacy_0085() override {} - char FileHeaderVersion[FileHeaderVersionLength+1] = "00.85"; - char TargetConvertVersion[FileHeaderVersionLength+1] = "01.00"; - std::string fileFormatVersion() override { - return std::string(FileHeaderVersion); - } - std::string targetFileFormatVersion() override { - return std::string(TargetConvertVersion); - } - std::string getLegacyConversionResult(std::string filename, int depth) override; - - struct ScriptMessage_legacy_0085 : public datamessagestructures::ScriptMessage { - void read(std::istream* in) override { - size_t strLen; - //Read string length from file - in->read(reinterpret_cast(&strLen), sizeof(strLen)); - if (strLen > saveBufferStringSize_max) { - throw ConversionError("Invalid script size for conversion read"); + auto operator<=>(const SessionRecording&) const = default; + + std::vector entries; + + bool hasCameraFrame() const noexcept; + + // Call the provided \p function for all entries of the specified type \tparam T. The + // function calls will be ordered by the entries timestamps. If the callback function + // returns `true`, the loop is aborted + template + void forAll(std::function function) { + for (const Entry& e : entries) { + if (std::holds_alternative(e.value)) { + bool cont = function(std::get(e.value)); + if (cont) { + break; + } } - //Read back full string - std::vector temp(strLen + 1); - in->read(temp.data(), strLen); - temp[strLen] = '\0'; - - _script.erase(); - _script = temp.data(); } - }; - -protected: - bool convertScript(std::stringstream& inStream, DataMode mode, int lineNum, - std::string& inputLine, std::ofstream& outFile, unsigned char* buffer) override; + } }; -} // namespace openspace +SessionRecording loadSessionRecording(const std::filesystem::path& filename); +void saveSessionRecording(const std::filesystem::path& filename, + const SessionRecording& sessionRecording, DataMode dataMode); -#include "sessionrecording.inl" +std::vector sessionRecordingToDictionary( + const SessionRecording& recording); -#endif // __OPENSPACE_CORE___SESSIONRECORDING___H__ +} // namespace openspace::interaction + +#endif // __OPENSPACE_CORE___SESSIONRECORDINGHANDLER___H__ diff --git a/include/openspace/interaction/sessionrecording.inl b/include/openspace/interaction/sessionrecording.inl deleted file mode 100644 index 0be76b5709..0000000000 --- a/include/openspace/interaction/sessionrecording.inl +++ /dev/null @@ -1,72 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -namespace openspace::interaction { - -template -T nextKeyframeObj(unsigned int index, const std::vector& keyframeContainer, - std::function finishedCallback) -{ - if (index >= (keyframeContainer.size() - 1)) { - if (index == (keyframeContainer.size() - 1)) { - finishedCallback(); - } - return keyframeContainer.back(); - } - else if (index < keyframeContainer.size()) { - return keyframeContainer[index]; - } - else { - return keyframeContainer.back(); - } -} - -template -T prevKeyframeObj(unsigned int index, const std::vector& keyframeContainer) { - if (index >= keyframeContainer.size()) { - return keyframeContainer.back(); - } - else if (index > 0) { - return keyframeContainer[index - 1]; - } - else { - return keyframeContainer.front(); - } -} - -template -T readFromPlayback(std::ifstream& stream) { - T res; - stream.read(reinterpret_cast(&res), sizeof(T)); - return res; -} - -template -T readFromPlayback(std::stringstream& stream) { - T res; - stream.read(reinterpret_cast(&res), sizeof(T)); - return res; -} - -} // namespace openspace::interaction diff --git a/include/openspace/interaction/sessionrecordinghandler.h b/include/openspace/interaction/sessionrecordinghandler.h new file mode 100644 index 0000000000..796b0e7fcc --- /dev/null +++ b/include/openspace/interaction/sessionrecordinghandler.h @@ -0,0 +1,288 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_CORE___SESSIONRECORDINGHANDLER___H__ +#define __OPENSPACE_CORE___SESSIONRECORDINGHANDLER___H__ + +#include + +#include +#include +#include + +namespace openspace::interaction { + +class SessionRecordingHandler : public properties::PropertyOwner { +public: + enum class SessionState { + Idle = 0, + Recording, + Playback, + PlaybackPaused + }; + + using CallbackHandle = int; + using StateChangeCallback = std::function; + + SessionRecordingHandler(); + ~SessionRecordingHandler() override = default; + + /** + * This is called with every rendered frame. If in recording state, the camera state + * will be saved to the recording file (if its state has changed since last). If in + * playback state, the next keyframe will be used (if it is time to do so). + */ + void preSynchronization(double dt); + + /** + * If enabled, calling this function will render information about the session + * recording that is currently taking place to the screen. + */ + void render() const; + + /** + * Fixed delta time set by user for use during saving of frame during playback mode. + */ + double fixedDeltaTimeDuringFrameOutput() const; + + /** + * Returns the number of microseconds that have elapsed since playback started, if + * playback is set to be in the mode where a screenshot is captured with every + * rendered frame (enableTakeScreenShotDuringPlayback() is used to enable this mode). + * At the start of playback, this timer is set to the current steady_clock value. + * However, during playback it is incremented by the fixed framerate of the playback + * rather than the actual clock value (as in normal operation). + * + * \return Number of microseconds elapsed since playback started in terms of the + * number of rendered frames multiplied by the fixed time increment per frame + */ + std::chrono::steady_clock::time_point currentPlaybackInterpolationTime() const; + + /** + * Returns the simulated application time. This simulated application time is only + * used when playback is set to be in the mode where a screenshot is captured with + * every rendered frame (enableTakeScreenShotDuringPlayback() is used to enable this + * mode). At the start of playback, this timer is set to the value of the current + * applicationTime function provided by the window delegate (used during normal mode + * or playback). However, during playback it is incremented by the fixed framerate of + * the playback rather than the actual clock value. + * + * \return Application time in seconds, for use in playback-with-frames mode + */ + double currentApplicationInterpolationTime() const; + + /** + * Starts a recording session, which will save data to the provided filename according + * to the data format specified, and will continue until recording is stopped using + * stopRecording() method. + * + * \return `true` if recording to file starts without errors + */ + void startRecording(); + + /** + * Used to stop a recording in progress. If open, the recording file will be closed, + * and all keyframes deleted from memory. + * \param filename File saved with recorded keyframes + */ + void stopRecording(const std::filesystem::path& filename, DataMode dataMode); + + /** + * Used to check if a session recording is in progress. + * + * \return `true` if recording is in progress + */ + bool isRecording() const; + + /** + * Starts a playback session, which can run in one of three different time modes. + * + * \param filename File containing recorded keyframes to play back. The file path is + * relative to the base recordings directory specified in the config + * file by the RECORDINGS variable + * \param timeMode Which of the 3 time modes to use for time reference during + * \param loop If true then the file will playback in loop mode, continuously looping + * back to the beginning until it is manually stopped + * \param shouldWaitForFinishedTiles If true, the playback will wait for tiles to be + * finished before progressing to the next frame. This value is only used when + * `enableTakeScreenShotDuringPlayback` was called before. Otherwise this value + * will be ignored + */ + void startPlayback(SessionRecording timeline, bool loop, + bool shouldWaitForFinishedTiles, std::optional saveScreenshotFps); + + /** + * Used to stop a playback in progress. If open, the playback file will be closed, and + * all keyframes deleted from memory. + */ + void stopPlayback(); + + /** + * Returns playback pause status. + * + * \return `true` if playback is paused + */ + bool isPlaybackPaused() const; + + /** + * Pauses a playback session. This does both the normal pause functionality of setting + * simulation delta time to zero, and pausing the progression through the timeline. + * + * \param pause If `true`, then will set playback timeline progression to zero + */ + void setPlaybackPause(bool pause); + + /** + * Enables that rendered frames should be saved during playback. + * + * \param fps Number of frames per second. + */ + //void enableTakeScreenShotDuringPlayback(int fps); + + /** + * Used to disable that renderings are saved during playback. + */ + //void disableTakeScreenShotDuringPlayback(); + + /** + * Used to check if a session playback is in progress. + * + * \return `true` if playback is in progress + */ + bool isPlayingBack() const; + + void seek(double recordingTime); + + /** + * Is saving frames during playback. + */ + bool isSavingFramesDuringPlayback() const; + + bool shouldWaitForTileLoading() const; + + /** + * Used to obtain the state of idle/recording/playback. + * + * \return int value of state as defined by struct SessionState + */ + SessionState state() const; + + /** + * Used to trigger a save of a script to the recording file, but only if a recording + * is currently in progress. + * + * \param script String of the Lua command to be saved + */ + void saveScriptKeyframeToTimeline(std::string script); + + /** + * \return The Lua library that contains all Lua functions available to affect the + * interaction + */ + static openspace::scripting::LuaLibrary luaLibrary(); + + /** + * Used to request a callback for notification of playback state change. + * + * \param cb Function handle for callback + * \return CallbackHandle value of callback number + */ + CallbackHandle addStateChangeCallback(StateChangeCallback cb); + + /** + * Removes the callback for notification of playback state change. + * + * \param callback Function handle for the callback + */ + void removeStateChangeCallback(CallbackHandle handle); + + /** + * Provides list of available playback files. + * + * \return Vector of filenames in recordings dir + */ + std::vector playbackList() const; + + /** + * Since session recordings only record changes, the initial conditions aren't + * preserved when a playback starts. This function is called whenever a property value + * is set and a recording is in progress. Before the set happens, this function will + * read the current value of the property and store it so that when the recording is + * finished, the initial state will be added as a set property command at the + * beginning of the recording file, to be applied when playback starts. + * + * \param prop The property being set + */ + void savePropertyBaseline(properties::Property& prop); + +private: + void tickPlayback(double dt); + void tickRecording(double dt); + + void setupPlayback(double startTime); + + void cleanUpTimelinesAndKeyframes(); + + void checkIfScriptUsesScenegraphNode(std::string_view s) const; + + + properties::BoolProperty _renderPlaybackInformation; + properties::BoolProperty _ignoreRecordedScale; + properties::BoolProperty _addModelMatrixinAscii; + + struct { + double elapsedTime = 0.0; + bool isLooping = false; + bool playbackPausedWithDeltaTimePause = false; + bool waitForLoading = false; + + struct { + bool enabled = false; + double deltaTime = 1.0 / 30.0; + std::chrono::steady_clock::time_point currentRecordedTime; + double currentApplicationTime = 0.0; + } saveScreenshots; + } _playback; + + struct { + double elapsedTime = 0.0; + } _recording; + + SessionState _state = SessionState::Idle; + SessionState _lastState = SessionState::Idle; + + + SessionRecording _timeline; + std::vector::const_iterator _currentEntry = + _timeline.entries.end(); + std::unordered_map _savePropertiesBaseline; + std::vector _loadedNodes; + + int _nextCallbackHandle = 0; + std::vector> _stateChangeCallbacks; +}; + +} // namespace openspace::interaction + +#endif // __OPENSPACE_CORE___SESSIONRECORDINGHANDLER___H__ diff --git a/include/openspace/interaction/tasks/convertrecfileversiontask.h b/include/openspace/interaction/tasks/convertrecfileversiontask.h deleted file mode 100644 index 5ae50c68ca..0000000000 --- a/include/openspace/interaction/tasks/convertrecfileversiontask.h +++ /dev/null @@ -1,55 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -#ifndef __OPENSPACE_CORE___CONVERTRECFILEVERSIONTASK___H__ -#define __OPENSPACE_CORE___CONVERTRECFILEVERSIONTASK___H__ - -#include -#include - -#include -#include -#include - -namespace openspace::interaction { - -class ConvertRecFileVersionTask : public Task { -public: - ConvertRecFileVersionTask(const ghoul::Dictionary& dictionary); - ~ConvertRecFileVersionTask() override; - std::string description() override; - void perform(const Task::ProgressCallback& progressCallback) override; - static documentation::Documentation documentation(); - void convert(); - SessionRecording* sessRec; - -private: - std::string _inFilename; - std::filesystem::path _inFilePath; - std::string _valueFunctionLua; -}; - -} // namespace openspace::interaction - -#endif // __OPENSPACE_CORE___CONVERTRECFILEVERSIONTASK___H__ diff --git a/include/openspace/interaction/tasks/convertrecformattask.h b/include/openspace/interaction/tasks/convertrecformattask.h index be519c99f2..86d3539ca1 100644 --- a/include/openspace/interaction/tasks/convertrecformattask.h +++ b/include/openspace/interaction/tasks/convertrecformattask.h @@ -26,7 +26,7 @@ #define __OPENSPACE_CORE___CONVERTRECFORMATTASK___H__ #include -#include +#include #include #include @@ -41,25 +41,15 @@ public: ToBinary }; ConvertRecFormatTask(const ghoul::Dictionary& dictionary); - ~ConvertRecFormatTask() override; + ~ConvertRecFormatTask() override = default; std::string description() override; void perform(const Task::ProgressCallback& progressCallback) override; static documentation::Documentation documentation(); - void convert(); private: - void convertToAscii(); - void convertToBinary(); - void determineFormatType(); std::filesystem::path _inFilePath; std::filesystem::path _outFilePath; - std::ifstream _iFile; - std::ofstream _oFile; - SessionRecording::DataMode _fileFormatType; - std::string _version; - - std::string _valueFunctionLua; - SessionRecording* sessRec; + DataMode _dataMode; }; } // namespace openspace::interaction diff --git a/include/openspace/navigation/keyframenavigator.h b/include/openspace/navigation/keyframenavigator.h index 76d1f58554..4e923dcdb5 100644 --- a/include/openspace/navigation/keyframenavigator.h +++ b/include/openspace/navigation/keyframenavigator.h @@ -57,6 +57,7 @@ public: CameraPose() = default; CameraPose(datamessagestructures::CameraKeyframe&& kf); + auto operator<=>(const CameraPose&) const = default; }; /** diff --git a/include/openspace/scene/scene.h b/include/openspace/scene/scene.h index 38d77c931d..0c26ef79bf 100644 --- a/include/openspace/scene/scene.h +++ b/include/openspace/scene/scene.h @@ -241,7 +241,7 @@ public: * \return Vector of Property objs containing property names that matched the regex */ std::vector propertiesMatchingRegex( - const std::string& propertyString); + std::string_view propertyString); /** * Returns a list of all unique tags that are used in the currently loaded scene. diff --git a/include/openspace/scripting/scriptscheduler.h b/include/openspace/scripting/scriptscheduler.h index 77f80e4c26..7e9198a171 100644 --- a/include/openspace/scripting/scriptscheduler.h +++ b/include/openspace/scripting/scriptscheduler.h @@ -120,19 +120,7 @@ public: std::vector allScripts( std::optional group = std::nullopt) const; - /** - * Sets the mode for how each scheduled script's timestamp will be interpreted. - * \param refType reference mode (for exact syntax, see definition of - * interaction::KeyframeTimeRef) which is either relative to the application start - * time, relative to the recorded session playback start time, or according to the - * absolute simulation time in seconds from J2000 epoch. - */ - void setTimeReferenceMode(openspace::interaction::KeyframeTimeRef refType); - static LuaLibrary luaLibrary(); - void setModeApplicationTime(); - void setModeRecordedTime(); - void setModeSimulationTime(); static documentation::Documentation Documentation(); @@ -143,9 +131,6 @@ private: int _currentIndex = 0; double _currentTime = 0; - - openspace::interaction::KeyframeTimeRef _timeframeMode - = openspace::interaction::KeyframeTimeRef::Absolute_simTimeJ2000; }; } // namespace openspace::scripting diff --git a/modules/globebrowsing/src/renderableglobe.cpp b/modules/globebrowsing/src/renderableglobe.cpp index 1d5cd71e9d..9c0693c1f0 100644 --- a/modules/globebrowsing/src/renderableglobe.cpp +++ b/modules/globebrowsing/src/renderableglobe.cpp @@ -33,7 +33,7 @@ #include #include #include -#include +#include #include #include #include @@ -1363,8 +1363,8 @@ void RenderableGlobe::renderChunks(const RenderData& data, RendererTasks&, } _localRenderer.program->deactivate(); - if (global::sessionRecording->isSavingFramesDuringPlayback() && - global::sessionRecording->shouldWaitForTileLoading()) + if (global::sessionRecordingHandler->isSavingFramesDuringPlayback() && + global::sessionRecordingHandler->shouldWaitForTileLoading()) { // If our tile cache is very full, we assume we need to adjust the level of detail // dynamically to not keep rendering frames with unavailable data diff --git a/modules/imgui/imguimodule.cpp b/modules/imgui/imguimodule.cpp index 8031e560cf..4de4e8edcd 100644 --- a/modules/imgui/imguimodule.cpp +++ b/modules/imgui/imguimodule.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -258,7 +258,7 @@ void ImGUIModule::internalInitialize(const ghoul::Dictionary&) { global::screenSpaceRootPropertyOwner, global::moduleEngine, global::navigationHandler, - global::sessionRecording, + global::sessionRecordingHandler, global::timeManager, global::renderEngine, global::parallelPeer, diff --git a/modules/server/include/topics/sessionrecordingtopic.h b/modules/server/include/topics/sessionrecordingtopic.h index 7e7d7ce41c..48fc4b710b 100644 --- a/modules/server/include/topics/sessionrecordingtopic.h +++ b/modules/server/include/topics/sessionrecordingtopic.h @@ -27,7 +27,7 @@ #include -#include +#include namespace openspace { @@ -48,8 +48,8 @@ private: // Provides the idle/recording/playback state int value in json message void sendJsonData(); - interaction::SessionRecording::SessionState _lastState = - interaction::SessionRecording::SessionState::Idle; + interaction::SessionRecordingHandler::SessionState _lastState = + interaction::SessionRecordingHandler::SessionState::Idle; int _stateCallbackHandle = UnsetOnChangeHandle; bool _isDone = false; }; diff --git a/modules/server/src/topics/sessionrecordingtopic.cpp b/modules/server/src/topics/sessionrecordingtopic.cpp index 5b785a3e48..bf2660da91 100644 --- a/modules/server/src/topics/sessionrecordingtopic.cpp +++ b/modules/server/src/topics/sessionrecordingtopic.cpp @@ -51,7 +51,7 @@ SessionRecordingTopic::SessionRecordingTopic() { SessionRecordingTopic::~SessionRecordingTopic() { if (_stateCallbackHandle != UnsetOnChangeHandle) { - global::sessionRecording->removeStateChangeCallback(_stateCallbackHandle); + global::sessionRecordingHandler->removeStateChangeCallback(_stateCallbackHandle); } } @@ -99,10 +99,10 @@ void SessionRecordingTopic::handleJson(const nlohmann::json& json) { sendJsonData(); if (event == SubscribeEvent && _sendState) { - _stateCallbackHandle = global::sessionRecording->addStateChangeCallback( + _stateCallbackHandle = global::sessionRecordingHandler->addStateChangeCallback( [this]() { - const interaction::SessionRecording::SessionState currentState = - global::sessionRecording->state(); + const interaction::SessionRecordingHandler::SessionState currentState = + global::sessionRecordingHandler->state(); if (currentState != _lastState) { sendJsonData(); _lastState = currentState; @@ -114,18 +114,17 @@ void SessionRecordingTopic::handleJson(const nlohmann::json& json) { void SessionRecordingTopic::sendJsonData() { json stateJson; - using SessionRecording = interaction::SessionRecording; + using SessionRecordingHandler = interaction::SessionRecordingHandler; if (_sendState) { - const SessionRecording::SessionState state = global::sessionRecording->state(); std::string stateString; - switch (state) { - case SessionRecording::SessionState::Recording: + switch (global::sessionRecordingHandler->state()) { + case SessionRecordingHandler::SessionState::Recording: stateString = "recording"; break; - case SessionRecording::SessionState::Playback: + case SessionRecordingHandler::SessionState::Playback: stateString = "playing"; break; - case SessionRecording::SessionState::PlaybackPaused: + case SessionRecordingHandler::SessionState::PlaybackPaused: stateString = "playing-paused"; break; default: @@ -135,7 +134,7 @@ void SessionRecordingTopic::sendJsonData() { stateJson[StateKey] = stateString; }; if (_sendFiles) { - stateJson[FilesKey] = global::sessionRecording->playbackList(); + stateJson[FilesKey] = global::sessionRecordingHandler->playbackList(); } if (!stateJson.empty()) { _connection->sendJson(wrappedPayload(stateJson)); diff --git a/modules/video/src/videoplayer.cpp b/modules/video/src/videoplayer.cpp index 389fef2cfd..ddc0708e9d 100644 --- a/modules/video/src/videoplayer.cpp +++ b/modules/video/src/videoplayer.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -495,8 +495,8 @@ void VideoPlayer::update() { return; } - if (global::sessionRecording->isSavingFramesDuringPlayback()) { - const double dt = global::sessionRecording->fixedDeltaTimeDuringFrameOutput(); + if (global::sessionRecordingHandler->isSavingFramesDuringPlayback()) { + const double dt = global::sessionRecordingHandler->fixedDeltaTimeDuringFrameOutput(); if (_playbackMode == PlaybackMode::MapToSimulationTime) { _currentVideoTime = correctVideoPlaybackTime(); } diff --git a/modules/volume/linearlrucache.inl b/modules/volume/linearlrucache.inl index 39fac068af..302a5b8cf0 100644 --- a/modules/volume/linearlrucache.inl +++ b/modules/volume/linearlrucache.inl @@ -51,7 +51,7 @@ void LinearLruCache::set(size_t key, ValueType value) { template ValueType& LinearLruCache::use(size_t key) { - auto& pair = _cache[key]; + const auto& pair = _cache[key]; const std::list::iterator trackerIter = pair.second; _tracker.splice(_tracker.end(), _tracker, trackerIter); return pair.first; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3cf7b141b6..0c6468a574 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,16 +58,16 @@ set(OPENSPACE_SOURCE interaction/joystickcamerastates.cpp interaction/keybindingmanager.cpp interaction/keybindingmanager_lua.inl - interaction/keyframerecording.cpp - interaction/keyframerecording_lua.inl + interaction/keyframerecordinghandler.cpp + interaction/keyframerecordinghandler_lua.inl interaction/keyboardinputstate.cpp interaction/mousecamerastates.cpp interaction/scriptcamerastates.cpp interaction/sessionrecording.cpp - interaction/sessionrecording_lua.inl + interaction/sessionrecordinghandler.cpp + interaction/sessionrecordinghandler_lua.inl interaction/websocketinputstate.cpp interaction/websocketcamerastates.cpp - interaction/tasks/convertrecfileversiontask.cpp interaction/tasks/convertrecformattask.cpp mission/mission.cpp mission/missionmanager.cpp @@ -252,14 +252,13 @@ set(OPENSPACE_HEADER ${PROJECT_SOURCE_DIR}/include/openspace/interaction/joystickcamerastates.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/keybindingmanager.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/keyboardinputstate.h - ${PROJECT_SOURCE_DIR}/include/openspace/interaction/keyframerecording.h + ${PROJECT_SOURCE_DIR}/include/openspace/interaction/keyframerecordinghandler.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/mousecamerastates.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/scriptcamerastates.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/sessionrecording.h - ${PROJECT_SOURCE_DIR}/include/openspace/interaction/sessionrecording.inl + ${PROJECT_SOURCE_DIR}/include/openspace/interaction/sessionrecordinghandler.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/websocketinputstate.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/websocketcamerastates.h - ${PROJECT_SOURCE_DIR}/include/openspace/interaction/tasks/convertrecfileversiontask.h ${PROJECT_SOURCE_DIR}/include/openspace/interaction/tasks/convertrecformattask.h ${PROJECT_SOURCE_DIR}/include/openspace/mission/mission.h ${PROJECT_SOURCE_DIR}/include/openspace/mission/missionmanager.h diff --git a/src/documentation/core_registration.cpp b/src/documentation/core_registration.cpp index ff209ccca3..5673548aae 100644 --- a/src/documentation/core_registration.cpp +++ b/src/documentation/core_registration.cpp @@ -32,8 +32,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -107,11 +107,11 @@ void registerCoreClasses(scripting::ScriptEngine& engine) { engine.addLibrary(Time::luaLibrary()); engine.addLibrary(interaction::ActionManager::luaLibrary()); engine.addLibrary(interaction::KeybindingManager::luaLibrary()); - engine.addLibrary(interaction::KeyframeRecording::luaLibrary()); + engine.addLibrary(interaction::KeyframeRecordingHandler::luaLibrary()); engine.addLibrary(interaction::NavigationHandler::luaLibrary()); engine.addLibrary(interaction::OrbitalNavigator::luaLibrary()); engine.addLibrary(interaction::PathNavigator::luaLibrary()); - engine.addLibrary(interaction::SessionRecording::luaLibrary()); + engine.addLibrary(interaction::SessionRecordingHandler::luaLibrary()); engine.addLibrary(scripting::ScriptScheduler::luaLibrary()); engine.addLibrary(scripting::generalSystemCapabilities()); engine.addLibrary(scripting::openglSystemCapabilities()); diff --git a/src/engine/globals.cpp b/src/engine/globals.cpp index d9be116dc5..97745bdf94 100644 --- a/src/engine/globals.cpp +++ b/src/engine/globals.cpp @@ -35,10 +35,10 @@ #include #include #include -#include +#include #include #include -#include +#include #include #include #include @@ -96,9 +96,9 @@ namespace { sizeof(interaction::JoystickInputStates) + sizeof(interaction::WebsocketInputStates) + sizeof(interaction::KeybindingManager) + - sizeof(interaction::KeyframeRecording) + + sizeof(interaction::KeyframeRecordingHandler) + sizeof(interaction::NavigationHandler) + - sizeof(interaction::SessionRecording) + + sizeof(interaction::SessionRecordingHandler) + sizeof(properties::PropertyOwner) + sizeof(properties::PropertyOwner) + sizeof(properties::PropertyOwner) + @@ -317,11 +317,11 @@ void create() { #endif // WIN32 #ifdef WIN32 - keyframeRecording = new (currentPos) interaction::KeyframeRecording; + keyframeRecording = new (currentPos) interaction::KeyframeRecordingHandler; ghoul_assert(keyframeRecording, "No keyframeRecording"); - currentPos += sizeof(interaction::KeyframeRecording); + currentPos += sizeof(interaction::KeyframeRecordingHandler); #else // ^^^ WIN32 / !WIN32 vvv - keyframeRecording = new interaction::KeyframeRecording; + keyframeRecording = new interaction::KeyframeRecordingHandler; #endif // WIN32 #ifdef WIN32 @@ -333,11 +333,11 @@ void create() { #endif // WIN32 #ifdef WIN32 - sessionRecording = new (currentPos) interaction::SessionRecording(true); - ghoul_assert(sessionRecording, "No sessionRecording"); - currentPos += sizeof(interaction::SessionRecording); + sessionRecordingHandler = new (currentPos) interaction::SessionRecordingHandler; + ghoul_assert(sessionRecordingHandler, "No sessionRecording"); + currentPos += sizeof(interaction::SessionRecordingHandler); #else // ^^^ WIN32 / !WIN32 vvv - sessionRecording = new interaction::SessionRecording(true); + sessionRecordingHandler = new interaction::SessionRecordingHandler; #endif // WIN32 #ifdef WIN32 @@ -399,7 +399,7 @@ void initialize() { rootPropertyOwner->addPropertySubOwner(global::navigationHandler); rootPropertyOwner->addPropertySubOwner(global::keyframeRecording); rootPropertyOwner->addPropertySubOwner(global::interactionMonitor); - rootPropertyOwner->addPropertySubOwner(global::sessionRecording); + rootPropertyOwner->addPropertySubOwner(global::sessionRecordingHandler); rootPropertyOwner->addPropertySubOwner(global::timeManager); rootPropertyOwner->addPropertySubOwner(global::scriptScheduler); @@ -456,11 +456,11 @@ void destroy() { delete rootPropertyOwner; #endif // WIN32 - LDEBUGC("Globals", "Destroying 'SessionRecording'"); + LDEBUGC("Globals", "Destroying 'SessionRecordingHandler'"); #ifdef WIN32 - sessionRecording->~SessionRecording(); + sessionRecordingHandler->~SessionRecordingHandler(); #else // ^^^ WIN32 / !WIN32 vvv - delete sessionRecording; + delete sessionRecordingHandler; #endif // WIN32 LDEBUGC("Globals", "Destroying 'NavigationHandler'"); @@ -470,9 +470,9 @@ void destroy() { delete navigationHandler; #endif // WIN32 - LDEBUGC("Globals", "Destroying 'KeyframeRecording'"); + LDEBUGC("Globals", "Destroying 'KeyframeRecordingHandler'"); #ifdef WIN32 - keyframeRecording->~KeyframeRecording(); + keyframeRecording->~KeyframeRecordingHandler(); #else // ^^^ WIN32 / !WIN32 vvv delete keyframeRecording; #endif // WIN32 diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index ac7d32ca34..aaff37332b 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -40,8 +40,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -63,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -231,6 +232,11 @@ OpenSpaceEngine::OpenSpaceEngine() addProperty(_fadeOnEnableDuration); addProperty(_disableAllMouseInputs); + + ghoul::TemplateFactory* fTask = FactoryManager::ref().factory(); + ghoul_assert(fTask, "No task factory existed"); + fTask->registerClass("ConvertRecFormatTask"); + #ifdef WIN32 PDH_STATUS status = PdhOpenQueryA(nullptr, 0, &vramQuery); if (status != ERROR_SUCCESS) { @@ -875,7 +881,6 @@ void OpenSpaceEngine::deinitialize() { global::renderEngine->scene()->camera()->syncables() ); } - global::sessionRecording->deinitialize(); global::versionChecker->cancel(); _assetManager = nullptr; @@ -1083,8 +1088,8 @@ void OpenSpaceEngine::preSynchronization() { global::syncEngine->preSynchronization(SyncEngine::IsMaster(master)); if (master) { const double dt = - global::sessionRecording->isSavingFramesDuringPlayback() ? - global::sessionRecording->fixedDeltaTimeDuringFrameOutput() : + global::sessionRecordingHandler->isSavingFramesDuringPlayback() ? + global::sessionRecordingHandler->fixedDeltaTimeDuringFrameOutput() : global::windowDelegate->deltaTime(); global::timeManager->preSynchronization(dt); @@ -1113,8 +1118,7 @@ void OpenSpaceEngine::preSynchronization() { camera->invalidateCache(); } } - global::sessionRecording->preSynchronization(); - global::keyframeRecording->preSynchronization(dt); + global::sessionRecordingHandler->preSynchronization(dt); global::parallelPeer->preSynchronization(); global::interactionMonitor->updateActivityState(); } @@ -1263,7 +1267,7 @@ void OpenSpaceEngine::drawOverlays() { if (isGuiWindow) { global::renderEngine->renderOverlays(_shutdown); global::luaConsole->render(); - global::sessionRecording->render(); + global::sessionRecordingHandler->render(); } for (const std::function& func : *global::callback::draw2D) { diff --git a/src/interaction/keyframerecording.cpp b/src/interaction/keyframerecording.cpp deleted file mode 100644 index 88d33e49d0..0000000000 --- a/src/interaction/keyframerecording.cpp +++ /dev/null @@ -1,430 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "keyframerecording_lua.inl" - -namespace { - constexpr std::string_view _loggerCat = "KeyframeRecording"; -} // namespace - -namespace openspace::interaction::keys { - // These keys are const char* since nlohmann::json do not support string_view in .at() - constexpr const char* Camera = "camera"; - constexpr const char* Position = "position"; - constexpr const char* Rotation = "rotation"; - constexpr const char* Scale = "scale"; - constexpr const char* FollowFocusNodeRotation = "followFocusNodeRotation"; - constexpr const char* FocusNode = "focusNode"; - - constexpr const char* Timestamp = "timestamp"; - constexpr const char* Application = "application"; - constexpr const char* Sequence = "sequence"; - constexpr const char* Simulation = "simulation"; - constexpr const char* X = "x"; - constexpr const char* Y = "y"; - constexpr const char* Z = "z"; - constexpr const char* W = "w"; -} - -namespace openspace::interaction { - -void to_json(nlohmann::json& j, const KeyframeRecording::Keyframe& keyframe) { - nlohmann::json position = { - { keys::X, keyframe.camera.position.x }, - { keys::Y, keyframe.camera.position.y }, - { keys::Z, keyframe.camera.position.z }, - }; - nlohmann::json rotation = { - { keys::X, keyframe.camera.rotation.x }, - { keys::Y, keyframe.camera.rotation.y }, - { keys::Z, keyframe.camera.rotation.z }, - { keys::W, keyframe.camera.rotation.w }, - }; - nlohmann::json camera = { - { keys::Position, position }, - { keys::Rotation, rotation }, - { keys::Scale, keyframe.camera.scale }, - { keys::FollowFocusNodeRotation, keyframe.camera.followFocusNodeRotation }, - { keys::FocusNode, keyframe.camera.focusNode } - }; - - nlohmann::json timestamp = { - { keys::Application, keyframe.timestamp.application }, - { keys::Sequence, keyframe.timestamp.sequenceTime }, - { keys::Simulation, keyframe.timestamp.simulation } - }; - - j = { - { keys::Camera, camera }, - { keys::Timestamp, timestamp } - }; -} - -void from_json(const nlohmann::json& j, KeyframeRecording::Keyframe::TimeStamp& ts) { - j.at(keys::Application).get_to(ts.application); - j.at(keys::Sequence).get_to(ts.sequenceTime); - j.at(keys::Simulation).get_to(ts.simulation); -} - -void from_json(const nlohmann::json& j, KeyframeNavigator::CameraPose& pose) { - j.at(keys::Position).at(keys::X).get_to(pose.position.x); - j.at(keys::Position).at(keys::Y).get_to(pose.position.y); - j.at(keys::Position).at(keys::Z).get_to(pose.position.z); - - j.at(keys::Rotation).at(keys::X).get_to(pose.rotation.x); - j.at(keys::Rotation).at(keys::Y).get_to(pose.rotation.y); - j.at(keys::Rotation).at(keys::Z).get_to(pose.rotation.z); - j.at(keys::Rotation).at(keys::W).get_to(pose.rotation.w); - - j.at(keys::FocusNode).get_to(pose.focusNode); - j.at(keys::Scale).get_to(pose.scale); - j.at(keys::FollowFocusNodeRotation).get_to(pose.followFocusNodeRotation); -} - -void from_json(const nlohmann::json& j, KeyframeRecording::Keyframe& keyframe) { - j.at(keys::Camera).get_to(keyframe.camera); - j.at(keys::Timestamp).get_to(keyframe.timestamp); -} - -KeyframeRecording::KeyframeRecording() - : properties::PropertyOwner({ "KeyframeRecording", "Keyframe Recording" }) -{} - -void KeyframeRecording::newSequence() { - _keyframes.clear(); - _filename.clear(); - LINFO("Created new sequence"); -} - -void KeyframeRecording::addKeyframe(double sequenceTime) { - ghoul_assert(sequenceTime >= 0, "Sequence time must be positive"); - - Keyframe keyframe = newKeyframe(sequenceTime); - - auto it = std::find_if( - _keyframes.begin(), - _keyframes.end(), - [&sequenceTime](const Keyframe& entry) { - return sequenceTime < entry.timestamp.sequenceTime; - } - ); - _keyframes.insert(it, keyframe); - LINFO(std::format( - "Added new keyframe {} at time: {}", - _keyframes.size() - 1 , sequenceTime - )); -} - -void KeyframeRecording::removeKeyframe(int index) { - ghoul_assert(hasKeyframeRecording(), "Can't remove keyframe on empty sequence"); - - if (!isInRange(index)) { - LERROR(std::format("Index {} out of range", index)); - return; - } - _keyframes.erase(_keyframes.begin() + index); - LINFO(std::format("Removed keyframe with index {}", index)); -} - -void KeyframeRecording::updateKeyframe(int index) { - ghoul_assert(hasKeyframeRecording(), "Can't update keyframe on empty sequence"); - - if (!isInRange(index)) { - LERROR(std::format("Index {} out of range", index)); - return; - } - Keyframe old = _keyframes[index]; - _keyframes[index] = newKeyframe(old.timestamp.sequenceTime); - LINFO(std::format("Update camera position of keyframe {}", index)); -} - -void KeyframeRecording::moveKeyframe(int index, double sequenceTime) { - ghoul_assert(hasKeyframeRecording(), "can't move keyframe on empty sequence"); - ghoul_assert(sequenceTime >= 0, "Sequence time must be positive"); - - if (!isInRange(index)) { - LERROR(std::format("Index {} out of range", index)); - return; - } - double oldSequenceTime = _keyframes[index].timestamp.sequenceTime; - _keyframes[index].timestamp.sequenceTime = sequenceTime; - sortKeyframes(); - LINFO(std::format( - "Moved keyframe {} from sequence time: {} to {}", - index, oldSequenceTime, sequenceTime - )); -} - -bool KeyframeRecording::saveSequence(std::optional filename) { - ghoul_assert(hasKeyframeRecording(), "Keyframe sequence can't be empty"); - - // If we didn't specify any filename we save the one we currently have stored - if (filename.has_value()) { - _filename = filename.value(); - } - - if (_filename.empty()) { - LERROR("Failed to save file, reason: Invalid empty file name"); - return false; - } - - nlohmann::json sequence = _keyframes; - std::filesystem::path path = absPath( - std::format("${{RECORDINGS}}/{}.json", _filename) - ); - std::ofstream ofs(path); - ofs << sequence.dump(2); - LINFO(std::format("Saved keyframe sequence to '{}'", path.string())); - return true; -} - -void KeyframeRecording::loadSequence(std::string filename) { - std::filesystem::path path = absPath( - std::format("${{RECORDINGS}}/{}.json", filename) - ); - if (!std::filesystem::exists(path)) { - LERROR(std::format("File '{}' does not exist", path)); - return; - } - - LINFO(std::format("Loading keyframe sequence from '{}'", path)); - _keyframes.clear(); - std::ifstream file(path); - std::vector jsonKeyframes = - nlohmann::json::parse(file).get>(); - - for (const nlohmann::json& keyframeJson : jsonKeyframes) { - Keyframe keyframe = keyframeJson; - _keyframes.push_back(keyframe); - } - _filename = filename; -} - -void KeyframeRecording::play() { - ghoul_assert(hasKeyframeRecording(), "Keyframe sequence can't be empty"); - - LINFO("Keyframe sequence playing"); - _isPlaying = true; -} - -void KeyframeRecording::pause() { - LINFO("Keyframe sequence paused"); - _isPlaying = false; -} - -void KeyframeRecording::setSequenceTime(double sequenceTime) { - ghoul_assert(sequenceTime >= 0, "Sequence time must be positive"); - - _sequenceTime = sequenceTime; - _hasStateChanged = true; - LINFO(std::format("Set sequence time to {}", sequenceTime)); -} - -void KeyframeRecording::jumpToKeyframe(int index) { - if (!isInRange(index)) { - LERROR(std::format("Index {} out of range", index)); - return; - } - const double time = _keyframes[index].timestamp.sequenceTime; - LINFO(std::format("Jumped to keyframe {}", index)); - setSequenceTime(time); -} - -bool KeyframeRecording::hasKeyframeRecording() const { - return !_keyframes.empty(); -} - -std::vector KeyframeRecording::keyframes() const { - std::vector result; - for (const auto& keyframe : _keyframes) { - ghoul::Dictionary camera; - ghoul::Dictionary timestamp; - ghoul::Dictionary entry; - ghoul::Dictionary position; - ghoul::Dictionary rotation; - - // Add each entry to position & rotation to avoid ambiguity on the client side - position.setValue(keys::X, keyframe.camera.position.x); - position.setValue(keys::Y, keyframe.camera.position.y); - position.setValue(keys::Z, keyframe.camera.position.z); - camera.setValue(keys::Position, position); - - rotation.setValue(keys::X, static_cast(keyframe.camera.rotation.x)); - rotation.setValue(keys::Y, static_cast(keyframe.camera.rotation.y)); - rotation.setValue(keys::Z, static_cast(keyframe.camera.rotation.z)); - rotation.setValue(keys::W, static_cast(keyframe.camera.rotation.w)); - camera.setValue(keys::Rotation, rotation); - - camera.setValue(keys::Scale, static_cast(keyframe.camera.scale)); - camera.setValue(keys::FocusNode, keyframe.camera.focusNode); - camera.setValue(keys::FollowFocusNodeRotation, keyframe.camera.followFocusNodeRotation); - - timestamp.setValue(keys::Application, keyframe.timestamp.application); - timestamp.setValue(keys::Sequence, keyframe.timestamp.sequenceTime); - timestamp.setValue(keys::Simulation, keyframe.timestamp.simulation); - - entry.setValue(keys::Camera, camera); - entry.setValue(keys::Timestamp, timestamp); - result.push_back(entry); - } - return result; -} - -void KeyframeRecording::preSynchronization(double dt) { - if (_hasStateChanged) { - auto it = std::find_if( - _keyframes.rbegin(), - _keyframes.rend(), - [timestamp = _sequenceTime](const Keyframe& entry) { - return timestamp >= entry.timestamp.sequenceTime; - } - ); - - Keyframe currKeyframe; - Keyframe nextKeyframe; - double factor = 0.0; - - // Before first keyframe - if (it == _keyframes.rend()) { - currKeyframe = nextKeyframe = _keyframes.front(); - } - // At or after last keyframe - else if (it == _keyframes.rbegin()) { - currKeyframe = nextKeyframe = _keyframes.back(); - _isPlaying = false; - } - else { - currKeyframe = *it; - nextKeyframe = *(--it); - double t0 = currKeyframe.timestamp.sequenceTime; - double t1 = nextKeyframe.timestamp.sequenceTime; - factor = (_sequenceTime - t0) / (t1 - t0); - } - - interaction::KeyframeNavigator::CameraPose curr = currKeyframe.camera; - interaction::KeyframeNavigator::CameraPose next = nextKeyframe.camera; - - Camera* camera = global::navigationHandler->camera(); - Scene* scene = camera->parent()->scene(); - SceneGraphNode* node = scene->sceneGraphNode(curr.focusNode); - - global::navigationHandler->orbitalNavigator().setFocusNode(node); - interaction::KeyframeNavigator::updateCamera( - global::navigationHandler->camera(), - curr, - next, - factor, - false - ); - - _hasStateChanged = false; - } - - if (_isPlaying) { - _sequenceTime += dt; - _hasStateChanged = true; - } -} - -scripting::LuaLibrary KeyframeRecording::luaLibrary() { - return { - "keyframeRecording", - { - codegen::lua::NewSequence, - codegen::lua::AddKeyframe, - codegen::lua::RemoveKeyframe, - codegen::lua::UpdateKeyframe, - codegen::lua::MoveKeyframe, - codegen::lua::SaveSequence, - codegen::lua::LoadSequence, - codegen::lua::Play, - codegen::lua::Pause, - codegen::lua::Resume, - codegen::lua::SetTime, - codegen::lua::JumpToKeyframe, - codegen::lua::HasKeyframeRecording, - codegen::lua::Keyframes - } - }; -} - -void KeyframeRecording::sortKeyframes() { - std::sort( - _keyframes.begin(), - _keyframes.end(), - [](Keyframe lhs, Keyframe rhs) { - return lhs.timestamp.sequenceTime < rhs.timestamp.sequenceTime; - } - ); -} - -KeyframeRecording::Keyframe KeyframeRecording::newKeyframe(double sequenceTime) { - interaction::NavigationHandler& handler = *global::navigationHandler; - interaction::OrbitalNavigator& navigator = handler.orbitalNavigator(); - const SceneGraphNode* node = navigator.anchorNode(); - - glm::dvec3 position = navigator.anchorNodeToCameraVector(); - glm::dquat rotation = handler.camera()->rotationQuaternion(); - float scale = handler.camera()->scaling(); - bool followNodeRotation = navigator.followingAnchorRotation(); - - if (followNodeRotation) { - position = glm::inverse(node->worldRotationMatrix()) * position; - rotation = navigator.anchorNodeToCameraRotation(); - } - - Keyframe keyframe; - keyframe.camera.position = position; - keyframe.camera.rotation = rotation; - keyframe.camera.focusNode = navigator.anchorNode()->identifier(); - keyframe.camera.scale = scale; - keyframe.camera.followFocusNodeRotation = followNodeRotation; - - keyframe.timestamp.application = global::windowDelegate->applicationTime(); - keyframe.timestamp.sequenceTime = sequenceTime; - keyframe.timestamp.simulation = global::timeManager->time().j2000Seconds(); - - return keyframe; -} - -bool KeyframeRecording::isInRange(int index) const { - return index >= 0 && index < _keyframes.size(); -} - -} // namespace openspace::interaction diff --git a/src/interaction/keyframerecording_lua.inl b/src/interaction/keyframerecording_lua.inl deleted file mode 100644 index 94c8e37378..0000000000 --- a/src/interaction/keyframerecording_lua.inl +++ /dev/null @@ -1,190 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -#include -#include - -namespace { - -/** - * Starts a new sequence of keyframes. Any previously loaded sequence is discarded. - */ -[[codegen::luawrap]] void newSequence() { - openspace::global::keyframeRecording->newSequence(); -} - -/** - * Adds a keyframe at the specified time in the sequence. - * - * \param sequenceTime The time at which to add the new keyframe in the sequence given in - * seconds - */ -[[codegen::luawrap]] void addKeyframe(double sequenceTime) { - if (sequenceTime < 0) { - throw ghoul::lua::LuaError("Error can't add keyframe with negative time value"); - } - openspace::global::keyframeRecording->addKeyframe(sequenceTime); -} - -/** - * Removes a keyframe at the specified index. - * - * \param index The 0-based index of the keyframe to remove - */ -[[codegen::luawrap]] void removeKeyframe(int index) { - if (!openspace::global::keyframeRecording->hasKeyframeRecording()) { - throw ghoul::lua::LuaError("Can't remove keyframe on empty sequence"); - } - if (index < 0) { - throw ghoul::lua::LuaError("Index value must be positive"); - } - openspace::global::keyframeRecording->removeKeyframe(index); -} - -/** - * Update the camera position of a keyframe at the specified index. - * - * \param index The 0-based index of the keyframe to update - */ -[[codegen::luawrap]] void updateKeyframe(int index) { - if (!openspace::global::keyframeRecording->hasKeyframeRecording()) { - throw ghoul::lua::LuaError("Can't update keyframe on empty sequence"); - } - if (index < 0) { - throw ghoul::lua::LuaError("Index value must be positive"); - } - openspace::global::keyframeRecording->updateKeyframe(index); -} - -/** - * Move an existing keyframe in time. - * - * \param index The index of the keyframe to move - * \param sequenceTime The new time in seconds to update the keyframe to - */ -[[codegen::luawrap]] void moveKeyframe(int index, double sequenceTime) { - if (!openspace::global::keyframeRecording->hasKeyframeRecording()) { - throw ghoul::lua::LuaError("Can't move keyframe on empty sequence"); - } - if (index < 0) { - throw ghoul::lua::LuaError("Index value must be positive"); - } - if (sequenceTime < 0) { - throw ghoul::lua::LuaError("Error can't add keyframe with negative time value"); - } - openspace::global::keyframeRecording->moveKeyframe(index, sequenceTime); -} - -/** - * Saves the current sequence of keyframes to disk by the optionally specified `filename`. - * `filename` can be omitted if the sequence was previously saved or loaded from file. - * - * \param filename The name of the file to save - */ -[[codegen::luawrap]] void saveSequence(std::optional filename) { - if (!openspace::global::keyframeRecording->hasKeyframeRecording()) { - throw ghoul::lua::LuaError("No keyframe sequence to save"); - } - openspace::global::keyframeRecording->saveSequence(filename); -} - -/** - * Loads a keyframe recording sequence from the specified file. - * - * \param filename The name of the file to load - */ -[[codegen::luawrap]] void loadSequence(std::string filename) { - openspace::global::keyframeRecording->loadSequence(std::move(filename)); -} - -/** - * Playback keyframe recording sequence optionally from the specified `sequenceTime` or if - * not specified starts playing from the beginning. - * - * \param sequenceTime The time in seconds at which to start playing the sequence. If - * omitted, the playback starts at the beginning of the sequence. - */ -[[codegen::luawrap]] void play(std::optional sequenceTime) { - if (!openspace::global::keyframeRecording->hasKeyframeRecording()) { - throw ghoul::lua::LuaError("No keyframe sequence to play"); - } - openspace::global::keyframeRecording->setSequenceTime(sequenceTime.value_or(0.0)); - openspace::global::keyframeRecording->play(); -} - -/** - * Pauses a playing keyframe recording sequence. - */ -[[codegen::luawrap]] void pause() { - openspace::global::keyframeRecording->pause(); -} - -/** - * Resume playing a keyframe recording sequence that has been paused. - */ -[[codegen::luawrap]] void resume() { - openspace::global::keyframeRecording->play(); -} - -/** - * Jumps to a specified time within the keyframe recording sequence. - * - * \param sequenceTime The time in seconds to jump to - */ -[[codegen::luawrap]] void setTime(double sequenceTime) { - if (sequenceTime < 0) { - throw ghoul::lua::LuaError("Sequence time must be greater or equal than 0"); - } - openspace::global::keyframeRecording->setSequenceTime(sequenceTime); -} - -/** - * Jumps to a specified keyframe within the keyframe recording sequence. - * - * \param index The index of the keyframe to jump to - */ -[[codegen::luawrap]] void jumpToKeyframe(int index) { - if (index < 0) { - throw ghoul::lua::LuaError("Index must be positive"); - } - openspace::global::keyframeRecording->jumpToKeyframe(index); -} - -/** - * Returns true if there currently is a sequence loaded, otherwise false. - */ -[[codegen::luawrap]] bool hasKeyframeRecording() { - return openspace::global::keyframeRecording->hasKeyframeRecording(); -} - -/** - * Fetches the sequence keyframes as a JSON object. - */ -[[codegen::luawrap]] std::vector keyframes() { - return openspace::global::keyframeRecording->keyframes(); -} - -#include "keyframerecording_lua_codegen.cpp" - -} // namespace diff --git a/src/interaction/keyframerecordinghandler.cpp b/src/interaction/keyframerecordinghandler.cpp new file mode 100644 index 0000000000..5ba3eff36b --- /dev/null +++ b/src/interaction/keyframerecordinghandler.cpp @@ -0,0 +1,166 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include + +#include "keyframerecordinghandler_lua.inl" + +namespace { + constexpr std::string_view _loggerCat = "KeyframeRecording"; +} // namespace + +namespace openspace::interaction { + +KeyframeRecordingHandler::KeyframeRecordingHandler() + : properties::PropertyOwner({ "KeyframeRecording", "Keyframe Recording" }) +{} + +void KeyframeRecordingHandler::newSequence() { + _timeline = SessionRecording(); +} + +void KeyframeRecordingHandler::addCameraKeyframe(double sequenceTime) { + using namespace datamessagestructures; + CameraKeyframe kf = datamessagestructures::generateCameraKeyframe(); + + SessionRecording::Entry entry = { + sequenceTime, + global::timeManager->time().j2000Seconds(), + KeyframeNavigator::CameraPose(std::move(kf)) + }; + + auto it = std::upper_bound( + _timeline.entries.begin(), + _timeline.entries.end(), + sequenceTime, + [](double value, const SessionRecording::Entry& e) { + return value < e.timestamp; + } + ); + _timeline.entries.insert(it, std::move(entry)); +} + +void KeyframeRecordingHandler::addScriptKeyframe(double sequenceTime, std::string script) +{ + SessionRecording::Entry entry = { + sequenceTime, + global::timeManager->time().j2000Seconds(), + std::move(script) + }; + + auto it = std::upper_bound( + _timeline.entries.begin(), + _timeline.entries.end(), + sequenceTime, + [](double value, const SessionRecording::Entry& e) { + return value < e.timestamp; + } + ); + _timeline.entries.insert(it, std::move(entry)); +} + +void KeyframeRecordingHandler::removeKeyframe(int index) { + if (index < 0 || static_cast(index) >(_timeline.entries.size() - 1)) { + throw ghoul::RuntimeError(std::format("Index {} out of range", index)); + } + _timeline.entries.erase(_timeline.entries.begin() + index); +} + +void KeyframeRecordingHandler::updateKeyframe(int index) { + using namespace datamessagestructures; + if (index < 0 || static_cast(index) > (_timeline.entries.size() - 1)) { + throw ghoul::RuntimeError(std::format("Index {} out of range", index)); + } + + SessionRecording::Entry& entry = _timeline.entries[index]; + if (!std::holds_alternative(entry.value)) { + throw ghoul::RuntimeError(std::format("Index {} is not a camera frame", index)); + } + auto& camera = std::get(entry.value); + camera = KeyframeNavigator::CameraPose(generateCameraKeyframe()); +} + +void KeyframeRecordingHandler::moveKeyframe(int index, double sequenceTime) { + if (index < 0 || static_cast(index) >(_timeline.entries.size() - 1)) { + throw ghoul::RuntimeError(std::format("Index {} out of range", index)); + } + + _timeline.entries[index].timestamp = sequenceTime; + std::sort( + _timeline.entries.begin(), + _timeline.entries.end(), + [](const SessionRecording::Entry& lhs, const SessionRecording::Entry& rhs) { + return lhs.timestamp < rhs.timestamp; + } + ); +} + +void KeyframeRecordingHandler::saveSequence(std::filesystem::path filename) { + if (filename.empty()) { + throw ghoul::RuntimeError("Failed to save file, reason: Invalid empty file name"); + } + + saveSessionRecording(filename, _timeline, DataMode::Ascii); +} + +void KeyframeRecordingHandler::loadSequence(std::filesystem::path filename) { + _timeline = loadSessionRecording(filename); +} + +void KeyframeRecordingHandler::play() { + global::sessionRecordingHandler->startPlayback(_timeline, false, false, std::nullopt); +} + +bool KeyframeRecordingHandler::hasKeyframeRecording() const { + return !_timeline.entries.empty(); +} + +std::vector KeyframeRecordingHandler::keyframes() const { + return sessionRecordingToDictionary(_timeline); +} + +scripting::LuaLibrary KeyframeRecordingHandler::luaLibrary() { + return { + "keyframeRecording", + { + codegen::lua::NewSequence, + codegen::lua::AddCameraKeyframe, + codegen::lua::AddScriptKeyframe, + codegen::lua::RemoveKeyframe, + codegen::lua::UpdateKeyframe, + codegen::lua::MoveKeyframe, + codegen::lua::SaveSequence, + codegen::lua::LoadSequence, + codegen::lua::Play, + codegen::lua::Pause, + codegen::lua::Keyframes + } + }; +} + +} // namespace openspace::interaction diff --git a/src/interaction/keyframerecordinghandler_lua.inl b/src/interaction/keyframerecordinghandler_lua.inl new file mode 100644 index 0000000000..a2d3ad600f --- /dev/null +++ b/src/interaction/keyframerecordinghandler_lua.inl @@ -0,0 +1,108 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include +#include + +namespace { + +// Starts a new sequence of keyframes, any previously loaded sequence is discarded +[[codegen::luawrap]] void newSequence() { + using namespace openspace; + global::keyframeRecording->newSequence(); +} + +// Adds a keyframe at the specified sequence-time +[[codegen::luawrap]] void addCameraKeyframe(double sequenceTime) { + using namespace openspace; + global::keyframeRecording->addCameraKeyframe(sequenceTime); +} + +// Adds a keyframe at the specified sequence-time +[[codegen::luawrap]] void addScriptKeyframe(double sequenceTime, std::string script) { + using namespace openspace; + global::keyframeRecording->addScriptKeyframe(sequenceTime, std::move(script)); +} + +// Removes a keyframe at the specified 0-based index +[[codegen::luawrap]] void removeKeyframe(int index) { + using namespace openspace; + global::keyframeRecording->removeKeyframe(index); +} + +// Update the camera position at keyframe specified by the 0-based index +[[codegen::luawrap]] void updateKeyframe(int index) { + using namespace openspace; + global::keyframeRecording->updateKeyframe(index); +} + +// Move keyframe of `index` to the new specified `sequenceTime` +[[codegen::luawrap]] void moveKeyframe(int index, double sequenceTime) { + using namespace openspace; + global::keyframeRecording->moveKeyframe(index, sequenceTime); +} + +// Saves the current sequence of keyframes to disk by the optionally specified `filename`. +[[codegen::luawrap]] void saveSequence(std::filesystem::path filename) { + using namespace openspace; + global::keyframeRecording->saveSequence(std::move(filename)); +} + +// Loads a sequence from the specified file +[[codegen::luawrap]] void loadSequence(std::filesystem::path filename) { + using namespace openspace; + global::keyframeRecording->loadSequence(std::move(filename)); +} + +// Playback sequence optionally from the specified `sequenceTime` or if not specified +// starts playing from the current time set within the sequence +[[codegen::luawrap]] void play(std::optional sequenceTime) { + using namespace openspace; + + global::keyframeRecording->play(); + if (sequenceTime.has_value()) { + global::sessionRecordingHandler->seek(*sequenceTime); + } +} + +// Pauses a playing sequence +[[codegen::luawrap]] void pause() { + using namespace openspace; + global::sessionRecordingHandler->setPlaybackPause(true); +} + +// Returns `true` if there currently is a sequence loaded, otherwise `false` +[[codegen::luawrap]] bool hasKeyframeRecording() { + using namespace openspace; + return global::keyframeRecording->hasKeyframeRecording(); +} + +[[codegen::luawrap]] std::vector keyframes() { + using namespace openspace; + return global::keyframeRecording->keyframes(); +} + +#include "keyframerecordinghandler_lua_codegen.cpp" + +} // namespace diff --git a/src/interaction/sessionrecording.cpp b/src/interaction/sessionrecording.cpp index d1b2322c04..3b579ae726 100644 --- a/src/interaction/sessionrecording.cpp +++ b/src/interaction/sessionrecording.cpp @@ -24,2622 +24,677 @@ #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#endif // WIN32 - -#include "sessionrecording_lua.inl" +#include +#include +#include +#include namespace { - constexpr std::string_view _loggerCat = "SessionRecording"; + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; - constexpr bool UsingTimeKeyframes = false; + using namespace openspace::interaction; - constexpr openspace::properties::Property::PropertyInfo RenderPlaybackInfo = { - "RenderInfo", - "Render Playback Information", - "If enabled, information about a currently played back session recording is " - "rendering to screen.", - openspace::properties::Property::Visibility::AdvancedUser + class LoadingError : public ghoul::RuntimeError { + public: + LoadingError(std::string error_, std::filesystem::path file_, int entry_) + : ghoul::RuntimeError( + std::format( + "Error loading session recording '{}' (entry #{}): {}", + file_, entry_, error_ + ), + "SessionRecording" + ) + , error(std::move(error_)) + , file(std::move(file_)) + , entry(entry_) + {} + + LoadingError(std::string error_, std::filesystem::path file_) + : ghoul::RuntimeError( + std::format("Error loading session recording '{}': {}", file_, error_), + "SessionRecording" + ) + , error(std::move(error_)) + , file(std::move(file_)) + {} + + explicit LoadingError(std::string error_) + : ghoul::RuntimeError(error_, "SessionRecording") + , error(std::move(error_)) + {} + + std::string error; + std::filesystem::path file; + int entry = -1; }; - constexpr openspace::properties::Property::PropertyInfo IgnoreRecordedScaleInfo = { - "IgnoreRecordedScale", - "Ignore Recorded Scale", - "If this value is enabled, the scale value from a recording is ignored and the " - "computed values are used instead.", - openspace::properties::Property::Visibility::AdvancedUser + constexpr std::string_view FrameTypeCameraAscii = "camera"; + constexpr std::string_view FrameTypeScriptAscii = "script"; + constexpr std::string_view FrameTypeCommentAscii = "#"; + constexpr char FrameTypeCameraBinary = 'c'; + constexpr char FrameTypeScriptBinary = 's'; + + // Mapping for version numbers in session recording files + constexpr std::array, 3> Versions = { + std::pair("00.85", 0), + std::pair("01.00", 1), + std::pair("02.00", 2) }; - constexpr openspace::properties::Property::PropertyInfo AddModelMatrixinAsciiInfo = { - "AddModelMatrixinAscii", - "Add Model Matrix in ASCII recording", - "If this is 'true', the model matrix is written into the ASCII recording format " - "in the line before each camera keyframe. The model matrix is the full matrix " - "that converts the position into a J2000+Galactic reference frame.", - openspace::properties::Property::Visibility::Developer + + // + // Header information + // + struct Header { + static constexpr std::string_view MagicBytes = "OpenSpace_record/playback"; + static constexpr char DataModeAscii = 'A'; + static constexpr char DataModeBinary = 'B'; + + int version; + DataMode dataMode = DataMode::Ascii; }; + + Header readHeader(std::istream& stream, const std::filesystem::path& filename) { + Header result; + + // Read magic title bytes that must always be the same + std::string magicBytes; + magicBytes.resize(Header::MagicBytes.size()); + stream.read(magicBytes.data(), Header::MagicBytes.size()); + if (!stream || magicBytes != Header::MagicBytes) { + throw LoadingError("Error loading header magic bytes", filename); + } + + // Read the version of the session recording + std::string version; + constexpr int VersionLength = 5 * sizeof(std::byte); + version.resize(VersionLength); + stream.read(version.data(), VersionLength); + if (!stream) { + throw LoadingError("Error loading header version information", filename); + } + result.version = [&version, &filename]() { + for (const std::pair& p : Versions) { + if (p.first == version) { + return p.second; + } + } + throw LoadingError(std::format("Unsupported version {}", version), filename); + }(); + + // Read whether the rest of the file is in ASCII or binary mode + char dataMode = 0; + stream.read(&dataMode, sizeof(char)); + const bool goodDataMode = + dataMode == Header::DataModeAscii || dataMode == Header::DataModeBinary; + if (!stream || !goodDataMode) { + throw LoadingError("Error loading header data mode", filename); + } + result.dataMode = + (dataMode == Header::DataModeAscii) ? DataMode::Ascii : DataMode::Binary; + + // Skip over the line ending + char buffer = 0; + stream.read(&buffer, sizeof(char)); + if (buffer == '\r') { + // Skip over the following \n as well as we have a DOS line ending + stream.seekg(1, std::ios::cur); + } + + return result; + } + + void writeHeader(std::ostream& stream, const Header& header) { + stream.write(Header::MagicBytes.data(), Header::MagicBytes.size()); + + std::string_view version = [&header]() { + for (const std::pair& p : Versions) { + if (p.second == header.version) { + return p.first; + } + } + throw std::logic_error(std::format("Unsupported version {}", header.version)); + }(); + stream.write(version.data(), version.size()); + + const char dataMode = + header.dataMode == DataMode::Ascii ? + Header::DataModeAscii : + Header::DataModeBinary; + stream.write(&dataMode, sizeof(char)); + + stream.write("\n", sizeof(char)); + } + + + // + // Type of the frame + // + enum class FrameType { + Camera, + Script + }; + + template + std::optional readFrameType(std::istream&, int) { + static_assert(sizeof(int) == 0, "Unimplemented overload"); + } + + template <> + std::optional readFrameType(std::istream& stream, int) { + std::string frameType; + stream >> frameType; + + if (!stream || frameType.empty()) { + // Reading the frame type is the first action when reading a frame so we have + // to check here if we have reached the end of the file. This is either + // signalled by the stream ending (if there no terminating empty line) or by + // the `frameType` being empty (if there is a terminating empty line) + return std::nullopt; + } + + if (frameType == FrameTypeCameraAscii) { + return FrameType::Camera; + } + else if (frameType == FrameTypeScriptAscii) { + return FrameType::Script; + } + else { + throw LoadingError(std::format("Unrecognized frame '{}'", frameType)); + } + } + + template <> + std::optional readFrameType(std::istream& stream, int) { + char frameType = 0; + stream.read(&frameType, sizeof(char)); + + if (!stream) { + // Reading the frame type is the first action when reading a frame so we have + // to check here if we have reached the end of the file. + return std::nullopt; + } + + switch (frameType) { + case FrameTypeCameraBinary: + return FrameType::Camera; + case FrameTypeScriptBinary: + return FrameType::Script; + default: + throw LoadingError(std::format("Unrecognized frame '{}'", frameType)); + } + } + + template + void writeFrameType(std::ostream&, const FrameType&) { + static_assert(sizeof(int) == 0, "Unimplemented overload"); + } + + template <> + void writeFrameType(std::ostream& stream, const FrameType& type) { + switch (type) { + case FrameType::Camera: + stream.write(FrameTypeCameraAscii.data(), FrameTypeCameraAscii.size()); + break; + case FrameType::Script: + stream.write(FrameTypeScriptAscii.data(), FrameTypeScriptAscii.size()); + break; + } + } + + template <> + void writeFrameType(std::ostream& stream, const FrameType& type) { + switch (type) { + case FrameType::Camera: + stream.write(&FrameTypeCameraBinary, sizeof(char)); + break; + case FrameType::Script: + stream.write(&FrameTypeScriptBinary, sizeof(char)); + break; + } + } + + // + // Reading the first part of an entry + // + struct Timestamps { + double timestamp = 0.0; + double simulationTime = 0.0; + }; + + // Not defined on purpose + template + Timestamps readTimestamps(std::istream&, int); + + template <> + Timestamps readTimestamps(std::istream& stream, int version) { + Timestamps result; + if (version < 2) { + double dummy; // previously `times.timeOs` + stream >> dummy; + } + + stream >> result.timestamp >> result.simulationTime; + return result; + } + + template <> + Timestamps readTimestamps(std::istream& stream, int version) { + Timestamps result; + if (version < 2) { + stream.seekg(sizeof(double), std::ios::cur); // previously `times.timeOs` + } + stream.read(reinterpret_cast(&result.timestamp), sizeof(double)); + stream.read(reinterpret_cast(&result.simulationTime), sizeof(double)); + return result; + } + + // Not defined on purpose + template + void writeTimestamps(std::ostream&, const Timestamps&); + + template <> + void writeTimestamps(std::ostream& stream, + const Timestamps& timestamps) + { + std::string buffer = std::format( + "{} {}", timestamps.timestamp, timestamps.simulationTime + ); + stream.write(buffer.data(), buffer.size()); + } + + template <> + void writeTimestamps(std::ostream& stream, + const Timestamps& timestamps) + { + stream.write( + reinterpret_cast(×tamps.timestamp), + sizeof(double) + ); + stream.write( + reinterpret_cast(×tamps.simulationTime), + sizeof(double) + ); + } + + + // + // Camera frames + // + + // Not defined on purpose + template + SessionRecording::Entry::Camera readCamera(std::istream&, int); + + template <> + SessionRecording::Entry::Camera readCamera(std::istream& stream, int) + { + SessionRecording::Entry::Camera camera; + std::string rotationFollowing; + stream >> camera.position.x >> camera.position.y >> camera.position.z + >> camera.rotation.x >> camera.rotation.y + >> camera.rotation.z >> camera.rotation.w + >> camera.scale + >> rotationFollowing + >> camera.focusNode; + camera.followFocusNodeRotation = (rotationFollowing == "F"); + return camera; + } + + template <> + SessionRecording::Entry::Camera readCamera(std::istream& stream, + int version) + { + SessionRecording::Entry::Camera camera; + std::array buffer = {}; + stream.read(reinterpret_cast(buffer.data()), 3 * sizeof(double)); + camera.position = glm::dvec3(buffer[0], buffer[1], buffer[2]); + + if (version < 2) { + // Rotations are stored as four doubles immediately get downcasted to floats + stream.read(reinterpret_cast(buffer.data()), 4 * sizeof(double)); + camera.rotation = glm::dquat(buffer[3], buffer[0], buffer[1], buffer[2]); + } + else { + std::array b = {}; + stream.read(reinterpret_cast(b.data()), 4 * sizeof(float)); + camera.rotation = glm::quat(b[3], b[0], b[1], b[2]); + } + char follow = 0; + stream.read(&follow, sizeof(char)); + camera.followFocusNodeRotation = (follow == 1); + + int32_t nodeNameLength = 0; + stream.read(reinterpret_cast(&nodeNameLength), sizeof(int32_t)); + camera.focusNode.resize(nodeNameLength); + stream.read(camera.focusNode.data(), nodeNameLength); + + stream.read(reinterpret_cast(&camera.scale), sizeof(float)); + + if (version < 2) { + stream.seekg(sizeof(double), std::ios::cur); // previously `timestamp` + } + return camera; + } + + // Not defined on purpose + template + void writeCamera(std::ostream&, const SessionRecording::Entry::Camera&); + + template <> + void writeCamera(std::ostream& stream, + const SessionRecording::Entry::Camera& camera) + { + std::string buffer = std::format( + "{} {} {} {} {} {} {} {} {} {}", + camera.position.x, camera.position.y, camera.position.z, + camera.rotation.x, camera.rotation.y, camera.rotation.z, camera.rotation.w, + camera.scale, + camera.followFocusNodeRotation ? "F" : "-", + camera.focusNode + ); + stream.write(buffer.data(), buffer.size()); + } + + template <> + void writeCamera(std::ostream& stream, + const SessionRecording::Entry::Camera& camera) + { + stream.write( + reinterpret_cast(glm::value_ptr(camera.position)), + 3 * sizeof(double) + ); + stream.write( + reinterpret_cast(glm::value_ptr(camera.rotation)), + 4 * sizeof(float) + ); + + const char follow = camera.followFocusNodeRotation ? 1 : 0; + stream.write(&follow, sizeof(char)); + + const int32_t nodeNameLength = static_cast(camera.focusNode.size()); + stream.write(reinterpret_cast(&nodeNameLength), sizeof(int32_t)); + stream.write(camera.focusNode.data(), camera.focusNode.size()); + + stream.write(reinterpret_cast(&camera.scale), sizeof(float)); + } + + // + // Script frames + // + + // Not defined on purpose + template + SessionRecording::Entry::Script readScript(std::istream&, int); + + template <> + SessionRecording::Entry::Script readScript(std::istream& stream, int) + { + SessionRecording::Entry::Script script; + + int numScriptLines = 0; + stream >> numScriptLines; + + std::string tmpReadbackScript; + for (int i = 0; i < numScriptLines; i++) { + ghoul::getline(stream, tmpReadbackScript); + size_t start = tmpReadbackScript.find_first_not_of(" "); + tmpReadbackScript = tmpReadbackScript.substr(start); + if (tmpReadbackScript.back() == '\r') { + tmpReadbackScript.pop_back(); + } + script.append(tmpReadbackScript); + if (i < (numScriptLines - 1)) { + script.append("\n"); + } + } + + return script; + } + + template <> + SessionRecording::Entry::Script readScript(std::istream& stream, + int) + { + SessionRecording::Entry::Script script; + + uint32_t scriptLength = 0; + stream.read(reinterpret_cast(&scriptLength), sizeof(uint32_t)); + script.resize(scriptLength); + stream.read(script.data(), scriptLength); + + return script; + } + + // Not defined on purpose + template + void writeScript(std::ostream&, const SessionRecording::Entry::Script&); + + template <> + void writeScript(std::ostream& stream, + const SessionRecording::Entry::Script& script) + { + SessionRecording::Entry::Script s = script; + + // Erase all \r (from windows newline), and all \n from line endings and replace + // with ';' so that lua will treat them as separate lines. This is done in order + // to treat a multi-line script as a single line in the file. + size_t startPos = s.find('\r', 0); + while (startPos != std::string::npos) { + s.erase(startPos, 1); + startPos = s.find('\r', startPos); + } + startPos = s.find('\n', 0); + while (startPos != std::string::npos) { + s.replace(startPos, 1, ";"); + startPos = s.find('\n', startPos); + } + stream.write("1 ", 2 * sizeof(char)); + stream.write(s.data(), s.size()); + } + + template <> + void writeScript(std::ostream& stream, + const SessionRecording::Entry::Script& script) + { + uint32_t scriptLength = static_cast(script.size()); + stream.write(reinterpret_cast(&scriptLength), sizeof(uint32_t)); + stream.write(script.data(), script.size()); + } + + // + // SessionRecordingEntry + // + template + std::optional readEntry(std::istream& stream, int version) { + std::optional frameType = readFrameType(stream, version); + + if (!frameType.has_value()) { + // We have reached the end of the file + return std::nullopt; + } + + Timestamps timestamps = readTimestamps(stream, version); + SessionRecording::Entry entry = { + .timestamp = timestamps.timestamp, + .simulationTime = timestamps.simulationTime + }; + + switch (*frameType) { + case FrameType::Camera: + entry.value = readCamera(stream, version); + break; + case FrameType::Script: + entry.value = readScript(stream, version); + break; + } + + return entry; + } + + std::optional readEntry(std::istream& stream, + DataMode dataMode, int version) + { + return + dataMode == DataMode::Ascii ? + readEntry(stream, version) : + readEntry(stream, version); + } + + template + void writeEntry(std::ostream& stream, const SessionRecording::Entry& entry) { + if (std::holds_alternative(entry.value)) { + writeFrameType(stream, FrameType::Camera); + } + else if (std::holds_alternative(entry.value)) { + writeFrameType(stream, FrameType::Script); + } + else { + throw std::logic_error("Unhandled variant"); + } + if constexpr (mode == DataMode::Ascii) { + stream.write(" ", sizeof(char)); + } + + + writeTimestamps(stream, { entry.timestamp, entry.simulationTime }); + if constexpr (mode == DataMode::Ascii) { + stream.write(" ", sizeof(char)); + } + + std::visit(overloaded { + [&stream](const SessionRecording::Entry::Camera& value) { + writeCamera(stream, value); + }, + [&stream](const SessionRecording::Entry::Script& value) { + writeScript(stream, value); + } + }, entry.value); + } + + void writeEntry(std::ostream& stream, const SessionRecording::Entry& entry, + DataMode dataMode) + { + dataMode == DataMode::Ascii ? + writeEntry(stream, entry) : + writeEntry(stream, entry); + } + + } // namespace namespace openspace::interaction { -ConversionError::ConversionError(std::string msg) - : ghoul::RuntimeError(std::move(msg), "conversionError") -{} - -SessionRecording::SessionRecording() - : properties::PropertyOwner({ "SessionRecording", "Session Recording" }) - , _renderPlaybackInformation(RenderPlaybackInfo, false) - , _ignoreRecordedScale(IgnoreRecordedScaleInfo, false) - , _addModelMatrixinAscii(AddModelMatrixinAsciiInfo, false) -{} - -SessionRecording::SessionRecording(bool isGlobal) - : SessionRecording() -{ - if (isGlobal) { - ghoul::TemplateFactory* fTask = FactoryManager::ref().factory(); - ghoul_assert(fTask, "No task factory existed"); - fTask->registerClass("ConvertRecFormatTask"); - fTask->registerClass("ConvertRecFileVersionTask"); - addProperty(_renderPlaybackInformation); - addProperty(_ignoreRecordedScale); - addProperty(_addModelMatrixinAscii); - } -} - -void SessionRecording::deinitialize() { - stopRecording(); - stopPlayback(); -} - -void SessionRecording::setRecordDataFormat(DataMode dataMode) { - _recordingDataMode = dataMode; -} - -bool SessionRecording::hasFileExtension(const std::string& filename, - const std::string& extension) -{ - if (filename.length() <= extension.length()) { - return false; - } - else { - return (filename.substr(filename.length() - extension.length()) == extension); - } -} - -bool SessionRecording::isPath(std::string& filename) { - const size_t unixDelimiter = filename.find('/'); - const size_t windowsDelimiter = filename.find('\\'); - return (unixDelimiter != std::string::npos || windowsDelimiter != std::string::npos); -} - -void SessionRecording::removeTrailingPathSlashes(std::string& filename) const { - while (filename.substr(filename.length() - 1, 1) == "/") { - filename.pop_back(); - } - while (filename.substr(filename.length() - 1, 1) == "\\") { - filename.pop_back(); - } -} - -bool SessionRecording::handleRecordingFile(std::string filenameIn) { - if (_recordingDataMode == DataMode::Binary) { - if (hasFileExtension(filenameIn, FileExtensionAscii)) { - LERROR("Specified filename for binary recording has ascii file extension"); - return false; - } - else if (!hasFileExtension(filenameIn, FileExtensionBinary)) { - filenameIn += FileExtensionBinary; - } - } - else if (_recordingDataMode == DataMode::Ascii) { - if (hasFileExtension(filenameIn, FileExtensionBinary)) { - LERROR("Specified filename for ascii recording has binary file extension"); - return false; - } - else if (!hasFileExtension(filenameIn, FileExtensionAscii)) { - filenameIn += FileExtensionAscii; - } - } - - std::filesystem::path absFilename = filenameIn; - if (absFilename.parent_path().empty() || absFilename.parent_path() == absFilename) { - absFilename = absPath("${RECORDINGS}/" + filenameIn); - } - else if (absFilename.parent_path().is_relative()) { - LERROR("If path is provided with the filename, then it must be an absolute path"); - return false; - } - else if (!std::filesystem::exists(absFilename.parent_path())) { - LERROR(std::format( - "The recording filename path '{}' is not a valid location in the filesytem", - absFilename.parent_path().string() - )); - return false; - } - - if (std::filesystem::is_regular_file(absFilename)) { - LERROR(std::format( - "Unable to start recording; file '{}' already exists", absFilename - )); - return false; - } - if (_recordingDataMode == DataMode::Binary) { - _recordFile.open(absFilename, std::ios::binary); - } - else { - _recordFile.open(absFilename); - } - - if (!_recordFile.is_open() || !_recordFile.good()) { - LERROR(std::format( - "Unable to open file '{}' for keyframe recording", absFilename - )); - return false; - } - return true; -} - -bool SessionRecording::startRecording(const std::string& filename) { - _timeline.clear(); - if (_state == SessionState::Recording) { - LERROR("Unable to start recording while already in recording mode"); - return false; - } - else if (isPlayingBack()) { - LERROR("Unable to start recording while in session playback mode"); - return false; - } - if (!std::filesystem::is_directory(absPath("${RECORDINGS}"))) { - std::filesystem::create_directories(absPath("${RECORDINGS}")); - } - - const bool recordingFileOK = handleRecordingFile(filename); - - if (recordingFileOK) { - _state = SessionState::Recording; - _playbackActive_camera = false; - _playbackActive_time = false; - _playbackActive_script = false; - _propertyBaselinesSaved.clear(); - _keyframesSavePropertiesBaseline_scripts.clear(); - _keyframesSavePropertiesBaseline_timeline.clear(); - _recordingEntryNum = 1; - - _recordFile << FileHeaderTitle; - _recordFile.write(FileHeaderVersion, FileHeaderVersionLength); - if (_recordingDataMode == DataMode::Binary) { - _recordFile << DataFormatBinaryTag; - } - else { - _recordFile << DataFormatAsciiTag; - } - _recordFile << '\n'; - - _timestampRecordStarted = global::windowDelegate->applicationTime(); - - // Record the current delta time as the first property to save in the file. - // This needs to be saved as a baseline whether or not it changes during recording - _timestamps3RecordStarted = { - .timeOs = _timestampRecordStarted, - .timeRec = 0.0, - .timeSim = global::timeManager->time().j2000Seconds() - }; - - recordCurrentTimePauseState(); - recordCurrentTimeRate(); - LINFO("Session recording started"); - } - - return recordingFileOK; -} - -void SessionRecording::recordCurrentTimePauseState() { - const bool isPaused = global::timeManager->isPaused(); - std::string initialTimePausedCommand = "openspace.time.setPause(" + - std::string(isPaused ? "true" : "false") + ")"; - saveScriptKeyframeToPropertiesBaseline(std::move(initialTimePausedCommand)); -} - -void SessionRecording::recordCurrentTimeRate() { - std::string initialTimeRateCommand = std::format( - "openspace.time.setDeltaTime({})", global::timeManager->targetDeltaTime() - ); - saveScriptKeyframeToPropertiesBaseline(std::move(initialTimeRateCommand)); -} - -void SessionRecording::stopRecording() { - if (_state == SessionState::Recording) { - // Add all property baseline scripts to the beginning of the recording file - datamessagestructures::ScriptMessage smTmp; - for (TimelineEntry& initPropScripts : _keyframesSavePropertiesBaseline_timeline) { - if (initPropScripts.keyframeType == RecordedType::Script) { - smTmp._script = _keyframesSavePropertiesBaseline_scripts - [initPropScripts.idxIntoKeyframeTypeArray]; - saveSingleKeyframeScript( - smTmp, - _timestamps3RecordStarted, - _recordingDataMode, - _recordFile, - _keyframeBuffer - ); - } - } - for (TimelineEntry entry : _timeline) { - switch (entry.keyframeType) { - case RecordedType::Camera: - { - interaction::KeyframeNavigator::CameraPose kf - = _keyframesCamera[entry.idxIntoKeyframeTypeArray]; - datamessagestructures::CameraKeyframe kfMsg( - std::move(kf.position), - std::move(kf.rotation), - std::move(kf.focusNode), - kf.followFocusNodeRotation, - kf.scale - ); - saveSingleKeyframeCamera( - kfMsg, - entry.t3stamps, - _recordingDataMode, - _recordFile, - _keyframeBuffer - ); - break; - } - case RecordedType::Time: - { - datamessagestructures::TimeKeyframe tf - = _keyframesTime[entry.idxIntoKeyframeTypeArray]; - saveSingleKeyframeTime( - tf, - entry.t3stamps, - _recordingDataMode, - _recordFile, - _keyframeBuffer - ); - break; - } - case RecordedType::Script: - { - smTmp._script = _keyframesScript[entry.idxIntoKeyframeTypeArray]; - saveSingleKeyframeScript( - smTmp, - entry.t3stamps, - _recordingDataMode, - _recordFile, - _keyframeBuffer - ); - break; - } - default: - { - break; - } - } - } - _state = SessionState::Idle; - LINFO("Session recording stopped"); - } - // Close the recording file - _recordFile.close(); - _cleanupNeededRecording = true; -} - -bool SessionRecording::startPlayback(std::string& filename, - KeyframeTimeRef timeMode, - bool forceSimTimeAtStart, - bool loop, bool shouldWaitForFinishedTiles) -{ - std::string absFilename; - if (std::filesystem::is_regular_file(filename)) { - absFilename = filename; - } - else { - absFilename = absPath("${RECORDINGS}/" + filename).string(); - } - // Run through conversion in case file is older. Does nothing if the file format - // is up-to-date - absFilename = convertFile(absFilename); - - if (_state == SessionState::Recording) { - LERROR("Unable to start playback while in session recording mode"); - return false; - } - else if (isPlayingBack()) { - LERROR("Unable to start new playback while in session playback mode"); - return false; - } - - if (!std::filesystem::is_regular_file(absFilename)) { - LERROR("Cannot find the specified playback file"); - cleanUpPlayback(); - return false; - } - - _playbackLineNum = 1; - _playbackFilename = absFilename; - _playbackLoopMode = loop; - _shouldWaitForFinishLoadingWhenPlayback = shouldWaitForFinishedTiles; - - // Open in ASCII first - _playbackFile.open(_playbackFilename, std::ifstream::in); - // Read header - const std::string readBackHeaderString = readHeaderElement( - _playbackFile, - FileHeaderTitle.length() - ); - if (readBackHeaderString != FileHeaderTitle) { - LERROR("Specified playback file does not contain expected header"); - cleanUpPlayback(); - return false; - } - readHeaderElement(_playbackFile, FileHeaderVersionLength); - std::string readDataMode = readHeaderElement(_playbackFile, 1); - if (readDataMode[0] == DataFormatAsciiTag) { - _recordingDataMode = DataMode::Ascii; - } - else if (readDataMode[0] == DataFormatBinaryTag) { - _recordingDataMode = DataMode::Binary; - } - else { - LERROR("Unknown data type in header (should be Ascii or Binary)"); - cleanUpPlayback(); - } - // throwaway newline character(s) - std::string lineEnd = readHeaderElement(_playbackFile, 1); - bool hasDosLineEnding = (lineEnd == "\r"); - if (hasDosLineEnding) { - // throwaway the second newline character (\n) also - readHeaderElement(_playbackFile, 1); - } - - if (_recordingDataMode == DataMode::Binary) { - // Close & re-open the file, starting from the beginning, and do dummy read - // past the header, version, and data type - _playbackFile.close(); - _playbackFile.open(_playbackFilename, std::ifstream::in | std::ios::binary); - const size_t headerSize = FileHeaderTitle.length() + FileHeaderVersionLength - + sizeof(DataFormatBinaryTag) + sizeof('\n'); - std::vector hBuffer; - hBuffer.resize(headerSize); - _playbackFile.read(hBuffer.data(), headerSize); - } - - if (!_playbackFile.is_open() || !_playbackFile.good()) { - LERROR(std::format( - "Unable to open file '{}' for keyframe playback", absFilename.c_str() - )); - stopPlayback(); - cleanUpPlayback(); - return false; - } - _saveRendering_isFirstFrame = true; - // Set time reference mode - _playbackForceSimTimeAtStart = forceSimTimeAtStart; - double now = global::windowDelegate->applicationTime(); - _playbackTimeReferenceMode = timeMode; - initializePlayback_time(now); - - global::scriptScheduler->setTimeReferenceMode(timeMode); - _loadedNodes.clear(); - populateListofLoadedSceneGraphNodes(); - - if (!playbackAddEntriesToTimeline()) { - cleanUpPlayback(); - return false; - } - - initializePlayback_modeFlags(); - if (!initializePlayback_timeline()) { - cleanUpPlayback(); - return false; - } - - const bool canTriggerPlayback = global::openSpaceEngine->setMode( - OpenSpaceEngine::Mode::SessionRecordingPlayback - ); - - if (!canTriggerPlayback) { - cleanUpPlayback(); - return false; - } - - LINFO(std::format( - "Playback session started: ({:8.3f},0.0,{:13.3f}) with {}/{}/{} entries, " - "forceTime={}", - now, _timestampPlaybackStarted_simulation, _keyframesCamera.size(), - _keyframesTime.size(), _keyframesScript.size(), - (_playbackForceSimTimeAtStart ? 1 : 0) - )); - - global::eventEngine->publishEvent( - events::EventSessionRecordingPlayback::State::Started - ); - initializePlayback_triggerStart(); - - global::navigationHandler->orbitalNavigator().updateOnCameraInteraction(); - - return true; -} - -void SessionRecording::initializePlayback_time(double now) { - using namespace std::chrono; - _timestampPlaybackStarted_application = now; - _timestampPlaybackStarted_simulation = global::timeManager->time().j2000Seconds(); - _timestampApplicationStarted_simulation = _timestampPlaybackStarted_simulation - now; - _saveRenderingCurrentRecordedTime_interpolation = steady_clock::now(); - _saveRenderingCurrentApplicationTime_interpolation = - global::windowDelegate->applicationTime(); - _saveRenderingClockInterpolation_countsPerSec = - system_clock::duration::period::den / system_clock::duration::period::num; - _playbackPauseOffset = 0.0; - global::navigationHandler->keyframeNavigator().setTimeReferenceMode( - _playbackTimeReferenceMode, now); -} - -void SessionRecording::initializePlayback_modeFlags() { - _playbackActive_camera = true; - _playbackActive_script = true; - if (UsingTimeKeyframes) { - _playbackActive_time = true; - } - _hasHitEndOfCameraKeyframes = false; -} - -bool SessionRecording::initializePlayback_timeline() { - if (!findFirstCameraKeyframeInTimeline()) { - return false; - } - if (_playbackForceSimTimeAtStart) { - const Timestamps times = _timeline[_idxTimeline_cameraFirstInTimeline].t3stamps; - global::timeManager->setTimeNextFrame(Time(times.timeSim)); - _saveRenderingCurrentRecordedTime = times.timeRec; - } - _idxTimeline_nonCamera = 0; - _idxTime = 0; - _idxScript = 0; - _idxTimeline_cameraPtrNext = 0; - _idxTimeline_cameraPtrPrev = 0; - return true; -} - -void SessionRecording::initializePlayback_triggerStart() { - _state = SessionState::Playback; -} - -bool SessionRecording::isPlaybackPaused() { - return (_state == SessionState::PlaybackPaused); -} - -void SessionRecording::setPlaybackPause(bool pause) { - if (pause && _state == SessionState::Playback) { - _playbackPausedWithinDeltaTimePause = global::timeManager->isPaused(); - if (!_playbackPausedWithinDeltaTimePause) { - global::timeManager->setPause(true); - } - _state = SessionState::PlaybackPaused; - global::eventEngine->publishEvent( - events::EventSessionRecordingPlayback::State::Paused - ); - } - else if (!pause && _state == SessionState::PlaybackPaused) { - if (!_playbackPausedWithinDeltaTimePause) { - global::timeManager->setPause(false); - } - _state = SessionState::Playback; - global::eventEngine->publishEvent( - events::EventSessionRecordingPlayback::State::Resumed - ); - } -} - -bool SessionRecording::findFirstCameraKeyframeInTimeline() { - bool foundCameraKeyframe = false; - for (unsigned int i = 0; i < _timeline.size(); i++) { - if (doesTimelineEntryContainCamera(i)) { - _idxTimeline_cameraFirstInTimeline = i; - _idxTimeline_cameraPtrPrev = _idxTimeline_cameraFirstInTimeline; - _idxTimeline_cameraPtrNext = _idxTimeline_cameraFirstInTimeline; - _cameraFirstInTimeline_timestamp = appropriateTimestamp( - _timeline[_idxTimeline_cameraFirstInTimeline].t3stamps); - foundCameraKeyframe = true; - break; - } - } - - if (!foundCameraKeyframe) { - signalPlaybackFinishedForComponent(RecordedType::Camera); - return true; - } - else { - return checkIfInitialFocusNodeIsLoaded(_idxTimeline_cameraFirstInTimeline); - } -} - -void SessionRecording::signalPlaybackFinishedForComponent(RecordedType type) { - if (type == RecordedType::Camera) { - _playbackActive_camera = false; - LINFO("Playback finished signal: camera"); - } - else if (type == RecordedType::Time) { - _playbackActive_time = false; - LINFO("Playback finished signal: time"); - } - else if (type == RecordedType::Script) { - _playbackActive_script = false; - LINFO("Playback finished signal: script"); - } - - if (!_playbackActive_camera && !_playbackActive_time && !_playbackActive_script) { - if (_playbackLoopMode) { - // Loop back to the beginning to replay - _saveRenderingDuringPlayback = false; - initializePlayback_time(global::windowDelegate->applicationTime()); - initializePlayback_modeFlags(); - initializePlayback_timeline(); - initializePlayback_triggerStart(); - } - else { - LINFO("Playback session finished"); - handlePlaybackEnd(); - } - } -} - -void SessionRecording::handlePlaybackEnd() { - _state = SessionState::Idle; - _cleanupNeededPlayback = true; - global::eventEngine->publishEvent( - events::EventSessionRecordingPlayback::State::Finished - ); - global::openSpaceEngine->resetMode(); - global::navigationHandler->resetNavigationUpdateVariables(); -} - -void SessionRecording::enableTakeScreenShotDuringPlayback(int fps) { - _saveRenderingDuringPlayback = true; - _saveRenderingDeltaTime = 1.0 / fps; - _saveRenderingDeltaTime_interpolation_usec = - std::chrono::microseconds(static_cast(_saveRenderingDeltaTime * 1000000)); -} - -void SessionRecording::disableTakeScreenShotDuringPlayback() { - _saveRenderingDuringPlayback = false; -} - -void SessionRecording::stopPlayback() { - if (isPlayingBack()) { - LINFO("Session playback stopped"); - handlePlaybackEnd(); - } -} - -void SessionRecording::cleanUpPlayback() { - Camera* camera = global::navigationHandler->camera(); - ghoul_assert(camera != nullptr, "Camera must not be nullptr"); - Scene* scene = camera->parent()->scene(); - if (!_timeline.empty()) { - const unsigned int p = - _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; - if (!_keyframesCamera.empty()) { - const SceneGraphNode* n = scene->sceneGraphNode( - _keyframesCamera[p].focusNode - ); - if (n) { - global::navigationHandler->orbitalNavigator().setFocusNode( - n->identifier() - ); - } - } - } - - _playbackFile.close(); - cleanUpTimelinesAndKeyframes(); - _cleanupNeededPlayback = false; -} - -void SessionRecording::cleanUpRecording() { - cleanUpTimelinesAndKeyframes(); - _cleanupNeededRecording = false; -} - -void SessionRecording::cleanUpTimelinesAndKeyframes() { - _timeline.clear(); - _keyframesCamera.clear(); - _keyframesTime.clear(); - _keyframesScript.clear(); - _keyframesSavePropertiesBaseline_scripts.clear(); - _keyframesSavePropertiesBaseline_timeline.clear(); - _propertyBaselinesSaved.clear(); - _loadedNodes.clear(); - _idxTimeline_nonCamera = 0; - _idxTime = 0; - _idxScript = 0; - _idxTimeline_cameraPtrNext = 0; - _idxTimeline_cameraPtrPrev = 0; - _hasHitEndOfCameraKeyframes = false; - _saveRenderingDuringPlayback = false; - _saveRendering_isFirstFrame = true; - _playbackPauseOffset = 0.0; - _playbackLoopMode = false; - _playbackForceSimTimeAtStart = false; -} - -void SessionRecording::writeToFileBuffer(unsigned char* buf, - size_t& idx, - double src) -{ - const size_t writeSize_bytes = sizeof(double); - unsigned char const *p = reinterpret_cast(&src); - memcpy((buf + idx), p, writeSize_bytes); - idx += writeSize_bytes; -} - -void SessionRecording::writeToFileBuffer(unsigned char* buf, - size_t& idx, - std::vector& cv) -{ - const size_t writeSize_bytes = cv.size() * sizeof(char); - memcpy((buf + idx), cv.data(), writeSize_bytes); - idx += writeSize_bytes; -} - -void SessionRecording::writeToFileBuffer(unsigned char* buf, - size_t& idx, - unsigned char c) -{ - const size_t writeSize_bytes = sizeof(char); - buf[idx] = c; - idx += writeSize_bytes; -} - -void SessionRecording::writeToFileBuffer(unsigned char* buf, - size_t& idx, - bool b) -{ - buf[idx] = b ? 1 : 0; - idx += sizeof(char); -} - -void SessionRecording::saveStringToFile(const std::string& s, - unsigned char* kfBuffer, - size_t& idx, - std::ofstream& file) -{ - size_t strLen = s.size(); - const size_t writeSize_bytes = sizeof(size_t); - - idx = 0; - unsigned char const *p = reinterpret_cast(&strLen); - memcpy((kfBuffer + idx), p, writeSize_bytes); - idx += static_cast(writeSize_bytes); - saveKeyframeToFileBinary(kfBuffer, idx, file); - - file.write(s.c_str(), s.size()); -} - -bool SessionRecording::hasCameraChangedFromPrev( - const datamessagestructures::CameraKeyframe& kfNew) -{ - constexpr double threshold = 1e-2; - bool hasChanged = false; - - const glm::dvec3 position = kfNew._position - _prevRecordedCameraKeyframe._position; - if (glm::length(position) > threshold) { - hasChanged = true; - } - - const double rotation = dot(kfNew._rotation, _prevRecordedCameraKeyframe._rotation); - if (std::abs(rotation - 1.0) > threshold) { - hasChanged = true; - } - - _prevRecordedCameraKeyframe = kfNew; - return hasChanged; -} - -SessionRecording::Timestamps SessionRecording::generateCurrentTimestamp3( - double keyframeTime) const -{ - return { - keyframeTime, - keyframeTime - _timestampRecordStarted, - global::timeManager->time().j2000Seconds() - }; -} - -void SessionRecording::saveCameraKeyframeToTimeline() { - const SceneGraphNode* an = global::navigationHandler->orbitalNavigator().anchorNode(); - if (!an) { - return; - } - - // Create a camera keyframe, then call to populate it with current position - // & orientation of camera - datamessagestructures::CameraKeyframe kf = - datamessagestructures::generateCameraKeyframe(); - - Timestamps times = generateCurrentTimestamp3(kf._timestamp); - interaction::KeyframeNavigator::CameraPose pbFrame(std::move(kf)); - addKeyframe(std::move(times), std::move(pbFrame), _recordingEntryNum++); -} - -void SessionRecording::saveHeaderBinary(Timestamps& times, - char type, - unsigned char* kfBuffer, - size_t& idx) -{ - kfBuffer[idx++] = type; - writeToFileBuffer(kfBuffer, idx, times.timeOs); - writeToFileBuffer(kfBuffer, idx, times.timeRec); - writeToFileBuffer(kfBuffer, idx, times.timeSim); -} - -void SessionRecording::saveHeaderAscii(Timestamps& times, - const std::string& type, - std::stringstream& line) -{ - line << type << ' '; - line << times.timeOs << ' '; - line << times.timeRec << ' '; - line << std::fixed << std::setprecision(3) << times.timeSim << ' '; -} - -void SessionRecording::saveCameraKeyframeBinary(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, - unsigned char* kfBuffer, - std::ofstream& file) -{ - // Writing to a binary session recording file - size_t idx = 0; - saveHeaderBinary(times, HeaderCameraBinary, kfBuffer, idx); - // Writing to internal buffer, and then to file, for performance reasons - std::vector writeBuffer; - kf.serialize(writeBuffer); - writeToFileBuffer(kfBuffer, idx, writeBuffer); - saveKeyframeToFileBinary(kfBuffer, idx, file); -} - -void SessionRecording::saveCameraKeyframeAscii(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, - std::ofstream& file) -{ - if (_addModelMatrixinAscii) { - SceneGraphNode* node = sceneGraphNode(kf._focusNode); - const glm::dmat4 modelTransform = node->modelTransform(); - - file << HeaderCommentAscii << ' ' << ghoul::to_string(modelTransform) << '\n'; - } - - std::stringstream keyframeLine = std::stringstream(); - saveHeaderAscii(times, HeaderCameraAscii, keyframeLine); - kf.write(keyframeLine); - saveKeyframeToFile(keyframeLine.str(), file); -} - -void SessionRecording::saveTimeKeyframeToTimeline() { - // Create a time keyframe, then call to populate it with current time props - const datamessagestructures::TimeKeyframe kf = - datamessagestructures::generateTimeKeyframe(); - - Timestamps times = generateCurrentTimestamp3(kf._timestamp); - addKeyframe(std::move(times), kf, _recordingEntryNum++); -} - -void SessionRecording::saveTimeKeyframeBinary(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, - unsigned char* kfBuffer, - std::ofstream& file) -{ - size_t idx = 0; - saveHeaderBinary(times, HeaderTimeBinary, kfBuffer, idx); - std::vector writeBuffer; - kf.serialize(writeBuffer); - writeToFileBuffer(kfBuffer, idx, writeBuffer); - saveKeyframeToFileBinary(kfBuffer, idx, file); -} - -void SessionRecording::saveTimeKeyframeAscii(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, - std::ofstream& file) -{ - std::stringstream keyframeLine = std::stringstream(); - saveHeaderAscii(times, HeaderTimeAscii, keyframeLine); - kf.write(keyframeLine); - saveKeyframeToFile(keyframeLine.str(), file); -} - -void SessionRecording::saveScriptKeyframeToTimeline(std::string script) { - if (script.starts_with(scriptReturnPrefix)) { - script = script.substr(scriptReturnPrefix.length()); - } - for (const std::string& reject : _scriptRejects) { - if (script.starts_with(reject)) { - return; - } - } - trimCommandsFromScriptIfFound(script); - replaceCommandsFromScriptIfFound(script); - const datamessagestructures::ScriptMessage sm - = datamessagestructures::generateScriptMessage(script); - - Timestamps times = generateCurrentTimestamp3(sm._timestamp); - addKeyframe(std::move(times), sm._script, _playbackLineNum); -} - -void SessionRecording::saveScriptKeyframeToPropertiesBaseline(std::string script) { - const Timestamps times = generateCurrentTimestamp3( - global::windowDelegate->applicationTime() - ); - const size_t indexIntoScriptKeyframesFromMainTimeline = - _keyframesSavePropertiesBaseline_scripts.size(); - _keyframesSavePropertiesBaseline_scripts.push_back(std::move(script)); - addKeyframeToTimeline( - _keyframesSavePropertiesBaseline_timeline, - RecordedType::Script, - indexIntoScriptKeyframesFromMainTimeline, - times, - 0 - ); -} - -void SessionRecording::trimCommandsFromScriptIfFound(std::string& script) { - for (const std::string& trimSnippet : _scriptsToBeTrimmed) { - auto findIdx = script.find(trimSnippet); - if (findIdx != std::string::npos) { - auto findClosingParens = script.find_first_of(')', findIdx); - script.erase(findIdx, findClosingParens + 1); - } - } -} - -void SessionRecording::replaceCommandsFromScriptIfFound(std::string& script) { - for (const ScriptSubstringReplace& replacementSnippet : _scriptsToBeReplaced) { - auto findIdx = script.find(replacementSnippet.substringFound); - if (findIdx != std::string::npos) { - script.erase(findIdx, replacementSnippet.substringFound.length()); - script.insert(findIdx, replacementSnippet.substringReplacement); - } - } -} - -void SessionRecording::saveScriptKeyframeBinary(Timestamps& times, - datamessagestructures::ScriptMessage& sm, - unsigned char* smBuffer, - std::ofstream& file) -{ - size_t idx = 0; - saveHeaderBinary(times, HeaderScriptBinary, smBuffer, idx); - // Writing to internal buffer, and then to file, for performance reasons - std::vector writeBuffer; - sm.serialize(writeBuffer); - writeToFileBuffer(smBuffer, idx, writeBuffer); - saveKeyframeToFileBinary(smBuffer, idx, file); -} - -void SessionRecording::saveScriptKeyframeAscii(Timestamps& times, - datamessagestructures::ScriptMessage& sm, - std::ofstream& file) -{ - std::stringstream keyframeLine = std::stringstream(); - saveHeaderAscii(times, HeaderScriptAscii, keyframeLine); - // Erase all \r (from windows newline), and all \n from line endings and replace with - // ';' so that lua will treat them as separate lines. This is done in order to treat - // a multi-line script as a single line in the file. - size_t startPos = sm._script.find('\r', 0); - while (startPos != std::string::npos) { - sm._script.erase(startPos, 1); - startPos = sm._script.find('\r', startPos); - } - startPos = sm._script.find('\n', 0); - while (startPos != std::string::npos) { - sm._script.replace(startPos, 1, ";"); - startPos = sm._script.find('\n', startPos); - } - sm.write(keyframeLine); - saveKeyframeToFile(keyframeLine.str(), file); -} - -void SessionRecording::savePropertyBaseline(properties::Property& prop) { - const std::string propIdentifier = prop.uri(); - if (isPropertyAllowedForBaseline(propIdentifier)) { - const bool isPropAlreadySaved = ( - std::find( - _propertyBaselinesSaved.begin(), - _propertyBaselinesSaved.end(), - propIdentifier - ) - != _propertyBaselinesSaved.end() - ); - if (!isPropAlreadySaved) { - const std::string initialScriptCommand = std::format( - "openspace.setPropertyValueSingle(\"{}\", {})", - propIdentifier, prop.stringValue() - ); - saveScriptKeyframeToPropertiesBaseline(initialScriptCommand); - _propertyBaselinesSaved.push_back(propIdentifier); - } - } -} - -bool SessionRecording::isPropertyAllowedForBaseline(const std::string& propString) { - for (const std::string& reject : _propertyBaselineRejects) { - if (propString.starts_with(reject)) { - return false; - } - } - return true; -} - -void SessionRecording::preSynchronization() { - ZoneScoped; - - if (_state == SessionState::Recording) { - saveCameraKeyframeToTimeline(); - if (UsingTimeKeyframes) { - saveTimeKeyframeToTimeline(); - } - } - else if (isPlayingBack()) { - moveAheadInTime(); - } - else if (_cleanupNeededPlayback) { - cleanUpPlayback(); - } - else if (_cleanupNeededRecording) { - cleanUpRecording(); - } - - // Handle callback(s) for change in idle/record/playback state - if (_state != _lastState) { - using K = CallbackHandle; - using V = StateChangeCallback; - for (const std::pair& it : _stateChangeCallbacks) { - it.second(); - } - } - _lastState = _state; -} - -void SessionRecording::render() { - ZoneScoped; - - if (!(_renderPlaybackInformation && isPlayingBack())) { - return; - } - - - constexpr std::string_view FontName = "Mono"; - constexpr float FontSizeFrameinfo = 32.f; - const std::shared_ptr font = - global::fontManager->font(FontName, FontSizeFrameinfo); - - const glm::vec2 res = global::renderEngine->fontResolution(); - glm::vec2 penPosition = glm::vec2( - res.x / 2 - 150.f, - res.y / 4 - ); - const std::string text1 = std::to_string(currentTime()); - ghoul::fontrendering::RenderFont( - *font, - penPosition, - text1, - glm::vec4(1.f), - ghoul::fontrendering::CrDirection::Down - ); - const std::string text2 = std::format( - "Scale: {}", global::navigationHandler->camera()->scaling() - ); - ghoul::fontrendering::RenderFont(*font, penPosition, text2, glm::vec4(1.f)); -} - -bool SessionRecording::isRecording() const { - return (_state == SessionState::Recording); -} - -bool SessionRecording::isPlayingBack() const { - return (_state == SessionState::Playback || _state == SessionState::PlaybackPaused); -} - -bool SessionRecording::isSavingFramesDuringPlayback() const { - return (isPlayingBack() && _saveRenderingDuringPlayback); -} - -bool SessionRecording::shouldWaitForTileLoading() const { - return _shouldWaitForFinishLoadingWhenPlayback; -} - -SessionRecording::SessionState SessionRecording::state() const { - return _state; -} - -bool SessionRecording::playbackAddEntriesToTimeline() { - bool parsingStatusOk = true; - - if (_recordingDataMode == DataMode::Binary) { - while (parsingStatusOk) { - unsigned char frameType = readFromPlayback(_playbackFile); - // Check if have reached EOF - if (!_playbackFile) { - LINFO(std::format( - "Finished parsing {} entries from playback file '{}'", - _playbackLineNum - 1, _playbackFilename - )); - break; - } - if (frameType == HeaderCameraBinary) { - parsingStatusOk = playbackCamera(); - } - else if (frameType == HeaderTimeBinary) { - parsingStatusOk = playbackTimeChange(); - } - else if (frameType == HeaderScriptBinary) { - parsingStatusOk = playbackScript(); - } - else { - LERROR(std::format( - "Unknown frame type {} @ index {} of playback file '{}'", - frameType, _playbackLineNum - 1, _playbackFilename - )); - parsingStatusOk = false; - break; - } - - _playbackLineNum++; - } - } - else { - while (parsingStatusOk && ghoul::getline(_playbackFile, _playbackLineParsing)) { - _playbackLineNum++; - - std::istringstream iss(_playbackLineParsing); - std::string entryType; - if (!(iss >> entryType)) { - LERROR(std::format( - "Error reading entry type @ line {} of playback file '{}'", - _playbackLineNum, _playbackFilename - )); - break; - } - - if (entryType == HeaderCameraAscii) { - parsingStatusOk = playbackCamera(); - } - else if (entryType == HeaderTimeAscii) { - parsingStatusOk = playbackTimeChange(); - } - else if (entryType == HeaderScriptAscii) { - parsingStatusOk = playbackScript(); - } - else if (entryType.substr(0, 1) == HeaderCommentAscii) { - continue; - } - else { - LERROR(std::format( - "Unknown frame type {} @ line {} of playback file '{}'", - entryType, _playbackLineNum, _playbackFilename - )); - parsingStatusOk = false; - break; - } - } - LINFO(std::format( - "Finished parsing {} entries from playback file '{}'", - _playbackLineNum, _playbackFilename - )); - } - - return parsingStatusOk; -} - -double SessionRecording::appropriateTimestamp(Timestamps t3stamps) -{ - if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { - return t3stamps.timeRec; - } - else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { - return t3stamps.timeSim; - } - else { - return t3stamps.timeOs; - } -} - -double SessionRecording::equivalentSimulationTime(double timeOs, - double timeRec, - double timeSim) -{ - if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { - return _timestampPlaybackStarted_simulation + timeRec; - } - else if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_applicationStart) { - return _timestampApplicationStarted_simulation + timeOs; - } - else { - return timeSim; - } -} - -double SessionRecording::equivalentApplicationTime(double timeOs, - double timeRec, - double timeSim) -{ - if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { - return _timestampPlaybackStarted_application + timeRec; - } - else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { - return timeSim - _timestampApplicationStarted_simulation; - } - else { - return timeOs; - } -} - -double SessionRecording::currentTime() const { - if (isSavingFramesDuringPlayback()) { - return _saveRenderingCurrentRecordedTime; - } - else if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { - return (global::windowDelegate->applicationTime() - - _timestampPlaybackStarted_application) - _playbackPauseOffset; - } - else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { - return global::timeManager->time().j2000Seconds(); - } - else { - return global::windowDelegate->applicationTime() - _playbackPauseOffset; - } -} - -double SessionRecording::fixedDeltaTimeDuringFrameOutput() const { - // Check if renderable in focus is still resolving tile loading - // do not adjust time while we are doing this - const SceneGraphNode* focusNode = - global::navigationHandler->orbitalNavigator().anchorNode(); - const Renderable* focusRenderable = focusNode->renderable(); - if (!focusRenderable || focusRenderable->renderedWithDesiredData()) { - return _saveRenderingDeltaTime; - } - else { - return 0; - } -} - -std::chrono::steady_clock::time_point -SessionRecording::currentPlaybackInterpolationTime() const { - return _saveRenderingCurrentRecordedTime_interpolation; -} - -double SessionRecording::currentApplicationInterpolationTime() const { - return _saveRenderingCurrentApplicationTime_interpolation; -} - -bool SessionRecording::playbackCamera() { - Timestamps times; - datamessagestructures::CameraKeyframe kf; - - bool success = readSingleKeyframeCamera( - kf, - times, - _recordingDataMode, - _playbackFile, - _playbackLineParsing, - _playbackLineNum - ); - - const interaction::KeyframeNavigator::CameraPose pbFrame(std::move(kf)); - if (success) { - success = addKeyframe( - {times.timeOs, times.timeRec, times.timeSim}, - pbFrame, - _playbackLineNum - ); - } - return success; -} - -bool SessionRecording::convertCamera(std::stringstream& inStream, DataMode mode, - int lineNum, std::string& inputLine, - std::ofstream& outFile, unsigned char* buffer) -{ - Timestamps times; - datamessagestructures::CameraKeyframe kf; - - const bool success = readSingleKeyframeCamera( - kf, - times, - mode, - reinterpret_cast(inStream), - inputLine, - lineNum - ); - if (success) { - saveSingleKeyframeCamera( - kf, - times, - mode, - outFile, - buffer - ); - } - return success; -} - -bool SessionRecording::readSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, - Timestamps& times, DataMode mode, - std::ifstream& file, std::string& inLine, - const int lineNum) -{ - if (mode == DataMode::Binary) { - return readCameraKeyframeBinary(times, kf, file, lineNum); - } - else { - return readCameraKeyframeAscii(times, kf, inLine, lineNum); - } -} - -void SessionRecording::saveSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, - Timestamps& times, DataMode mode, - std::ofstream& file, - unsigned char* buffer) -{ - if (mode == DataMode::Binary) { - saveCameraKeyframeBinary(times, kf, buffer, file); - } - else { - saveCameraKeyframeAscii(times, kf, file); - } -} - -bool SessionRecording::readCameraKeyframeBinary(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, - std::ifstream& file, int lineN) -{ - times.timeOs = readFromPlayback(file); - times.timeRec = readFromPlayback(file); - times.timeSim = readFromPlayback(file); - - try { - kf.read(&file); - } - catch (std::bad_alloc&) { - LERROR(std::format( - "Allocation error with camera playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - catch (std::length_error&) { - LERROR(std::format( - "length_error with camera playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - - if (!file) { - LINFO(std::format( - "Error reading camera playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - return true; -} - -bool SessionRecording::readCameraKeyframeAscii(Timestamps& times, - datamessagestructures::CameraKeyframe& kf, - const std::string& currentParsingLine, - int lineN) -{ - std::istringstream iss = std::istringstream(currentParsingLine); - std::string entryType; - iss >> entryType; - iss >> times.timeOs >> times.timeRec >> times.timeSim; - kf.read(iss); - // ASCII format does not contain trailing timestamp so add it here - kf._timestamp = times.timeOs; - - if (iss.fail() || !iss.eof()) { - LERROR(std::format("Error parsing camera line {} of playback file", lineN)); - return false; - } - return true; -} - -bool SessionRecording::playbackTimeChange() { - Timestamps times; - datamessagestructures::TimeKeyframe kf; - - bool success = readSingleKeyframeTime( - kf, - times, - _recordingDataMode, - _playbackFile, - _playbackLineParsing, - _playbackLineNum - ); - kf._timestamp = equivalentApplicationTime(times.timeOs, times.timeRec, times.timeSim); - kf._time = kf._timestamp + _timestampApplicationStarted_simulation; - if (success) { - success = addKeyframe( - {times.timeOs, times.timeRec, times.timeSim}, - kf, - _playbackLineNum - ); - } - return success; -} - -bool SessionRecording::convertTimeChange(std::stringstream& inStream, DataMode mode, - int lineNum, std::string& inputLine, - std::ofstream& outFile, unsigned char* buffer) -{ - Timestamps times; - datamessagestructures::TimeKeyframe kf; - - const bool success = readSingleKeyframeTime( - kf, - times, - mode, - reinterpret_cast(inStream), - inputLine, - lineNum - ); - if (success) { - saveSingleKeyframeTime( - kf, - times, - mode, - outFile, - buffer - ); - } - return success; -} - -bool SessionRecording::readSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, - Timestamps& times, DataMode mode, - std::ifstream& file, std::string& inLine, - const int lineNum) -{ - if (mode == DataMode::Binary) { - return readTimeKeyframeBinary(times, kf, file, lineNum); - } else { - return readTimeKeyframeAscii(times, kf, inLine, lineNum); - } -} - -void SessionRecording::saveSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, - Timestamps& times, DataMode mode, - std::ofstream& file, unsigned char* buffer) -{ - if (mode == DataMode::Binary) { - saveTimeKeyframeBinary(times, kf, buffer, file); - } else { - saveTimeKeyframeAscii(times, kf, file); - } -} - -bool SessionRecording::readTimeKeyframeBinary(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, - std::ifstream& file, int lineN) -{ - times.timeOs = readFromPlayback(file); - times.timeRec = readFromPlayback(file); - times.timeSim = readFromPlayback(file); - - try { - kf.read(&file); - } - catch (std::bad_alloc&) { - LERROR(std::format( - "Allocation error with time playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - catch (std::length_error&) { - LERROR(std::format( - "length_error with time playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - - if (!file) { - LERROR(std::format( - "Error reading time playback from keyframe entry {}", lineN - 1 - )); - return false; - } - return true; -} - -bool SessionRecording::readTimeKeyframeAscii(Timestamps& times, - datamessagestructures::TimeKeyframe& kf, - const std::string& currentParsingLine, - int lineN) -{ - std::string entryType; - - std::istringstream iss(currentParsingLine); - iss >> entryType; - iss >> times.timeOs >> times.timeRec >> times.timeSim; - kf.read(iss); - - if (iss.fail() || !iss.eof()) { - LERROR(std::format( - "Error parsing time line {} of playback file", lineN - )); - return false; - } - return true; -} - -std::string SessionRecording::readHeaderElement(std::ifstream& stream, - size_t readLenChars) -{ - std::vector readTemp(readLenChars); - stream.read(readTemp.data(), readLenChars); - return std::string(readTemp.begin(), readTemp.end()); -} - -std::string SessionRecording::readHeaderElement(std::stringstream& stream, - size_t readLenChars) -{ - std::vector readTemp = std::vector(readLenChars); - stream.read(readTemp.data(), readLenChars); - return std::string(readTemp.begin(), readTemp.end()); -} - -bool SessionRecording::playbackScript() { - Timestamps times; - datamessagestructures::ScriptMessage kf; - - bool success = readSingleKeyframeScript( - kf, - times, - _recordingDataMode, - _playbackFile, - _playbackLineParsing, - _playbackLineNum - ); - - checkIfScriptUsesScenegraphNode(kf._script); - - if (success) { - success = addKeyframe( - {times.timeOs, times.timeRec, times.timeSim}, - kf._script, - _playbackLineNum - ); - } - return success; -} - -void SessionRecording::populateListofLoadedSceneGraphNodes() { - const std::vector nodes = - global::renderEngine->scene()->allSceneGraphNodes(); - for (SceneGraphNode* n : nodes) { - _loadedNodes.push_back(n->identifier()); - } -} - -void SessionRecording::checkIfScriptUsesScenegraphNode(std::string s) { - if (s.rfind(scriptReturnPrefix, 0) == 0) { - s.erase(0, scriptReturnPrefix.length()); - } - // This works for both setPropertyValue and setPropertyValueSingle - const bool containsSetPropertyVal = (s.rfind("openspace.setPropertyValue", 0) == 0); - const bool containsParensStart = (s.find('(') != std::string::npos); - if (containsSetPropertyVal && containsParensStart) { - std::string subjectOfSetProp = isolateTermFromQuotes(s.substr(s.find('(') + 1)); - if (checkForScenegraphNodeAccessNav(subjectOfSetProp)) { - const size_t commaPos = s.find(','); - std::string navNode = isolateTermFromQuotes(s.substr(commaPos + 1)); - if (navNode != "nil") { - auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), navNode); - if (it == _loadedNodes.end()) { - LWARNING(std::format( - "Playback file contains a property setting of navigation using " - "scenegraph node '{}', which is not currently loaded", navNode - )); - } - } - } - else if (checkForScenegraphNodeAccessScene(subjectOfSetProp)) { - std::string found = extractScenegraphNodeFromScene(subjectOfSetProp); - if (!found.empty()) { - const std::vector matchHits = - global::renderEngine->scene()->propertiesMatchingRegex( - subjectOfSetProp - ); - if (matchHits.empty()) { - LWARNING(std::format( - "Playback file contains a property setting of scenegraph " - "node '{}', which is not currently loaded", found - )); - } - } - } - } -} - -bool SessionRecording::checkForScenegraphNodeAccessScene(const std::string& s) { - const std::string scene = "Scene."; - return (s.find(scene) != std::string::npos); -} - -std::string SessionRecording::extractScenegraphNodeFromScene(const std::string& s) { - const std::string scene = "Scene."; - std::string extracted; - const size_t posScene = s.find(scene); - if (posScene != std::string::npos) { - const size_t posDot = s.find('.', posScene + scene.length() + 1); - if (posDot > posScene && posDot != std::string::npos) { - extracted = s.substr(posScene + scene.length(), posDot - - (posScene + scene.length())); - } - } - return extracted; -} - -bool SessionRecording::checkForScenegraphNodeAccessNav(std::string& navTerm) { - const std::string nextTerm = "NavigationHandler.OrbitalNavigator."; - const size_t posNav = navTerm.find(nextTerm); - if (posNav != std::string::npos) { - for (const std::string& accessName : _navScriptsUsingNodes) { - if (navTerm.find(accessName) != std::string::npos) { - return true; - } +bool SessionRecording::hasCameraFrame() const noexcept { + for (const Entry& e : entries) { + if (std::holds_alternative(e.value)) { + return true; } } return false; } -std::string SessionRecording::isolateTermFromQuotes(std::string s) { - //Remove any leading spaces - while (s.front() == ' ') { - s.erase(0, 1); +SessionRecording loadSessionRecording(const std::filesystem::path& filename) { + ghoul_assert(std::filesystem::exists(filename), "Session recording did not exist"); + + std::ifstream file = std::ifstream(filename, std::ios::in | std::ios::binary); + if (!file) { + throw LoadingError("Failed to open file", filename); } - const std::string possibleQuotes = "\'\"[]"; - while (possibleQuotes.find(s.front()) != std::string::npos) { - s.erase(0, 1); - } - for (const char q : possibleQuotes) { - if (s.find(q) != std::string::npos) { - s = s.substr(0, s.find(q)); - return s; + + SessionRecording sessionRecording; + + Header header = readHeader(file, filename); + while (true) { + std::optional entry; + try { + entry = readEntry(file, header.dataMode, header.version); } - } - //If no quotes found, remove other possible characters from end - const std::string unwantedChars = " );"; - while (!s.empty() && (unwantedChars.find(s.back()) != std::string::npos)) { - s.pop_back(); - } - return s; + catch (const LoadingError& e) { + const int nEntries = static_cast(sessionRecording.entries.size()); + throw LoadingError(e.error, filename, nEntries + 1); + } + + if (!entry.has_value()) { + // Reached the end of the file + break; + } + + sessionRecording.entries.push_back(std::move(*entry)); + }; + + ghoul_assert( + std::is_sorted( + sessionRecording.entries.begin(), + sessionRecording.entries.end(), + [](const SessionRecording::Entry& lhs, const SessionRecording::Entry& rhs) { + return lhs.timestamp < rhs.timestamp; + } + ), + "Session Recording not sorted by timestamp" + ); + + return sessionRecording; } -void SessionRecording::eraseSpacesFromString(std::string& s) { - s.erase(std::remove_if(s.begin(), s.end(), ::isspace), s.end()); -} +void saveSessionRecording(const std::filesystem::path& filename, + const SessionRecording& sessionRecording, DataMode dataMode) +{ + std::ofstream file = std::ofstream(filename, std::ios::binary); -std::string SessionRecording::getNameFromSurroundingQuotes(std::string& s) { - std::string result; - const char quote = s.at(0); - // Handle either ' or " marks - if (quote == '\'' || quote == '\"') { - const size_t quoteCount = std::count(s.begin(), s.end(), quote); - // Must be an opening and closing quote char - if (quoteCount == 2) { - result = s.substr(1, s.rfind(quote) - 1); + constexpr int CurrentVersion = Versions.back().second; + const Header header = { + .version = CurrentVersion, + .dataMode = dataMode + }; + writeHeader(file, header); + + for (const SessionRecording::Entry& entry : sessionRecording.entries) { + writeEntry(file, entry, dataMode); + + if (dataMode == DataMode::Ascii) { + file.write("\n", sizeof(char)); } } +} + +std::vector sessionRecordingToDictionary( + const SessionRecording& recording) +{ + std::vector result; + for (const SessionRecording::Entry& entry : recording.entries) { + ghoul::Dictionary e; + e.setValue("Timestamp", entry.timestamp); + e.setValue("SimulationTime", entry.simulationTime); + + if (std::holds_alternative(entry.value)) { + const auto& cam = std::get(entry.value); + + ghoul::Dictionary c; + c.setValue("Position", cam.position); + glm::dvec4 q = glm::dvec4( + cam.rotation.w, + cam.rotation.x, + cam.rotation.y, + cam.rotation.z + ); + c.setValue("Rotation", q); + c.setValue("FocusNode", cam.focusNode); + c.setValue("Scale", static_cast(cam.scale)); + c.setValue("FollowFocusNode", cam.followFocusNodeRotation); + e.setValue("Camera", std::move(c)); + } + else if (std::holds_alternative(entry.value)) { + const std::string& s = std::get(entry.value); + e.setValue("Script", s); + } + + result.push_back(e); + } + return result; } -bool SessionRecording::checkIfInitialFocusNodeIsLoaded(unsigned int firstCamIndex) { - if (_keyframesCamera.empty()) { - return true; - } - - std::string startFocusNode = - _keyframesCamera[_timeline[firstCamIndex].idxIntoKeyframeTypeArray].focusNode; - auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), startFocusNode); - if (it == _loadedNodes.end()) { - LERROR(std::format( - "Playback file requires scenegraph node '{}', which is " - "not currently loaded", startFocusNode - )); - return false; - } - return true; -} - - -bool SessionRecording::convertScript(std::stringstream& inStream, DataMode mode, - int lineNum, std::string& inputLine, - std::ofstream& outFile, unsigned char* buffer) -{ - Timestamps times; - datamessagestructures::ScriptMessage kf; - - const bool success = readSingleKeyframeScript( - kf, - times, - mode, - reinterpret_cast(inStream), - inputLine, - lineNum - ); - if (success) { - saveSingleKeyframeScript( - kf, - times, - mode, - outFile, - buffer - ); - } - return success; -} - -bool SessionRecording::readSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, - Timestamps& times, DataMode mode, - std::ifstream& file, std::string& inLine, - const int lineNum) -{ - if (mode == DataMode::Binary) { - return readScriptKeyframeBinary(times, kf, file, lineNum); - } - else { - return readScriptKeyframeAscii(times, kf, inLine, lineNum); - } -} - -void SessionRecording::saveSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, - Timestamps& times, DataMode mode, - std::ofstream& file, - unsigned char* buffer) -{ - if (mode == DataMode::Binary) { - saveScriptKeyframeBinary(times, kf, buffer, file); - } - else { - saveScriptKeyframeAscii(times, kf, file); - } -} - -bool SessionRecording::readScriptKeyframeBinary(Timestamps& times, - datamessagestructures::ScriptMessage& kf, - std::ifstream& file, int lineN) -{ - times.timeOs = readFromPlayback(file); - times.timeRec = readFromPlayback(file); - times.timeSim = readFromPlayback(file); - - try { - kf.read(&file); - } - catch (std::bad_alloc&) { - LERROR(std::format( - "Allocation error with script playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - catch (std::length_error&) { - LERROR(std::format( - "length_error with script playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - - if (!file) { - LERROR(std::format( - "Error reading script playback from keyframe entry {}", - lineN - 1 - )); - return false; - } - return true; -} - -bool SessionRecording::readScriptKeyframeAscii(Timestamps& times, - datamessagestructures::ScriptMessage& kf, - const std::string& currentParsingLine, - int lineN) -{ - std::string entryType; - std::istringstream iss(currentParsingLine); - iss >> entryType; - iss >> times.timeOs >> times.timeRec >> times.timeSim; - kf.read(iss); - if (iss.fail()) { - LERROR(std::format("Error parsing script line {} of playback file", lineN)); - return false; - } - else if (!iss.eof()) { - LERROR(std::format("Did not find an EOL at line {} of playback file", lineN)); - return false; - } - return true; -} - -bool SessionRecording::addKeyframeToTimeline(std::vector& timeline, - RecordedType type, - size_t indexIntoTypeKeyframes, - Timestamps t3stamps, int lineNum) -{ - try { - timeline.push_back({ - type, - static_cast(indexIntoTypeKeyframes), - t3stamps - }); - } - catch(...) { - LERROR(std::format( - "Timeline memory allocation error trying to add keyframe {}. The playback " - "file may be too large for system memory", - lineNum - 1 - )); - return false; - } - return true; -} - -bool SessionRecording::addKeyframe(Timestamps t3stamps, - interaction::KeyframeNavigator::CameraPose keyframe, - int lineNum) -{ - const size_t indexIntoCameraKeyframesFromMainTimeline = _keyframesCamera.size(); - _keyframesCamera.push_back(std::move(keyframe)); - return addKeyframeToTimeline( - _timeline, - RecordedType::Camera, - indexIntoCameraKeyframesFromMainTimeline, - t3stamps, - lineNum - ); -} - -bool SessionRecording::addKeyframe(Timestamps t3stamps, - datamessagestructures::TimeKeyframe keyframe, - int lineNum) -{ - const size_t indexIntoTimeKeyframesFromMainTimeline = _keyframesTime.size(); - _keyframesTime.push_back(std::move(keyframe)); - return addKeyframeToTimeline( - _timeline, - RecordedType::Time, - indexIntoTimeKeyframesFromMainTimeline, - t3stamps, - lineNum - ); -} - -bool SessionRecording::addKeyframe(Timestamps t3stamps, - std::string scriptToQueue, - int lineNum) -{ - const size_t indexIntoScriptKeyframesFromMainTimeline = _keyframesScript.size(); - _keyframesScript.push_back(std::move(scriptToQueue)); - return addKeyframeToTimeline( - _timeline, - RecordedType::Script, - indexIntoScriptKeyframesFromMainTimeline, - t3stamps, - lineNum - ); -} - -void SessionRecording::moveAheadInTime() { - using namespace std::chrono; - - const bool playbackPaused = (_state == SessionState::PlaybackPaused); - if (playbackPaused) { - _playbackPauseOffset - += global::windowDelegate->applicationTime() - _previousTime; - } - _previousTime = global::windowDelegate->applicationTime(); - - const double currTime = currentTime(); - lookForNonCameraKeyframesThatHaveComeDue(currTime); - updateCameraWithOrWithoutNewKeyframes(currTime); - // Unfortunately the first frame is sometimes rendered because globebrowsing reports - // that all chunks are rendered when they apparently are not. - if (_saveRendering_isFirstFrame) { - _saveRendering_isFirstFrame = false; - return; - } - if (_shouldWaitForFinishLoadingWhenPlayback && isSavingFramesDuringPlayback()) { - // Check if renderable in focus is still resolving tile loading - // do not adjust time while we are doing this, or take screenshot - const SceneGraphNode* focusNode = - global::navigationHandler->orbitalNavigator().anchorNode(); - const Renderable* focusRenderable = focusNode->renderable(); - if (!focusRenderable || focusRenderable->renderedWithDesiredData()) { - if (!playbackPaused) { - _saveRenderingCurrentRecordedTime_interpolation += - _saveRenderingDeltaTime_interpolation_usec; - _saveRenderingCurrentRecordedTime += _saveRenderingDeltaTime; - _saveRenderingCurrentApplicationTime_interpolation += - _saveRenderingDeltaTime; - global::renderEngine->takeScreenshot(); - } - } - } -} - -void SessionRecording::lookForNonCameraKeyframesThatHaveComeDue(double currTime) { - while (isTimeToHandleNextNonCameraKeyframe(currTime)) { - if (!processNextNonCameraKeyframeAheadInTime()) { - break; - } - - if (++_idxTimeline_nonCamera >= _timeline.size()) { - _idxTimeline_nonCamera--; - if (_playbackActive_time) { - signalPlaybackFinishedForComponent(RecordedType::Time); - } - if (_playbackActive_script) { - signalPlaybackFinishedForComponent(RecordedType::Script); - } - break; - } - } -} - -void SessionRecording::updateCameraWithOrWithoutNewKeyframes(double currTime) { - if (!_playbackActive_camera) { - return; - } - - const bool didFindFutureCameraKeyframes = findNextFutureCameraIndex(currTime); - const bool isPrevAtFirstKeyframe = - (_idxTimeline_cameraPtrPrev == _idxTimeline_cameraFirstInTimeline); - const bool isFirstTimelineCameraKeyframeInFuture = - (currTime < _cameraFirstInTimeline_timestamp); - - if (! (isPrevAtFirstKeyframe && isFirstTimelineCameraKeyframeInFuture)) { - processCameraKeyframe(currTime); - } - if (!didFindFutureCameraKeyframes) { - signalPlaybackFinishedForComponent(RecordedType::Camera); - } -} - -bool SessionRecording::isTimeToHandleNextNonCameraKeyframe(double currTime) { - const bool nonCameraPlaybackActive = (_playbackActive_time || _playbackActive_script); - return (currTime > getNextTimestamp()) && nonCameraPlaybackActive; -} - -bool SessionRecording::findNextFutureCameraIndex(double currTime) { - unsigned int seekAheadIndex = _idxTimeline_cameraPtrPrev; - while (true) { - seekAheadIndex++; - if (seekAheadIndex >= static_cast(_timeline.size())) { - seekAheadIndex = static_cast(_timeline.size()) - 1; - } - - if (doesTimelineEntryContainCamera(seekAheadIndex)) { - const unsigned int indexIntoCameraKeyframes = - _timeline[seekAheadIndex].idxIntoKeyframeTypeArray; - const double seekAheadKeyframeTimestamp - = appropriateTimestamp(_timeline[seekAheadIndex].t3stamps); - - if (indexIntoCameraKeyframes >= (_keyframesCamera.size() - 1)) { - _hasHitEndOfCameraKeyframes = true; - } - - if (currTime < seekAheadKeyframeTimestamp) { - if (seekAheadIndex > _idxTimeline_cameraPtrNext) { - _idxTimeline_cameraPtrPrev = _idxTimeline_cameraPtrNext; - _idxTimeline_cameraPtrNext = seekAheadIndex; - } - break; - } - else { - // Force interpolation between consecutive keyframes - _idxTimeline_cameraPtrPrev = seekAheadIndex; - } - } - - const double interpolationUpperBoundTimestamp = - appropriateTimestamp(_timeline[_idxTimeline_cameraPtrNext].t3stamps); - if ((currTime > interpolationUpperBoundTimestamp) && _hasHitEndOfCameraKeyframes) - { - _idxTimeline_cameraPtrPrev = _idxTimeline_cameraPtrNext; - return false; - } - - if (seekAheadIndex == (_timeline.size() - 1)) { - break; - } - } - return true; -} - -bool SessionRecording::doesTimelineEntryContainCamera(unsigned int index) const { - return (_timeline[index].keyframeType == RecordedType::Camera); -} - -bool SessionRecording::processNextNonCameraKeyframeAheadInTime() { - switch (getNextKeyframeType()) { - case RecordedType::Camera: - // Just return true since this function no longer handles camera keyframes - return true; - case RecordedType::Time: - _idxTime = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; - if (_keyframesTime.empty()) { - return false; - } - LINFO("Time keyframe type"); - // TBD: the TimeManager restricts setting time directly - return false; - case RecordedType::Script: - _idxScript = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; - return processScriptKeyframe(); - default: - LERROR(std::format( - "Bad keyframe type encountered during playback at index {}", - _idxTimeline_nonCamera - )); - return false; - } -} - -//void SessionRecording::moveBackInTime() { } //for future use - -unsigned int SessionRecording::findIndexOfLastCameraKeyframeInTimeline() { - unsigned int i = static_cast(_timeline.size()) - 1; - for (; i > 0; i--) { - if (_timeline[i].keyframeType == RecordedType::Camera) { - break; - } - } - return i; -} - -bool SessionRecording::processCameraKeyframe(double now) { - interaction::KeyframeNavigator::CameraPose nextPose; - interaction::KeyframeNavigator::CameraPose prevPose; - - unsigned int prevIdx = 0; - unsigned int nextIdx = 0; - if (!_playbackActive_camera) { - return false; - } - else if (_keyframesCamera.empty()) { - return false; - } - else { - prevIdx = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; - prevPose = _keyframesCamera[prevIdx]; - nextIdx = _timeline[_idxTimeline_cameraPtrNext].idxIntoKeyframeTypeArray; - nextPose = _keyframesCamera[nextIdx]; - } - - // getPrevTimestamp(); - const double prevTime = appropriateTimestamp( - _timeline[_idxTimeline_cameraPtrPrev].t3stamps - ); - // getNextTimestamp(); - const double nextTime = appropriateTimestamp( - _timeline[_idxTimeline_cameraPtrNext].t3stamps - ); - - double t = 0.0; - if ((nextTime - prevTime) >= 1e-7) { - t = (now - prevTime) / (nextTime - prevTime); - } - -#ifdef INTERPOLATION_DEBUG_PRINT - LINFOC("prev", std::to_string(prevTime)); - LINFOC("now", std::to_string(prevTime + t)); - LINFOC("next", std::to_string(nextTime)); -#endif - - // Need to activly update the focusNode position of the camera in relation to - // the rendered objects will be unstable and actually incorrect - Camera* camera = global::navigationHandler->camera(); - Scene* scene = camera->parent()->scene(); - - const SceneGraphNode* n = scene->sceneGraphNode(_keyframesCamera[prevIdx].focusNode); - if (n) { - global::navigationHandler->orbitalNavigator().setFocusNode(n->identifier()); - } - - interaction::KeyframeNavigator::updateCamera( - global::navigationHandler->camera(), - prevPose, - nextPose, - t, - _ignoreRecordedScale - ); - return true; -} - -bool SessionRecording::processScriptKeyframe() { - if (!_playbackActive_script || _keyframesScript.empty()) { - return false; - } - - const std::string nextScript = nextKeyframeObj( - _idxScript, - _keyframesScript, - ([this]() { signalPlaybackFinishedForComponent(RecordedType::Script); }) - ); - global::scriptEngine->queueScript(nextScript); - - return true; -} - -double SessionRecording::getNextTimestamp() { - if (_timeline.empty()) { - return 0.0; - } - else if (_idxTimeline_nonCamera < _timeline.size()) { - return appropriateTimestamp(_timeline[_idxTimeline_nonCamera].t3stamps); - } - else { - return appropriateTimestamp(_timeline.back().t3stamps); - } -} - -double SessionRecording::getPrevTimestamp() { - if (_timeline.empty()) { - return 0.0; - } - else if (_idxTimeline_nonCamera == 0) { - return appropriateTimestamp(_timeline.front().t3stamps); - } - else if (_idxTimeline_nonCamera < _timeline.size()) { - return appropriateTimestamp(_timeline[_idxTimeline_nonCamera - 1].t3stamps); - } - else { - return appropriateTimestamp(_timeline.back().t3stamps); - } -} - -SessionRecording::RecordedType SessionRecording::getNextKeyframeType() { - if (_timeline.empty()) { - return RecordedType::Invalid; - } - else if (_idxTimeline_nonCamera < _timeline.size()) { - return _timeline[_idxTimeline_nonCamera].keyframeType; - } - else { - return _timeline.back().keyframeType; - } -} - -SessionRecording::RecordedType SessionRecording::getPrevKeyframeType() { - if (_timeline.empty()) { - return RecordedType::Invalid; - } - else if (_idxTimeline_nonCamera < _timeline.size()) { - if (_idxTimeline_nonCamera > 0) { - return _timeline[_idxTimeline_nonCamera - 1].keyframeType; - } - else { - return _timeline.front().keyframeType; - } - } - else { - return _timeline.back().keyframeType; - } -} - -void SessionRecording::saveKeyframeToFileBinary(unsigned char* buffer, - size_t size, - std::ofstream& file) -{ - file.write(reinterpret_cast(buffer), size); -} - -void SessionRecording::saveKeyframeToFile(const std::string& entry, std::ofstream& file) { - file << entry << '\n'; -} - -SessionRecording::CallbackHandle SessionRecording::addStateChangeCallback( - StateChangeCallback cb) -{ - const CallbackHandle handle = _nextCallbackHandle++; - _stateChangeCallbacks.emplace_back(handle, std::move(cb)); - return handle; -} - -void SessionRecording::removeStateChangeCallback(CallbackHandle handle) { - const auto it = std::find_if( - _stateChangeCallbacks.begin(), - _stateChangeCallbacks.end(), - [handle](const std::pair>& cb) { - return cb.first == handle; - } - ); - - ghoul_assert( - it != _stateChangeCallbacks.end(), - "handle must be a valid callback handle" - ); - - _stateChangeCallbacks.erase(it); -} - -std::vector SessionRecording::playbackList() const { - const std::filesystem::path path = absPath("${RECORDINGS}"); - - std::vector fileList; - if (std::filesystem::is_directory(path)) { - namespace fs = std::filesystem; - for (const fs::directory_entry& e : fs::directory_iterator(path)) { - if (!e.is_regular_file()) { - continue; - } - - // Remove path and keep only the filename - const std::string filename = e.path().filename().string(); -#ifdef WIN32 - DWORD attributes = GetFileAttributes(e.path().string().c_str()); - bool isHidden = attributes & FILE_ATTRIBUTE_HIDDEN; -#else - const bool isHidden = filename.find('.') == 0; -#endif // WIN32 - if (!isHidden) { - // Don't add hidden files - fileList.push_back(filename); - } - } - } - std::sort(fileList.begin(), fileList.end()); - return fileList; -} - -void SessionRecording::readPlaybackHeader_stream(std::stringstream& conversionInStream, - std::string& version, DataMode& mode) -{ - // Read header - const std::string readBackHeaderString = readHeaderElement( - conversionInStream, - FileHeaderTitle.length() - ); - - if (readBackHeaderString != FileHeaderTitle) { - throw ConversionError("File to convert does not contain expected header"); - } - version = readHeaderElement(conversionInStream, FileHeaderVersionLength); - std::string readDataMode = readHeaderElement(conversionInStream, 1); - if (readDataMode[0] == DataFormatAsciiTag) { - mode = DataMode::Ascii; - } - else if (readDataMode[0] == DataFormatBinaryTag) { - mode = DataMode::Binary; - } - else { - throw ConversionError("Unknown data type in header (needs Ascii or Binary)"); - } - // Read to throw out newline at end of header - readHeaderElement(conversionInStream, 1); -} - -SessionRecording::DataMode SessionRecording::readModeFromHeader( - const std::string& filename) -{ - DataMode mode = DataMode::Unknown; - std::ifstream inputFile; - // Open in ASCII first - inputFile.open(filename, std::ifstream::in); - // Read header - const std::string readBackHeaderString = readHeaderElement( - inputFile, - FileHeaderTitle.length() - ); - if (readBackHeaderString != FileHeaderTitle) { - LERROR("Specified playback file does not contain expected header"); - } - readHeaderElement(inputFile, FileHeaderVersionLength); - std::string readDataMode = readHeaderElement(inputFile, 1); - if (readDataMode[0] == DataFormatAsciiTag) { - mode = DataMode::Ascii; - } - else if (readDataMode[0] == DataFormatBinaryTag) { - mode = DataMode::Binary; - } - else { - throw ConversionError("Unknown data type in header (should be Ascii or Binary)"); - } - return mode; -} - -void SessionRecording::readFileIntoStringStream(std::string filename, - std::ifstream& inputFstream, - std::stringstream& stream) -{ - std::filesystem::path conversionInFilename = absPath(filename); - if (!std::filesystem::is_regular_file(conversionInFilename)) { - throw ConversionError(std::format( - "Cannot find the specified playback file '{}' to convert", - conversionInFilename - )); - } - - const DataMode mode = readModeFromHeader(conversionInFilename.string()); - - stream.str(""); - stream.clear(); - inputFstream.close(); - if (mode == DataMode::Binary) { - inputFstream.open(conversionInFilename, std::ifstream::in | std::ios::binary); - } - else { - inputFstream.open(conversionInFilename, std::ifstream::in); - } - stream << inputFstream.rdbuf(); - if (!inputFstream.is_open() || !inputFstream.good()) { - throw ConversionError(std::format( - "Unable to open file '{}' for conversion", filename - )); - } - inputFstream.close(); -} - -void SessionRecording::convertFileRelativePath(std::string filenameRelative) { - const std::filesystem::path path = absPath(std::move(filenameRelative)); - convertFile(path.string()); -} - -std::string SessionRecording::convertFile(std::string filename, int depth) { - std::string conversionOutFilename = filename; - std::ifstream conversionInFile; - std::stringstream conversionInStream; - if (depth >= _maximumRecursionDepth) { - LERROR("Runaway recursion in session recording conversion of file version"); - exit(EXIT_FAILURE); - } - std::string newFilename = filename; - try { - readFileIntoStringStream(filename, conversionInFile, conversionInStream); - DataMode mode = DataMode::Unknown; - std::string fileVersion; - readPlaybackHeader_stream( - conversionInStream, - fileVersion, - mode - ); - const int conversionLineNum = 1; - - // If this instance of the SessionRecording class isn't the instance with the - // correct version of the file to be converted, then call getLegacy() to recurse - // to the next level down in the legacy subclasses until we get the right - // version, then proceed with conversion from there. - if (fileVersion != fileFormatVersion()) { - //conversionInStream.seekg(conversionInStream.beg); - newFilename = getLegacyConversionResult(filename, depth + 1); - removeTrailingPathSlashes(newFilename); - if (filename == newFilename) { - return filename; - } - readFileIntoStringStream(newFilename, conversionInFile, conversionInStream); - readPlaybackHeader_stream( - conversionInStream, - fileVersion, - mode - ); - } - if (depth != 0) { - conversionOutFilename = determineConversionOutFilename(filename, mode); - LINFO(std::format( - "Starting conversion on rec file '{}', version {} in {} mode. " - "Writing result to '{}'", - newFilename, fileVersion, (mode == DataMode::Ascii) ? "ascii" : "binary", - conversionOutFilename - )); - std::ofstream conversionOutFile; - if (mode == DataMode::Binary) { - conversionOutFile.open(conversionOutFilename, std::ios::binary); - } - else { - conversionOutFile.open(conversionOutFilename); - } - if (!conversionOutFile.is_open() || !conversionOutFile.good()) { - LERROR(std::format( - "Unable to open file '{}' for conversion result", - conversionOutFilename - )); - return ""; - } - conversionOutFile << FileHeaderTitle; - conversionOutFile.write( - targetFileFormatVersion().c_str(), - FileHeaderVersionLength - ); - if (mode == DataMode::Binary) { - conversionOutFile << DataFormatBinaryTag; - } - else { - conversionOutFile << DataFormatAsciiTag; - } - conversionOutFile << '\n'; - convertEntries( - newFilename, - conversionInStream, - mode, - conversionLineNum, - conversionOutFile - ); - conversionOutFile.close(); - } - conversionInFile.close(); - } - catch (ConversionError& c) { - LERROR(c.message); - } - - if (depth == 0) { - return newFilename; - } - else { - return conversionOutFilename; - } -} - -bool SessionRecording::convertEntries(std::string& inFilename, - std::stringstream& inStream, DataMode mode, - int lineNum, std::ofstream& outFile) -{ - bool conversionStatusOk = true; - std::string lineParsing; - - if (mode == DataMode::Binary) { - while (conversionStatusOk) { - unsigned char frameType = readFromPlayback(inStream); - // Check if have reached EOF - if (!inStream) { - LINFO(std::format( - "Finished converting {} entries from playback file '{}'", - lineNum - 1, inFilename - )); - break; - } - if (frameType == HeaderCameraBinary) { - conversionStatusOk = convertCamera( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - else if (frameType == HeaderTimeBinary) { - conversionStatusOk = convertTimeChange( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - else if (frameType == HeaderScriptBinary) { - try { - conversionStatusOk = convertScript( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - catch (ConversionError& c) { - LERROR(c.message); - conversionStatusOk = false; - } - } - else { - LERROR(std::format( - "Unknown frame type {} @ index {} of conversion file '{}'", - frameType, lineNum - 1, inFilename - )); - conversionStatusOk = false; - } - lineNum++; - } - } - else { - while (conversionStatusOk && ghoul::getline(inStream, lineParsing)) { - lineNum++; - - std::istringstream iss(lineParsing); - std::string entryType; - if (!(iss >> entryType)) { - LERROR(std::format( - "Error reading entry type @ line {} of conversion file '{}'", - lineNum, inFilename - )); - break; - } - - if (entryType == HeaderCameraAscii) { - conversionStatusOk = convertCamera( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - else if (entryType == HeaderTimeAscii) { - conversionStatusOk = convertTimeChange( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - else if (entryType == HeaderScriptAscii) { - try { - conversionStatusOk = convertScript( - inStream, - mode, - lineNum, - lineParsing, - outFile, - _keyframeBuffer - ); - } - catch (ConversionError& c) { - LERROR(c.message); - conversionStatusOk = false; - } - } - else if (entryType.substr(0, 1) == HeaderCommentAscii) { - continue; - } - else { - LERROR(std::format( - "Unknown frame type {} @ line {} of conversion file '{}'", - entryType, lineNum, inFilename - )); - conversionStatusOk = false; - } - } - LINFO(std::format( - "Finished parsing {} entries from conversion file '{}'", - lineNum, inFilename - )); - } - return conversionStatusOk; -} - -std::string SessionRecording::getLegacyConversionResult(std::string filename, int depth) { - SessionRecording_legacy_0085 legacy; - return legacy.convertFile(std::move(filename), depth); -} - -std::string SessionRecording_legacy_0085::getLegacyConversionResult(std::string filename, - int) -{ - // This method is overriden in each legacy subclass, but does nothing in this instance - // as the oldest supported legacy version. - LERROR(std::format( - "Version 00.85 is the oldest supported legacy file format; no conversion " - "can be made. It is possible that file '{}' has a corrupted header or an invalid " - "file format version number", - filename - )); - return filename; -} - -std::string SessionRecording::fileFormatVersion() { - return std::string(FileHeaderVersion); -} - -std::string SessionRecording::targetFileFormatVersion() { - return std::string(FileHeaderVersion); -} - -std::string SessionRecording::determineConversionOutFilename(const std::string& filename, - DataMode mode) -{ - std::string filenameSansExtension = filename; - const std::string fileExtension = (mode == DataMode::Binary) ? - FileExtensionBinary : FileExtensionAscii; - - if (filename.find_last_of('.') != std::string::npos) { - filenameSansExtension = filename.substr(0, filename.find_last_of('.')); - } - filenameSansExtension += "_" + fileFormatVersion() + "-" + targetFileFormatVersion(); - return filenameSansExtension + fileExtension; -} - -bool SessionRecording_legacy_0085::convertScript(std::stringstream& inStream, - DataMode mode, int lineNum, - std::string& inputLine, - std::ofstream& outFile, - unsigned char* buffer) -{ - Timestamps times; - ScriptMessage_legacy_0085 kf; - - const bool success = readSingleKeyframeScript( - kf, - times, - mode, - reinterpret_cast(inStream), - inputLine, - lineNum - ); - if (success) { - saveSingleKeyframeScript(kf, times, mode, outFile, buffer); - } - return success; -} - -scripting::LuaLibrary SessionRecording::luaLibrary() { - return { - "sessionRecording", - { - codegen::lua::StartRecording, - codegen::lua::StartRecordingAscii, - codegen::lua::StopRecording, - codegen::lua::StartPlaybackDefault, - codegen::lua::StartPlaybackApplicationTime, - codegen::lua::StartPlaybackRecordedTime, - codegen::lua::StartPlaybackSimulationTime, - codegen::lua::StopPlayback, - codegen::lua::EnableTakeScreenShotDuringPlayback, - codegen::lua::DisableTakeScreenShotDuringPlayback, - codegen::lua::FileFormatConversion, - codegen::lua::SetPlaybackPause, - codegen::lua::TogglePlaybackPause, - codegen::lua::IsPlayingBack, - codegen::lua::IsRecording - } - }; -} - } // namespace openspace::interaction diff --git a/src/interaction/sessionrecording_lua.inl b/src/interaction/sessionrecording_lua.inl deleted file mode 100644 index 1fb6164865..0000000000 --- a/src/interaction/sessionrecording_lua.inl +++ /dev/null @@ -1,212 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -namespace { - -/** - * Starts a recording session. The string argument is the filename used for the file where - * the recorded keyframes are saved. The file data format is binary. - */ -[[codegen::luawrap]] void startRecording(std::string recordFilePath) { - using namespace openspace; - - if (recordFilePath.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->setRecordDataFormat( - interaction::SessionRecording::DataMode::Binary - ); - global::sessionRecording->startRecording(recordFilePath); -} - -/** - * Starts a recording session. The string argument is the filename used for the file where - * the recorded keyframes are saved. The file data format is ASCII. - */ -[[codegen::luawrap]] void startRecordingAscii(std::string recordFilePath) { - using namespace openspace; - - if (recordFilePath.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->setRecordDataFormat( - interaction::SessionRecording::DataMode::Ascii - ); - global::sessionRecording->startRecording(recordFilePath); -} - -// Stops a recording session. -[[codegen::luawrap]] void stopRecording() { - openspace::global::sessionRecording->stopRecording(); -} - -/** - * Starts a playback session with keyframe times that are relative to the time since the - * recording was started (the same relative time applies to the playback). When playback - * starts, the simulation time is automatically set to what it was at recording time. The - * string argument is the filename to pull playback keyframes from (the file path is - * relative to the RECORDINGS variable specified in the config file). If a second input - * value of true is given, then playback will continually loop until it is manually - * stopped. - */ -[[codegen::luawrap("startPlayback")]] void startPlaybackDefault(std::string file, - bool loop = false, - bool shouldWaitForTiles = true) -{ - using namespace openspace; - - if (file.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->startPlayback( - file, - interaction::KeyframeTimeRef::Relative_recordedStart, - true, - loop, - shouldWaitForTiles - ); -} - -/** - * Starts a playback session with keyframe times that are relative to application time - * (seconds since OpenSpace application started). The string argument is the filename to - * pull playback keyframes from (the file path is relative to the RECORDINGS variable - * specified in the config file). - */ -[[codegen::luawrap]] void startPlaybackApplicationTime(std::string file) { - using namespace openspace; - - if (file.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->startPlayback( - file, - interaction::KeyframeTimeRef::Relative_applicationStart, - false, - false, - false - ); -} - -/** - * Starts a playback session with keyframe times that are relative to the time since the - * recording was started (the same relative time applies to the playback). The string - * argument is the filename to pull playback keyframes from (the file path is relative to - * the RECORDINGS variable specified in the config file). If a second input value of true - * is given, then playback will continually loop until it is manually stopped. - */ -[[codegen::luawrap]] void startPlaybackRecordedTime(std::string file, bool loop = false) { - using namespace openspace; - - if (file.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->startPlayback( - file, - interaction::KeyframeTimeRef::Relative_recordedStart, - false, - loop, - false - ); -} - -/** - * Starts a playback session with keyframe times that are relative to the simulated date & - * time. The string argument is the filename to pull playback keyframes from (the file - * path is relative to the RECORDINGS variable specified in the config file). - */ -[[codegen::luawrap]] void startPlaybackSimulationTime(std::string file) { - using namespace openspace; - - if (file.empty()) { - throw ghoul::lua::LuaError("Filepath string is empty"); - } - global::sessionRecording->startPlayback( - file, - interaction::KeyframeTimeRef::Absolute_simTimeJ2000, - false, - false, - false - ); -} - -// Stops a playback session before playback of all keyframes is complete. -[[codegen::luawrap]] void stopPlayback() { - openspace::global::sessionRecording->stopPlayback(); -} - -/** - * Enables that rendered frames should be saved during playback. The parameter determines - * the number of frames that are exported per second if this value is not provided, 60 - * frames per second will be exported. - */ -[[codegen::luawrap]] void enableTakeScreenShotDuringPlayback(int fps = 60) { - openspace::global::sessionRecording->enableTakeScreenShotDuringPlayback(fps); -} - -// Used to disable that renderings are saved during playback. -[[codegen::luawrap]] void disableTakeScreenShotDuringPlayback() { - openspace::global::sessionRecording->disableTakeScreenShotDuringPlayback(); -} - -/** - * Performs a conversion of the specified file to the most most recent file format, - * creating a copy of the recording file. - */ -[[codegen::luawrap]] void fileFormatConversion(std::string convertFilePath) { - if (convertFilePath.empty()) { - throw ghoul::lua::LuaError("Filepath string must not be empty"); - } - openspace::global::sessionRecording->convertFile(convertFilePath); -} - -// Pauses or resumes the playback progression through keyframes. -[[codegen::luawrap]] void setPlaybackPause(bool pause) { - openspace::global::sessionRecording->setPlaybackPause(pause); -} - -/** - * Toggles the pause function, i.e. temporarily setting the delta time to 0 and restoring - * it afterwards. - */ -[[codegen::luawrap]] void togglePlaybackPause() { - using namespace openspace; - - bool isPlaybackPaused = global::sessionRecording->isPlaybackPaused(); - global::sessionRecording->setPlaybackPause(!isPlaybackPaused); -} - -// Returns true if session recording is currently playing back a recording. -[[codegen::luawrap]] bool isPlayingBack() { - return openspace::global::sessionRecording->isPlayingBack(); -} - -// Returns true if session recording is currently recording a recording. -[[codegen::luawrap]] bool isRecording() { - return openspace::global::sessionRecording->isRecording(); -} - -#include "sessionrecording_lua_codegen.cpp" - -} // namespace diff --git a/src/interaction/sessionrecordinghandler.cpp b/src/interaction/sessionrecordinghandler.cpp new file mode 100644 index 0000000000..fd50725b52 --- /dev/null +++ b/src/interaction/sessionrecordinghandler.cpp @@ -0,0 +1,786 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#endif // WIN32 + +#include "sessionrecordinghandler_lua.inl" + +namespace { + constexpr std::string_view _loggerCat = "SessionRecording"; + + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; + + constexpr openspace::properties::Property::PropertyInfo RenderPlaybackInfo = { + "RenderInfo", + "Render Playback Information", + "If enabled, information about a currently played back session recording is " + "rendering to screen.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo IgnoreRecordedScaleInfo = { + "IgnoreRecordedScale", + "Ignore Recorded Scale", + "If this value is enabled, the scale value from a recording is ignored and the " + "computed values are used instead.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo AddModelMatrixinAsciiInfo = { + "AddModelMatrixinAscii", + "Add Model Matrix in ASCII recording", + "If this is 'true', the model matrix is written into the ASCII recording format " + "in the line before each camera keyframe. The model matrix is the full matrix " + "that converts the position into a J2000+Galactic reference frame.", + openspace::properties::Property::Visibility::Developer + }; + + const std::string FileExtensionBinary = ".osrec"; + const std::string FileExtensionAscii = ".osrectxt"; + + constexpr std::string_view ScriptReturnPrefix = "return "; +} // namespace + +namespace openspace::interaction { + +SessionRecordingHandler::SessionRecordingHandler() + : properties::PropertyOwner({ "SessionRecording", "Session Recording" }) + , _renderPlaybackInformation(RenderPlaybackInfo, false) + , _ignoreRecordedScale(IgnoreRecordedScaleInfo, false) + , _addModelMatrixinAscii(AddModelMatrixinAsciiInfo, false) +{ + addProperty(_renderPlaybackInformation); + addProperty(_ignoreRecordedScale); + addProperty(_addModelMatrixinAscii); +} + +void SessionRecordingHandler::preSynchronization(double dt) { + ZoneScoped; + + if (_state == SessionState::Recording) { + tickRecording(dt); + } + else if (isPlayingBack()) { + tickPlayback(dt); + } + + // Handle callback(s) for change in idle/record/playback state + if (_state != _lastState) { + using K = CallbackHandle; + using V = StateChangeCallback; + for (const std::pair& it : _stateChangeCallbacks) { + it.second(); + } + _lastState = _state; + } +} + +void SessionRecordingHandler::tickPlayback(double dt) { + if (_state == SessionState::PlaybackPaused) { + return; + } + + const double previousTime = _playback.elapsedTime; + _playback.elapsedTime += dt; + + // Find the first value whose recording time is past now + std::vector::const_iterator probe = _currentEntry; + while (probe != _timeline.entries.end() && _playback.elapsedTime > probe->timestamp) { + probe++; + } + + // All script entries between _previous and now have to be applied + for (auto& it = _currentEntry; it != probe; it++) { + if (std::holds_alternative(it->value)) { + global::scriptEngine->queueScript( + std::get(it->value) + ); + } + } + + // ... < _previous < ... < prevCamera <= now <= nextCamera < ... + std::vector::const_iterator prevCamera = probe - 1; + while (prevCamera != _timeline.entries.begin() && + !std::holds_alternative(prevCamera->value)) + { + prevCamera--; + } + // We need to check this here as there might be a chance that the first camera entry + // is after the current elapsed time, which would result in `prevCamera` being equal + // to `begin()` but not being a camera keyframe + const bool hasValidPrevCamera = + std::holds_alternative(prevCamera->value); + + std::vector::const_iterator nextCamera = probe; + while (nextCamera != _timeline.entries.end() && + !std::holds_alternative(nextCamera->value)) + { + nextCamera++; + } + // Same argument but in reverse from the `hasValidPrevCamera` comment + const bool hasValidNextCamera = + (nextCamera != _timeline.entries.end()) && + std::holds_alternative(nextCamera->value); + + + if (!hasValidPrevCamera && !hasValidNextCamera) { + throw ghoul::RuntimeError("No valid camera keyframes found in recording"); + } + + // update camera with or without new keyframes + const auto& prevPose = + hasValidPrevCamera ? + std::get(prevCamera->value) : + std::get(nextCamera->value); + const double prevTime = + hasValidPrevCamera ? + prevCamera->timestamp : + 0.0; + + const auto& nextPose = + hasValidNextCamera ? + std::get(nextCamera->value) : + std::get(prevCamera->value); + const double nextTime = + hasValidNextCamera ? + nextCamera->timestamp : + _timeline.entries.back().timestamp; + + // Need to actively update the focusNode position of the camera in relation to + // the rendered objects will be unstable and actually incorrect + const SceneGraphNode* n = sceneGraphNode(prevPose.focusNode); + if (n) { + global::navigationHandler->orbitalNavigator().setFocusNode(n->identifier()); + } + + const double t = std::clamp( + (_playback.elapsedTime - prevTime) / (nextTime - prevTime), + 0.0, + 1.0 + ); + + interaction::KeyframeNavigator::updateCamera( + global::navigationHandler->camera(), + prevPose, + nextPose, + t, + _ignoreRecordedScale + ); + + if (isSavingFramesDuringPlayback()) { + ghoul_assert(dt == _playback.saveScreenshots.deltaTime, "Misaligned delta times"); + + // Check if renderable in focus is still resolving tile loading + // do not adjust time while we are doing this, or take screenshot + const SceneGraphNode* focusNode = + global::navigationHandler->orbitalNavigator().anchorNode(); + const Renderable* focusRenderable = focusNode->renderable(); + if (!_playback.waitForLoading || + !focusRenderable || focusRenderable->renderedWithDesiredData()) + { + _playback.saveScreenshots.currentRecordedTime += + std::chrono::microseconds(static_cast(dt * 1000000)); + _playback.saveScreenshots.currentApplicationTime += dt; + + // Unfortunately the first frame is sometimes rendered because globebrowsing + // reports that all chunks are rendered when they apparently are not. + // previousTime == 0.0 -> rendering the first frame + if (previousTime != 0.0) { + global::renderEngine->takeScreenshot(); + } + } + } + + _currentEntry = probe; + if (probe == _timeline.entries.end()) { + if (_playback.isLooping) { + _playback.saveScreenshots.enabled = false; + setupPlayback(global::windowDelegate->applicationTime()); + } + else { + stopPlayback(); + } + } +} + +void SessionRecordingHandler::tickRecording(double dt) { + _recording.elapsedTime += dt; + + using namespace datamessagestructures; + CameraKeyframe kf = generateCameraKeyframe(); + _timeline.entries.emplace_back( + _recording.elapsedTime, + global::timeManager->time().j2000Seconds(), + KeyframeNavigator::CameraPose(std::move(kf)) + ); +} + +void SessionRecordingHandler::render() const { + if (!_renderPlaybackInformation || !isPlayingBack()) { + return; + } + + constexpr std::string_view FontName = "Mono"; + constexpr float FontSizeFrameinfo = 32.f; + const std::shared_ptr font = + global::fontManager->font(FontName, FontSizeFrameinfo); + + glm::vec2 penPosition = global::renderEngine->fontResolution() - glm::ivec2(150, 0); + const std::string text = std::format( + "Elapsed: {:.3f} / {}\n" + "Keyframe: {} / {}\n" + "Is Looping: {}\n" + "Saving frames: {}\n" + "Wait for Loading: {}\n" + "Scale: {}", + _playback.elapsedTime, _timeline.entries.back().timestamp, + std::distance(_timeline.entries.begin(), _currentEntry), _timeline.entries.size(), + _playback.isLooping ? "true" : "false", + _playback.saveScreenshots.enabled ? "true" : "false", + _playback.waitForLoading ? "true" : "false", + global::navigationHandler->camera()->scaling() + ); + ghoul::fontrendering::RenderFont( + *font, + penPosition, + text, + glm::vec4(1.f), + ghoul::fontrendering::CrDirection::Down + ); +} + +void SessionRecordingHandler::startRecording() { + if (_state == SessionState::Recording) { + throw ghoul::RuntimeError( + "Unable to start recording while already in recording mode", + "SessionRecordingHandler" + ); + } + else if (isPlayingBack()) { + throw ghoul::RuntimeError( + "Unable to start recording while in session playback mode", + "SessionRecordingHandler" + ); + } + + LINFO("Session recording started"); + + _state = SessionState::Recording; + _timeline = SessionRecording(); + _savePropertiesBaseline.clear(); + _recording.elapsedTime = 0.0; + + // Record the current delta time as the first property to save in the file. + // This needs to be saved as a baseline whether or not it changes during recording + // Dummy `_time` "property" to store the time setup in the baseline + _savePropertiesBaseline["_time"] = std::format( + "openspace.time.setPause({});openspace.time.setDeltaTime({});", + global::timeManager->isPaused() ? "true" : "false", + global::timeManager->targetDeltaTime() + ); +} + +void SessionRecordingHandler::stopRecording(const std::filesystem::path& filename, + DataMode dataMode) +{ + if (_state != SessionState::Recording) { + return; + } + + if (std::filesystem::is_regular_file(filename)) { + throw ghoul::RuntimeError(std::format( + "Unable to start recording. File '{}' already exists", filename + ), "SessionRecording"); + } + + for (const auto& [prop, script] : _savePropertiesBaseline) { + _timeline.entries.insert(_timeline.entries.begin(), { 0.0, 0.0, script }); + } + + saveSessionRecording(filename, _timeline, dataMode); + _state = SessionState::Idle; + cleanUpTimelinesAndKeyframes(); + LINFO("Session recording stopped"); +} + +void SessionRecordingHandler::startPlayback(SessionRecording timeline, bool loop, + bool shouldWaitForFinishedTiles, + std::optional saveScreenshotFps) +{ + OpenSpaceEngine::Mode prevMode = global::openSpaceEngine->currentMode(); + const bool canTriggerPlayback = global::openSpaceEngine->setMode( + OpenSpaceEngine::Mode::SessionRecordingPlayback + ); + if (!canTriggerPlayback) { + return; + } + + if (_state == SessionState::Recording) { + global::openSpaceEngine->setMode(prevMode); + throw ghoul::RuntimeError( + "Unable to start playback while in session recording mode" + ); + } + else if (isPlayingBack()) { + global::openSpaceEngine->setMode(prevMode); + throw ghoul::RuntimeError( + "Unable to start new playback while in session playback mode" + ); + } + + _playback.isLooping = loop; + _playback.waitForLoading = shouldWaitForFinishedTiles; + + timeline.forAll( + [this](const SessionRecording::Entry::Script& script) { + checkIfScriptUsesScenegraphNode(script); + return false; + } + ); + + if (timeline.entries.empty()) { + global::openSpaceEngine->setMode(prevMode); + throw ghoul::RuntimeError("Session recording is empty"); + } + if (!timeline.hasCameraFrame()) { + global::openSpaceEngine->setMode(prevMode); + throw ghoul::RuntimeError("Session recording did not contain camera keyframes"); + } + + _timeline = std::move(timeline); + + // Populate list of loaded scene graph nodes + _loadedNodes.clear(); + const std::vector nodes = sceneGraph()->allSceneGraphNodes(); + for (SceneGraphNode* n : nodes) { + _loadedNodes.push_back(n->identifier()); + } + + double now = global::windowDelegate->applicationTime(); + setupPlayback(now); + _playback.saveScreenshots.enabled = saveScreenshotFps.has_value(); + if (saveScreenshotFps.has_value()) { + _playback.saveScreenshots.deltaTime = 1.0 / *saveScreenshotFps; + } + + global::navigationHandler->orbitalNavigator().updateOnCameraInteraction(); + + LINFO("Playback session started"); + global::eventEngine->publishEvent( + events::EventSessionRecordingPlayback::State::Started + ); +} + +void SessionRecordingHandler::setupPlayback(double startTime) { + _playback.elapsedTime = 0.0; + _playback.saveScreenshots.currentRecordedTime = std::chrono::steady_clock::now(); + _playback.saveScreenshots.currentApplicationTime = + global::windowDelegate->applicationTime(); + global::navigationHandler->keyframeNavigator().setTimeReferenceMode( + KeyframeTimeRef::Relative_recordedStart, startTime); + + + auto firstCamera = _timeline.entries.begin(); + while (firstCamera != _timeline.entries.end() && + !std::holds_alternative(firstCamera->value)) + { + firstCamera++; + } + + std::string startFocusNode = + std::get(firstCamera->value).focusNode; + auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), startFocusNode); + if (it == _loadedNodes.end()) { + throw ghoul::RuntimeError(std::format( + "Playback file requires scenegraph node '{}', which is " + "not currently loaded", startFocusNode + )); + } + + global::timeManager->setTimeNextFrame(Time(firstCamera->simulationTime)); + _currentEntry = _timeline.entries.begin(); + _state = SessionState::Playback; +} + +void SessionRecordingHandler::seek(double recordingTime) { + _currentEntry = std::upper_bound( + _timeline.entries.begin(), + _timeline.entries.end(), + recordingTime, + [](double recordingTime, const SessionRecording::Entry& e) { + return recordingTime < e.timestamp; + } + ); + _playback.elapsedTime = recordingTime; +} + +bool SessionRecordingHandler::isPlaybackPaused() const { + return (_state == SessionState::PlaybackPaused); +} + +void SessionRecordingHandler::setPlaybackPause(bool pause) { + if (pause && _state == SessionState::Playback) { + _playback.playbackPausedWithDeltaTimePause = global::timeManager->isPaused(); + if (!_playback.playbackPausedWithDeltaTimePause) { + global::timeManager->setPause(true); + } + _state = SessionState::PlaybackPaused; + global::eventEngine->publishEvent( + events::EventSessionRecordingPlayback::State::Paused + ); + } + else if (!pause && _state == SessionState::PlaybackPaused) { + if (!_playback.playbackPausedWithDeltaTimePause) { + global::timeManager->setPause(false); + } + _state = SessionState::Playback; + global::eventEngine->publishEvent( + events::EventSessionRecordingPlayback::State::Resumed + ); + } +} + +void SessionRecordingHandler::stopPlayback() { + if (!isPlayingBack()) { + return; + } + + LINFO("Session playback finished"); + _state = SessionState::Idle; + cleanUpTimelinesAndKeyframes(); + global::eventEngine->publishEvent( + events::EventSessionRecordingPlayback::State::Finished + ); + global::openSpaceEngine->resetMode(); + global::navigationHandler->resetNavigationUpdateVariables(); +} + +void SessionRecordingHandler::cleanUpTimelinesAndKeyframes() { + _timeline = SessionRecording(); + _savePropertiesBaseline.clear(); + _loadedNodes.clear(); + _currentEntry = _timeline.entries.end(); + _playback.saveScreenshots.enabled = false; + _playback.isLooping = false; +} + +void SessionRecordingHandler::saveScriptKeyframeToTimeline(std::string script) { + constexpr std::array ScriptRejects = { + "openspace.sessionRecording.enableTakeScreenShotDuringPlayback", + "openspace.sessionRecording.startPlayback", + "openspace.sessionRecording.stopPlayback", + "openspace.sessionRecording.startRecording", + "openspace.sessionRecording.stopRecording", + "openspace.scriptScheduler.clear" + }; + + constexpr std::array ScriptsToBeTrimmed = { + "openspace.sessionRecording.togglePlaybackPause" + }; + + if (script.starts_with(ScriptReturnPrefix)) { + script = script.substr(ScriptReturnPrefix.length()); + } + for (std::string_view reject : ScriptRejects) { + if (script.starts_with(reject)) { + return; + } + } + + // Trim commands from script if found + for (std::string_view trimSnippet : ScriptsToBeTrimmed) { + auto findIdx = script.find(trimSnippet); + if (findIdx != std::string::npos) { + auto findClosingParens = script.find_first_of(')', findIdx); + script.erase(findIdx, findClosingParens + 1); + } + } + + // Any script snippet included in this vector will be trimmed from any script + // from the script manager, before it is recorded in the session recording file. + // The remainder of the script will be retained. + using ScriptSubstringReplace = std::pair; + constexpr std::array ScriptsToBeReplaced = { + std::pair { + "openspace.time.pauseToggleViaKeyboard", + "openspace.time.interpolateTogglePause" + } + }; + + // Replace commands from script if found + for (const ScriptSubstringReplace& replacementSnippet : ScriptsToBeReplaced) { + auto findIdx = script.find(replacementSnippet.first); + if (findIdx != std::string::npos) { + script.erase(findIdx, replacementSnippet.first.length()); + script.insert(findIdx, replacementSnippet.second); + } + } + + _timeline.entries.emplace_back( + _recording.elapsedTime, + global::timeManager->time().j2000Seconds(), + std::move(script) + ); +} + +void SessionRecordingHandler::savePropertyBaseline(properties::Property& prop) { + constexpr std::array PropertyBaselineRejects{ + "NavigationHandler.OrbitalNavigator.Anchor", + "NavigationHandler.OrbitalNavigator.Aim", + "NavigationHandler.OrbitalNavigator.RetargetAnchor", + "NavigationHandler.OrbitalNavigator.RetargetAim" + }; + + const std::string propIdentifier = prop.uri(); + for (std::string_view reject : PropertyBaselineRejects) { + if (propIdentifier.starts_with(reject)) { + return; + } + } + + const bool isPropAlreadySaved = _savePropertiesBaseline.contains(propIdentifier); + if (!isPropAlreadySaved) { + const std::string initialScriptCommand = std::format( + "openspace.setPropertyValueSingle(\"{}\", {})", + propIdentifier, prop.stringValue() + ); + _savePropertiesBaseline[propIdentifier] = initialScriptCommand; + } +} + +bool SessionRecordingHandler::isRecording() const { + return _state == SessionState::Recording; +} + +bool SessionRecordingHandler::isPlayingBack() const { + return _state == SessionState::Playback || _state == SessionState::PlaybackPaused; +} + +bool SessionRecordingHandler::isSavingFramesDuringPlayback() const { + return isPlayingBack() && _playback.saveScreenshots.enabled; +} + +bool SessionRecordingHandler::shouldWaitForTileLoading() const { + return _playback.waitForLoading; +} + +SessionRecordingHandler::SessionState SessionRecordingHandler::state() const { + return _state; +} + +double SessionRecordingHandler::fixedDeltaTimeDuringFrameOutput() const { + // Check if renderable in focus is still resolving tile loading + // do not adjust time while we are doing this + const SceneGraphNode* an = global::navigationHandler->orbitalNavigator().anchorNode(); + const Renderable* focusRenderable = an->renderable(); + if (!focusRenderable || focusRenderable->renderedWithDesiredData()) { + return _playback.saveScreenshots.deltaTime; + } + else { + return 0.0; + } +} + +std::chrono::steady_clock::time_point +SessionRecordingHandler::currentPlaybackInterpolationTime() const { + return _playback.saveScreenshots.currentRecordedTime; +} + +double SessionRecordingHandler::currentApplicationInterpolationTime() const { + return _playback.saveScreenshots.currentApplicationTime; +} + +void SessionRecordingHandler::checkIfScriptUsesScenegraphNode(std::string_view s) const { + auto isolateTermFromQuotes = [](std::string_view s) -> std::string_view { + // Remove any leading spaces + s.remove_prefix(s.find_first_not_of(" ")); + + // Find the first substring that is surrounded by possible quotes + constexpr std::string_view PossibleQuotes = "\'\"[]"; + s.remove_prefix(s.find_first_not_of(PossibleQuotes)); + size_t end = s.find_first_of(PossibleQuotes); + if (end != std::string::npos) { + return s.substr(0, end); + } + else { + // There were no closing quotes so we remove as much as possible + constexpr std::string_view UnwantedChars = " );"; + s.remove_suffix(s.find_last_not_of(UnwantedChars)); + return s; + } + }; + + auto checkForScenegraphNodeAccessNav = [](std::string_view navTerm) -> bool { + constexpr std::array NavScriptsUsingNodes = { + "NavigationHandler.OrbitalNavigator.RetargetAnchor", + "NavigationHandler.OrbitalNavigator.Anchor", + "NavigationHandler.OrbitalNavigator.Aim" + }; + + for (std::string_view script : NavScriptsUsingNodes) { + if (navTerm.find(script) != std::string::npos) { + return true; + } + } + return false; + }; + + if (s.starts_with(ScriptReturnPrefix)) { + s.remove_prefix(ScriptReturnPrefix.length()); + } + // This works for both setPropertyValue and setPropertyValueSingle + if (!s.starts_with("openspace.setPropertyValue") || s.find('(') == std::string::npos) + { + return; + } + + std::string_view subjectOfSetProp = isolateTermFromQuotes(s.substr(s.find('(') + 1)); + if (checkForScenegraphNodeAccessNav(subjectOfSetProp)) { + std::string_view navNode = isolateTermFromQuotes(s.substr(s.find(',') + 1)); + if (navNode != "nil") { + auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), navNode); + if (it == _loadedNodes.end()) { + LWARNING(std::format( + "Playback file contains a property setting of navigation using " + "scenegraph node '{}', which is not currently loaded", navNode + )); + } + } + } + else if (subjectOfSetProp.find("Scene.") != std::string::npos) { + auto extractScenegraphNodeFromScene = [](std::string_view s) -> std::string_view { + constexpr std::string_view Scene = "Scene."; + size_t scene = s.find(Scene); + if (scene == std::string_view::npos) { + return ""; + } + s.remove_prefix(scene + Scene.length()); + size_t end = s.find('.'); + return end != std::string_view::npos ? s.substr(0, end) : ""; + }; + + std::string_view found = extractScenegraphNodeFromScene(subjectOfSetProp); + if (!found.empty()) { + const std::vector matchHits = + sceneGraph()->propertiesMatchingRegex(subjectOfSetProp); + if (matchHits.empty()) { + LWARNING(std::format( + "Playback file contains a property setting of scenegraph " + "node '{}', which is not currently loaded", found + )); + } + } + } +} + +SessionRecordingHandler::CallbackHandle SessionRecordingHandler::addStateChangeCallback( + StateChangeCallback cb) +{ + const CallbackHandle handle = _nextCallbackHandle++; + _stateChangeCallbacks.emplace_back(handle, std::move(cb)); + return handle; +} + +void SessionRecordingHandler::removeStateChangeCallback(CallbackHandle handle) { + const auto it = std::find_if( + _stateChangeCallbacks.begin(), + _stateChangeCallbacks.end(), + [handle](const std::pair>& cb) { + return cb.first == handle; + } + ); + + ghoul_assert(it != _stateChangeCallbacks.end(), "handle must be a valid callback"); + _stateChangeCallbacks.erase(it); +} + +std::vector SessionRecordingHandler::playbackList() const { + const std::filesystem::path path = absPath("${RECORDINGS}"); + if (!std::filesystem::is_directory(path)) { + return std::vector(); + } + + std::vector fileList; + namespace fs = std::filesystem; + for (const fs::directory_entry& e : fs::directory_iterator(path)) { + if (!e.is_regular_file()) { + continue; + } + + // Remove path and keep only the filename + const std::string filename = e.path().filename().string(); +#ifdef WIN32 + DWORD attributes = GetFileAttributes(e.path().string().c_str()); + bool isHidden = attributes & FILE_ATTRIBUTE_HIDDEN; +#else + const bool isHidden = filename.find('.') == 0; +#endif // WIN32 + if (!isHidden) { + // Don't add hidden files + fileList.push_back(filename); + } + } + std::sort(fileList.begin(), fileList.end()); + return fileList; +} + +scripting::LuaLibrary SessionRecordingHandler::luaLibrary() { + return { + "sessionRecording", + { + codegen::lua::StartRecording, + codegen::lua::StopRecording, + codegen::lua::StartPlayback, + codegen::lua::StopPlayback, + codegen::lua::SetPlaybackPause, + codegen::lua::TogglePlaybackPause, + codegen::lua::IsPlayingBack, + codegen::lua::IsRecording + } + }; +} + +} // namespace openspace diff --git a/src/interaction/sessionrecordinghandler_lua.inl b/src/interaction/sessionrecordinghandler_lua.inl new file mode 100644 index 0000000000..d533b59a39 --- /dev/null +++ b/src/interaction/sessionrecordinghandler_lua.inl @@ -0,0 +1,121 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +namespace { + +/** + * Starts a recording session. The string argument is the filename used for the file where + * the recorded keyframes are saved. + */ +[[codegen::luawrap]] void startRecording() { + openspace::global::sessionRecordingHandler->startRecording(); +} + +// Stops a recording session. `dataMode` has to be "Ascii" or "Binary" +[[codegen::luawrap]] void stopRecording(std::filesystem::path recordFilePath, + std::string dataMode) +{ + if (recordFilePath.empty()) { + throw ghoul::lua::LuaError("Filepath string is empty"); + } + + if (dataMode != "Ascii" && dataMode != "Binary") { + throw ghoul::lua::LuaError(std::format("Invalid data mode {}", dataMode)); + } + + using DataMode = openspace::interaction::DataMode; + openspace::global::sessionRecordingHandler->stopRecording( + recordFilePath, + dataMode == "Ascii" ? DataMode::Ascii : DataMode::Binary + ); +} + +/** + * Starts a playback session with keyframe times that are relative to the time since the + * recording was started (the same relative time applies to the playback). When playback + * starts, the simulation time is automatically set to what it was at recording time. The + * string argument is the filename to pull playback keyframes from (the file path is + * relative to the RECORDINGS variable specified in the config file). If a second input + * value of true is given, then playback will continually loop until it is manually + * stopped. + */ +[[codegen::luawrap]] void startPlayback(std::string file, bool loop = false, + bool shouldWaitForTiles = true, + std::optional screenshotFps = std::nullopt) +{ + using namespace openspace; + + if (file.empty()) { + throw ghoul::lua::LuaError("Filepath string is empty"); + } + + if (!std::filesystem::is_regular_file(file)) { + throw ghoul::RuntimeError(std::format( + "Cannot find the specified playback file '{}'", file + )); + } + + interaction::SessionRecording timeline = interaction::loadSessionRecording(file); + global::sessionRecordingHandler->startPlayback( + std::move(timeline), + loop, + shouldWaitForTiles, + screenshotFps + ); +} + +// Stops a playback session before playback of all keyframes is complete. +[[codegen::luawrap]] void stopPlayback() { + openspace::global::sessionRecordingHandler->stopPlayback(); +} + +// Pauses or resumes the playback progression through keyframes. +[[codegen::luawrap]] void setPlaybackPause(bool pause) { + openspace::global::sessionRecordingHandler->setPlaybackPause(pause); +} + +/** + * Toggles the pause function, i.e. temporarily setting the delta time to 0 and restoring + * it afterwards. + */ +[[codegen::luawrap]] void togglePlaybackPause() { + using namespace openspace; + + bool isPlaybackPaused = global::sessionRecordingHandler->isPlaybackPaused(); + global::sessionRecordingHandler->setPlaybackPause(!isPlaybackPaused); +} + +// Returns true if session recording is currently playing back a recording. +[[codegen::luawrap]] bool isPlayingBack() { + return openspace::global::sessionRecordingHandler->isPlayingBack(); +} + +// Returns true if session recording is currently recording a recording. +[[codegen::luawrap]] bool isRecording() { + return openspace::global::sessionRecordingHandler->isRecording(); +} + +#include "sessionrecordinghandler_lua_codegen.cpp" + +} // namespace diff --git a/src/interaction/tasks/convertrecfileversiontask.cpp b/src/interaction/tasks/convertrecfileversiontask.cpp deleted file mode 100644 index f309eab39e..0000000000 --- a/src/interaction/tasks/convertrecfileversiontask.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/***************************************************************************************** - * * - * OpenSpace * - * * - * Copyright (c) 2014-2024 * - * * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this * - * software and associated documentation files (the "Software"), to deal in the Software * - * without restriction, including without limitation the rights to use, copy, modify, * - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * - * permit persons to whom the Software is furnished to do so, subject to the following * - * conditions: * - * * - * The above copyright notice and this permission notice shall be included in all copies * - * or substantial portions of the Software. * - * * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - ****************************************************************************************/ - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace { - constexpr std::string_view _loggerCat = "ConvertRecFileVersionTask"; - - constexpr std::string_view KeyInFilePath = "InputFilePath"; -} // namespace - -namespace openspace::interaction { - -ConvertRecFileVersionTask::ConvertRecFileVersionTask(const ghoul::Dictionary& dictionary) -{ - openspace::documentation::testSpecificationAndThrow( - documentation(), - dictionary, - "ConvertRecFileVersionTask" - ); - - _inFilename = dictionary.value(KeyInFilePath); - _inFilePath = absPath(_inFilename); - - ghoul_assert(std::filesystem::is_regular_file(_inFilePath), "The file must exist"); - if (!std::filesystem::is_regular_file(_inFilePath)) { - LERROR(std::format("Failed to load session recording file: {}", _inFilePath)); - } - else { - sessRec = new SessionRecording(false); - } -} - -ConvertRecFileVersionTask::~ConvertRecFileVersionTask() { - delete sessRec; -} - -std::string ConvertRecFileVersionTask::description() { - std::string description = std::format( - "Convert file format of session recording file '{}' to current version", - _inFilePath - ); - return description; -} - -void ConvertRecFileVersionTask::perform(const Task::ProgressCallback&) { - convert(); -} - -void ConvertRecFileVersionTask::convert() { - const bool hasBinaryFileExtension = SessionRecording::hasFileExtension( - _inFilename, - SessionRecording::FileExtensionBinary - ); - const bool hasAsciiFileExtension = SessionRecording::hasFileExtension( - _inFilename, - SessionRecording::FileExtensionAscii - ); - if (!hasBinaryFileExtension && !hasAsciiFileExtension) { - LERROR(std::format( - "Input filename does not have expected '{}' or '{}' extension", - SessionRecording::FileExtensionBinary, SessionRecording::FileExtensionAscii - )); - return; - } - sessRec->convertFileRelativePath(_inFilename); -} - -documentation::Documentation ConvertRecFileVersionTask::documentation() { - using namespace documentation; - return { - "ConvertRecFileVersionTask", - "convert_file_version_task", - "", - { - { - "InputFilePath", - new StringAnnotationVerifier("A valid filename to convert"), - Optional::No, - Private::No, - "The filename to update to the current file format", - }, - }, - }; -} - -} // namespace openspace::interaction diff --git a/src/interaction/tasks/convertrecformattask.cpp b/src/interaction/tasks/convertrecformattask.cpp index d7f19fe300..289ee653d8 100644 --- a/src/interaction/tasks/convertrecformattask.cpp +++ b/src/interaction/tasks/convertrecformattask.cpp @@ -39,293 +39,54 @@ namespace { constexpr std::string_view KeyInFilePath = "InputFilePath"; constexpr std::string_view KeyOutFilePath = "OutputFilePath"; + + struct [[codegen::Dictionary(ConvertRecFormatTask)]] Parameters { + std::filesystem::path inputFilePath; + std::filesystem::path outputFilePath; + + enum class DataMode { + Ascii, + Binary + }; + DataMode outputMode; + }; + +#include "convertrecformattask_codegen.cpp" } // namespace namespace openspace::interaction { +documentation::Documentation ConvertRecFormatTask::documentation() { + return codegen::doc("convert_format_task"); +} + ConvertRecFormatTask::ConvertRecFormatTask(const ghoul::Dictionary& dictionary) { - openspace::documentation::testSpecificationAndThrow( - documentation(), - dictionary, - "ConvertRecFormatTask" - ); + const Parameters p = codegen::bake(dictionary); - _inFilePath = absPath(dictionary.value(KeyInFilePath)); - _outFilePath = absPath(dictionary.value(KeyOutFilePath)); + _inFilePath = p.inputFilePath; + _outFilePath = p.outputFilePath; + + switch (p.outputMode) { + case Parameters::DataMode::Ascii: + _dataMode = DataMode::Ascii; + break; + case Parameters::DataMode::Binary: + _dataMode = DataMode::Binary; + break; + } - ghoul_assert(std::filesystem::is_regular_file(_inFilePath), "The file must exist"); if (!std::filesystem::is_regular_file(_inFilePath)) { LERROR(std::format("Failed to load session recording file: {}", _inFilePath)); } - else { - _iFile.open(_inFilePath, std::ifstream::in | std::ifstream::binary); - determineFormatType(); - sessRec = new SessionRecording(false); - } -} - -ConvertRecFormatTask::~ConvertRecFormatTask() { - _iFile.close(); - _oFile.close(); - delete sessRec; } std::string ConvertRecFormatTask::description() { - std::string description = - std::format("Convert session recording file '{}'", _inFilePath); - if (_fileFormatType == SessionRecording::DataMode::Ascii) { - description += "(ascii format) "; - } - else if (_fileFormatType == SessionRecording::DataMode::Binary) { - description += "(binary format) "; - } - else { - description += "(UNKNOWN format) "; - } - description += std::format("conversion to file '{}'", _outFilePath); - return description; + return "Convert session recording files between ASCII and Binary formats"; } void ConvertRecFormatTask::perform(const Task::ProgressCallback&) { - convert(); -} - -void ConvertRecFormatTask::convert() { - std::string expectedFileExtension_in; - std::string expectedFileExtension_out; - std::string currentFormat; - if (_fileFormatType == SessionRecording::DataMode::Binary) { - currentFormat = "binary"; - expectedFileExtension_in = SessionRecording::FileExtensionBinary; - expectedFileExtension_out = SessionRecording::FileExtensionAscii; - } - else if (_fileFormatType == SessionRecording::DataMode::Ascii) { - currentFormat = "ascii"; - expectedFileExtension_in = SessionRecording::FileExtensionAscii; - expectedFileExtension_out = SessionRecording::FileExtensionBinary; - } - - if (_inFilePath.extension() != expectedFileExtension_in) { - LWARNING(std::format( - "Input filename doesn't have expected '{}' format file extension", - currentFormat - )); - } - if (_outFilePath.extension() == expectedFileExtension_in) { - LERROR(std::format( - "Output filename has '{}' file extension, but is conversion from '{}'", - currentFormat, currentFormat - )); - return; - } - else if (_outFilePath.extension() != expectedFileExtension_out) { - _outFilePath += expectedFileExtension_out; - } - - if (_fileFormatType == SessionRecording::DataMode::Ascii) { - _iFile.close(); - _iFile.open(_inFilePath, std::ifstream::in); - //Throw out first line - std::string throw_out; - ghoul::getline(_iFile, throw_out); - _oFile.open(_outFilePath); - } - else if (_fileFormatType == SessionRecording::DataMode::Binary) { - _oFile.open(_outFilePath, std::ios::binary); - } - _oFile.write( - SessionRecording::FileHeaderTitle.c_str(), - SessionRecording::FileHeaderTitle.length() - ); - _oFile.write(_version.c_str(), SessionRecording::FileHeaderVersionLength); - _oFile.close(); - - if (_fileFormatType == SessionRecording::DataMode::Ascii) { - convertToBinary(); - } - else if (_fileFormatType == SessionRecording::DataMode::Binary) { - convertToAscii(); - } - else { - // Add error output for file type not recognized - LERROR("Session recording file unrecognized format type"); - } -} - -void ConvertRecFormatTask::determineFormatType() { - _fileFormatType = SessionRecording::DataMode::Unknown; - std::string line; - - line = SessionRecording::readHeaderElement(_iFile, - SessionRecording::FileHeaderTitle.length()); - - if (line.substr(0, SessionRecording::FileHeaderTitle.length()) - != SessionRecording::FileHeaderTitle) - { - LERROR(std::format( - "Session recording file '{}' does not have expected header", _inFilePath - )); - } - else { - //Read version string and throw it away (and also line feed character at end) - _version = SessionRecording::readHeaderElement(_iFile, - SessionRecording::FileHeaderVersionLength); - line = SessionRecording::readHeaderElement(_iFile, 1); - SessionRecording::readHeaderElement(_iFile, 1); - - if (line.at(0) == SessionRecording::DataFormatAsciiTag) { - _fileFormatType = SessionRecording::DataMode::Ascii; - } - else if (line.at(0) == SessionRecording::DataFormatBinaryTag) { - _fileFormatType = SessionRecording::DataMode::Binary; - } - } -} - -void ConvertRecFormatTask::convertToAscii() { - SessionRecording::Timestamps times; - datamessagestructures::CameraKeyframe ckf; - datamessagestructures::TimeKeyframe tkf; - datamessagestructures::ScriptMessage skf; - int lineNum = 1; - _oFile.open(_outFilePath, std::ifstream::app); - const char tmpType = SessionRecording::DataFormatAsciiTag; - _oFile.write(&tmpType, 1); - _oFile.write("\n", 1); - - while (true) { - const unsigned char frameType = readFromPlayback(_iFile); - // Check if have reached EOF - if (!_iFile) { - LINFO(std::format( - "Finished converting {} entries from file '{}'", lineNum - 1, _inFilePath - )); - break; - } - - std::stringstream keyframeLine = std::stringstream(); - keyframeLine.str(std::string()); - - if (frameType == SessionRecording::HeaderCameraBinary) { - sessRec->readCameraKeyframeBinary(times, ckf, _iFile, lineNum); - SessionRecording::saveHeaderAscii( - times, - SessionRecording::HeaderCameraAscii, - keyframeLine - ); - ckf.write(keyframeLine); - } - else if (frameType == SessionRecording::HeaderTimeBinary) { - sessRec->readTimeKeyframeBinary(times, tkf, _iFile, lineNum); - SessionRecording::saveHeaderAscii( - times, - SessionRecording::HeaderTimeAscii, - keyframeLine - ); - tkf.write(keyframeLine); - } - else if (frameType == SessionRecording::HeaderScriptBinary) { - sessRec->readScriptKeyframeBinary(times, skf, _iFile, lineNum); - SessionRecording::saveHeaderAscii( - times, - SessionRecording::HeaderScriptAscii, - keyframeLine - ); - skf.write(keyframeLine); - } - else { - LERROR(std::format( - "Unknown frame type @ index {} of playback file '{}'", - lineNum - 1, _inFilePath - )); - break; - } - - SessionRecording::saveKeyframeToFile(keyframeLine.str(), _oFile); - lineNum++; - } -} - -void ConvertRecFormatTask::convertToBinary() { - SessionRecording::Timestamps times; - datamessagestructures::CameraKeyframe ckf; - datamessagestructures::TimeKeyframe tkf; - datamessagestructures::ScriptMessage skf; - int lineNum = 1; - std::string lineContents; - std::array keyframeBuffer; - _oFile.open(_outFilePath, std::ifstream::app | std::ios::binary); - const char tmpType = SessionRecording::DataFormatBinaryTag; - _oFile.write(&tmpType, 1); - _oFile.write("\n", 1); - - while (ghoul::getline(_iFile, lineContents)) { - lineNum++; - - std::istringstream iss(lineContents); - std::string entryType; - if (!(iss >> entryType)) { - LERROR(std::format( - "Error reading entry type @ line {} of file '{}'", lineNum, _inFilePath - )); - break; - } - - if (entryType == SessionRecording::HeaderCameraAscii) { - sessRec->readCameraKeyframeAscii(times, ckf, lineContents, lineNum); - sessRec->saveCameraKeyframeBinary(times, ckf, keyframeBuffer.data(), - _oFile); - } - else if (entryType == SessionRecording::HeaderTimeAscii) { - sessRec->readTimeKeyframeAscii(times, tkf, lineContents, lineNum); - sessRec->saveTimeKeyframeBinary(times, tkf, keyframeBuffer.data(), - _oFile); - } - else if (entryType == SessionRecording::HeaderScriptAscii) { - sessRec->readScriptKeyframeAscii(times, skf, lineContents, lineNum); - sessRec->saveScriptKeyframeBinary(times, skf, keyframeBuffer.data(), - _oFile); - } - else if (entryType.substr(0, 1) == SessionRecording::HeaderCommentAscii) { - continue; - } - else { - LERROR(std::format( - "Unknown frame type {} @ line {} of file '{}'", - entryType, lineContents, _inFilePath - )); - break; - } - } - _oFile.close(); - LINFO(std::format( - "Finished converting {} entries from file '{}'", lineNum, _inFilePath - )); -} - -documentation::Documentation ConvertRecFormatTask::documentation() { - using namespace documentation; - return { - "ConvertRecFormatTask", - "convert_format_task", - "", - { - { - "InputFilePath", - new StringAnnotationVerifier("A valid filename to convert"), - Optional::No, - Private::No, - "The filename to convert to the opposite format", - }, - { - "OutputFilePath", - new StringAnnotationVerifier("A valid output filename"), - Optional::No, - Private::No, - "The filename containing the converted result", - }, - }, - }; + SessionRecording sessionRecording = loadSessionRecording(_inFilePath); + saveSessionRecording(_outFilePath, sessionRecording, _dataMode); } } // namespace openspace::interaction diff --git a/src/navigation/keyframenavigator.cpp b/src/navigation/keyframenavigator.cpp index 571f7e82bc..47313e71b8 100644 --- a/src/navigation/keyframenavigator.cpp +++ b/src/navigation/keyframenavigator.cpp @@ -134,7 +134,7 @@ void KeyframeNavigator::updateCamera(Camera* camera, const CameraPose& prevPose, // Linear interpolation t = std::max(0.0, std::min(1.0, t)); - const glm::dvec3 nowCameraPosition = + glm::dvec3 nowCameraPosition = prevKeyframeCameraPosition * (1.0 - t) + nextKeyframeCameraPosition * t; glm::dquat nowCameraRotation = glm::slerp( prevKeyframeCameraRotation, diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp index 6e453bad6c..dd1af7d847 100644 --- a/src/scene/scene.cpp +++ b/src/scene/scene.cpp @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include #include @@ -91,8 +91,8 @@ namespace { std::chrono::steady_clock::time_point currentTimeForInterpolation() { using namespace openspace::global; - if (sessionRecording->isSavingFramesDuringPlayback()) { - return sessionRecording->currentPlaybackInterpolationTime(); + if (sessionRecordingHandler->isSavingFramesDuringPlayback()) { + return sessionRecordingHandler->currentPlaybackInterpolationTime(); } else { return std::chrono::steady_clock::now(); @@ -775,7 +775,7 @@ PropertyValueType Scene::propertyValueType(const std::string& value) { } std::vector Scene::propertiesMatchingRegex( - const std::string& propertyString) + std::string_view propertyString) { return findMatchesInAllProperties(propertyString, allProperties(), ""); } diff --git a/src/scene/scene_lua.inl b/src/scene/scene_lua.inl index 180956ad17..1bed6974a8 100644 --- a/src/scene/scene_lua.inl +++ b/src/scene/scene_lua.inl @@ -87,9 +87,9 @@ openspace::properties::PropertyOwner* findPropertyOwnerWithMatchingGroupTag(T* p } std::vector findMatchesInAllProperties( - const std::string& regex, - const std::vector& properties, - const std::string& groupName) + std::string_view regex, + const std::vector& properties, + const std::string& groupName) { using namespace openspace; @@ -241,8 +241,8 @@ void applyRegularExpression(lua_State* L, const std::string& regex, // value from the stack, so we need to push it to the end lua_pushvalue(L, -1); - if (global::sessionRecording->isRecording()) { - global::sessionRecording->savePropertyBaseline(*prop); + if (global::sessionRecordingHandler->isRecording()) { + global::sessionRecordingHandler->savePropertyBaseline(*prop); } if (interpolationDuration == 0.0) { global::renderEngine->scene()->removePropertyInterpolation(prop); @@ -312,8 +312,8 @@ int setPropertyCallSingle(properties::Property& prop, const std::string& uri, ); } else { - if (global::sessionRecording->isRecording()) { - global::sessionRecording->savePropertyBaseline(prop); + if (global::sessionRecordingHandler->isRecording()) { + global::sessionRecordingHandler->savePropertyBaseline(prop); } if (duration == 0.0) { global::renderEngine->scene()->removePropertyInterpolation(&prop); diff --git a/src/scripting/scriptengine.cpp b/src/scripting/scriptengine.cpp index 3ade730808..201cb544ec 100644 --- a/src/scripting/scriptengine.cpp +++ b/src/scripting/scriptengine.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -482,8 +483,8 @@ void ScriptEngine::preSync(bool isMaster) { // Not really a received script but the master also needs to run the script... _masterScriptQueue.push(item); - if (global::sessionRecording->isRecording()) { - global::sessionRecording->saveScriptKeyframeToTimeline(item.code); + if (global::sessionRecordingHandler->isRecording()) { + global::sessionRecordingHandler->saveScriptKeyframeToTimeline(item.code); } // Sync out to other nodes (cluster) @@ -562,8 +563,8 @@ void ScriptEngine::postSync(bool isMaster) { } double now = - global::sessionRecording->isSavingFramesDuringPlayback() ? - global::sessionRecording->currentApplicationInterpolationTime() : + global::sessionRecordingHandler->isSavingFramesDuringPlayback() ? + global::sessionRecordingHandler->currentApplicationInterpolationTime() : global::windowDelegate->applicationTime(); for (RepeatedScriptInfo& info : _repeatedScripts) { if (now - info.lastRun >= info.timeout) { diff --git a/src/scripting/scriptscheduler.cpp b/src/scripting/scriptscheduler.cpp index f7270ab576..c6a55a8d3c 100644 --- a/src/scripting/scriptscheduler.cpp +++ b/src/scripting/scriptscheduler.cpp @@ -244,10 +244,6 @@ std::vector ScriptScheduler::progressTo(double newTime) { } } -void ScriptScheduler::setTimeReferenceMode(interaction::KeyframeTimeRef refType) { - _timeframeMode = refType; -} - double ScriptScheduler::currentTime() const { return _currentTime; } @@ -276,27 +272,12 @@ std::vector ScriptScheduler::allScripts( return result; } -void ScriptScheduler::setModeApplicationTime() { - _timeframeMode = interaction::KeyframeTimeRef::Relative_applicationStart; -} - -void ScriptScheduler::setModeRecordedTime() { - _timeframeMode = interaction::KeyframeTimeRef::Relative_recordedStart; -} - -void ScriptScheduler::setModeSimulationTime() { - _timeframeMode = interaction::KeyframeTimeRef::Absolute_simTimeJ2000; -} - LuaLibrary ScriptScheduler::luaLibrary() { return { "scriptScheduler", { codegen::lua::LoadFile, codegen::lua::LoadScheduledScript, - codegen::lua::SetModeApplicationTime, - codegen::lua::SetModeRecordedTime, - codegen::lua::SetModeSimulationTime, codegen::lua::Clear, codegen::lua::ScheduledScripts } diff --git a/src/scripting/scriptscheduler_lua.inl b/src/scripting/scriptscheduler_lua.inl index 66d7598b0a..3b2a41c78d 100644 --- a/src/scripting/scriptscheduler_lua.inl +++ b/src/scripting/scriptscheduler_lua.inl @@ -78,30 +78,6 @@ namespace { global::scriptScheduler->loadScripts(scripts); } -/** - * Sets the time reference for scheduled scripts to application time (seconds since - * OpenSpace application started). - */ -[[codegen::luawrap]] void setModeApplicationTime() { - openspace::global::scriptScheduler->setModeApplicationTime(); -} - -/** - * Sets the time reference for scheduled scripts to the time since the recording was - * started (the same relative time applies to playback). - */ -[[codegen::luawrap]] void setModeRecordedTime() { - openspace::global::scriptScheduler->setModeRecordedTime(); -} - -/** - * Sets the time reference for scheduled scripts to the simulated date & time (J2000 epoch - * seconds). - */ -[[codegen::luawrap]] void setModeSimulationTime() { - openspace::global::scriptScheduler->setModeSimulationTime(); -} - // Clears all scheduled scripts. [[codegen::luawrap]] void clear(std::optional group) { openspace::global::scriptScheduler->clearSchedule(group); diff --git a/src/util/time.cpp b/src/util/time.cpp index a6ca8e8d37..97c51265d0 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/util/time_lua.inl b/src/util/time_lua.inl index cee0fe80ca..9b4abe4f21 100644 --- a/src/util/time_lua.inl +++ b/src/util/time_lua.inl @@ -178,8 +178,8 @@ namespace { OpenSpaceEngine::Mode m = global::openSpaceEngine->currentMode(); if (m == OpenSpaceEngine::Mode::SessionRecordingPlayback) { - bool isPlaybackPaused = global::sessionRecording->isPlaybackPaused(); - global::sessionRecording->setPlaybackPause(!isPlaybackPaused); + bool isPlaybackPaused = global::sessionRecordingHandler->isPlaybackPaused(); + global::sessionRecordingHandler->setPlaybackPause(!isPlaybackPaused); } else { const bool isPaused = !global::timeManager->isPaused(); diff --git a/src/util/timemanager.cpp b/src/util/timemanager.cpp index 3a5347971b..47cb8875ed 100644 --- a/src/util/timemanager.cpp +++ b/src/util/timemanager.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -88,8 +88,8 @@ namespace { double currentApplicationTimeForInterpolation() { using namespace openspace; - if (global::sessionRecording->isSavingFramesDuringPlayback()) { - return global::sessionRecording->currentApplicationInterpolationTime(); + if (global::sessionRecordingHandler->isSavingFramesDuringPlayback()) { + return global::sessionRecordingHandler->currentApplicationInterpolationTime(); } else { return global::windowDelegate->applicationTime(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3fc2ce772a..25ba287d85 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable( test_profile.cpp test_rawvolumeio.cpp test_scriptscheduler.cpp + test_sessionrecording.cpp test_settings.cpp test_sgctedit.cpp test_spicemanager.cpp diff --git a/tests/sessionrecording/0100_ascii_linux.osrectxt b/tests/sessionrecording/0100_ascii_linux.osrectxt new file mode 100644 index 0000000000..d51e31c259 --- /dev/null +++ b/tests/sessionrecording/0100_ascii_linux.osrectxt @@ -0,0 +1,7 @@ +OpenSpace_record/playback01.00A +script 212.227 0 762933560.401 1 openspace.setPropertyValueSingle("Modules.CefWebGui.Visible", true) +camera 212.237 0.0107105 762933560.412 -13521714.7579869 -18987604.3886074 -1780842.2573154 0.6953145 -0.2337213 -0.1973068 0.6503710 0.00126470590475947 F Earth +camera 212.248 0.0210999 762933560.423 -13521714.7579868 -18987604.3885993 -1780842.2573175 0.6953145 -0.2337212 -0.1973068 0.6503710 0.00126470590475947 F Earth +script 214.708 2.48093 762933562.877 1 openspace.pathnavigation.flyTo("Mars") +camera 214.709 2.48222 762933562.886 -13521714.7579865 -18987604.3886450 -1780842.2572641 0.6953144 -0.2337212 -0.1973068 0.6503708 0.00126470590475947 F Earth +camera 214.717 2.49067 762933562.886 -13523362.1802436 -18989917.8348593 -1781059.2290947 0.6953144 -0.2337212 -0.1973068 0.6503708 0.00126449402887374 F Earth diff --git a/tests/sessionrecording/0100_ascii_windows.osrectxt b/tests/sessionrecording/0100_ascii_windows.osrectxt new file mode 100644 index 0000000000..78a2e20adb --- /dev/null +++ b/tests/sessionrecording/0100_ascii_windows.osrectxt @@ -0,0 +1,7 @@ +OpenSpace_record/playback01.00A +script 10.0 0.0 100.0 1 openspace.time.setPause(false) +camera 11.0 1.0 101.0 101.123 201.456 301.789 0.06 0.26 0.49 0.82 1.26e-03 F Earth +camera 11.5 2.0 102.0 102.123 202.456 302.789 0.12 0.19 0.48 0.84 1.25e-03 F Earth +script 12.0 4.0 104.0 1 openspace.setPropertyValueSingle("Scene.Earth.Renderable.Fade", 1.0) +camera 12.5 4.0 104.0 104.123 204.456 304.789 0.30 -0.01 0.41 0.85 1.27e-03 F Earth +camera 13.0 5.0 105.0 105.123 205.456 305.789 0.31 -0.02 0.41 0.85 1.28e-03 F Earth diff --git a/tests/sessionrecording/0100_binary_linux.osrec b/tests/sessionrecording/0100_binary_linux.osrec new file mode 100644 index 0000000000000000000000000000000000000000..ea9b230bb67db878b1025ec0e1ed18924f6d94eb GIT binary patch literal 599 zcmeY-NX-i_NK8(RFG@|$FG|rb$VseBN=(i+Fw`?JaN;VSmA*A%Hgkvr0~GAvE_?m( zfn$zxKz=?@b1_J>UP)$ds$OwwNkC$0ajHgIVoq_YX7Vm}zlClrAgwGS!kMl1)BehV zwO)M7ARCmCcd*z$xXB+6A;IR}9zK5*B0Z4aa!_84E;52O#wczz2+H%$As zaz7(0kmZ_KRFYA9WT_R94F<_=rj3bJfdj{sJh}5>87$ z0MQF7Hk`FbgqGk@q|kaNowIbsqcDdBi?*g_{ZMm=d2$1+nJ9Je<}50OGOQApigX literal 0 HcmV?d00001 diff --git a/tests/sessionrecording/0100_binary_windows.osrec b/tests/sessionrecording/0100_binary_windows.osrec new file mode 100644 index 0000000000000000000000000000000000000000..5ae86513b7d6e4e8f52db4244d4b2bbaf4d58bdb GIT binary patch literal 600 zcmeY-NX-i_NK8(RFG@|$FG|rb$VseBN=(i+Fw`?JaN;UvzzQNAK$(W|L)Cy)gnl%g# zV2K8(HSJJq{GisnfmyT9J_u~z344gO_v{~5iL7&fh-?keUbr=A0U-dfh9Szq1!x~4 zAV6VI1mqT#RE8zyl%@t}=B4MPYA6LKr{<;VfgP_Gl$w{4T9lZSld9*In3AfbqW}zR z^x#5R01htpC`fSiLH)r~>|hKAm+h-T&aP_N4+*AQ_7L&cNWrC8whZnMwBQnf_`@Lz e8Y@t1)xHSM=zpnTI literal 0 HcmV?d00001 diff --git a/tests/sessionrecording/0200_ascii_linux.osrectxt b/tests/sessionrecording/0200_ascii_linux.osrectxt new file mode 100644 index 0000000000..3bcd2ae48a --- /dev/null +++ b/tests/sessionrecording/0200_ascii_linux.osrectxt @@ -0,0 +1,7 @@ +OpenSpace_record/playback02.00A +script 0 0 1 openspace.time.setPause(false);openspace.time.setDeltaTime(1); +script 0 0 1 openspace.setPropertyValueSingle("Scene.hoverCircle.Renderable.Fade", 0) +camera 0.002065126085653901 779267322.4886187 123389190187.6973 -987938150497.865 346357762351.6706 -0.36478543 0.4468942 -0.6903772 -0.4365736 2.0395897e-08 - Venus +camera 0.004236564040184021 779267322.4907901 123389190187.6973 -987938150497.865 346357762351.6706 -0.36478543 0.4468942 -0.6903772 -0.4365736 2.0395897e-08 - Venus +script 8.958355146809481 779267331.4449104 1 openspace.setPropertyValueSingle('Scene.hoverCircle.Renderable.Fade', 0.0); +camera 26.381116207921878 779267348.8676736 62165003943664156672 482784744565750824960 1117492338753629847552 -0.16281216 -0.117395274 -0.6741872 0.7107617 1.7638751e-17 - Venus diff --git a/tests/sessionrecording/0200_ascii_windows.osrectxt b/tests/sessionrecording/0200_ascii_windows.osrectxt new file mode 100644 index 0000000000..387187ff79 --- /dev/null +++ b/tests/sessionrecording/0200_ascii_windows.osrectxt @@ -0,0 +1,7 @@ +OpenSpace_record/playback02.00A +script 0.0 100.0 1 openspace.time.setPause(false) +camera 1.0 101.0 101.123 201.456 301.789 0.06 0.26 0.49 0.82 1.26e-03 F Earth +camera 2.0 102.0 102.123 202.456 302.789 0.12 0.19 0.48 0.84 1.25e-03 F Earth +script 4.0 104.0 1 openspace.setPropertyValueSingle("Scene.Earth.Renderable.Fade", 1.0) +camera 4.0 104.0 104.123 204.456 304.789 0.30 -0.01 0.41 0.85 1.27e-03 F Earth +camera 5.0 105.0 105.123 205.456 305.789 0.31 -0.02 0.41 0.85 1.28e-03 F Earth diff --git a/tests/sessionrecording/0200_binary_linux.osrec b/tests/sessionrecording/0200_binary_linux.osrec new file mode 100644 index 0000000000000000000000000000000000000000..e3d370fb0ea1112f78047ba50638a2ec4626efb6 GIT binary patch literal 531 zcmeY-NX-i_NK8(RFG@|$FG|rb$VseBN=(i+Fw!$HaN;UvKm&F_em+oBF-VhMNoH=U zUU6zkKw@ceszzF3PI0QHHKw#nYEDUF2vDttp{8{*(1ZrR+zk8vdr3)Qmd72rHg;-x zb~rog$Yw6F<_SA^Dy=1NE3?+YElL9R-xo62Tkv}CQ5n~L{gR++o_^CKZ>6WE~T$2wp7U2Vs-->|TqLRw6 z#GKO9;LN=AoKy{^;N;Z2R6Vd`^nz0JQc{Z&lX6n^d=e{Di;DG}^Kx5SiGB^?7zpjTwekNc}Q9L|Hdf#HL02UBqOL573} zPh+pgJ2D6rdDiWichI>>!@gL+a)0e3<$dm<+;$A`5V^8j%`%yxVeS2u9o7ynFRB%Q zyrNJp?;pqc@nC~X`Qf$C4?F(3x={OrG4r9Vts3@UL@oELO;+A_@D~@7G0MIFO#wwf Bxv~HN literal 0 HcmV?d00001 diff --git a/tests/sessionrecording/0200_binary_windows.osrec b/tests/sessionrecording/0200_binary_windows.osrec new file mode 100644 index 0000000000000000000000000000000000000000..419ce83f1fdbf03583da42a3959f74a07c379100 GIT binary patch literal 456 zcmeY-NX-i_NK8(RFG@|$FG|rb$VseBN=(i+Fw!$HaN;UvKmm~sazK7QP*X8TlU_+? zZmM2!YDqw1X>qDXT4GLds%A1o#|L{b?GWi88x-#ya7`o4efbiGW6EhozM&A(IR z(0{1Zc86T6oyWspcCD`i?HO5tI$RTrN;0OYEwzFg!{7imrU7b9JJc9Is4;Iq#(cGn z>36aN(ss67;Yh}CU0-Yk^?(4x7=|bZ7od3v4}jcW1mqT#RE8zyl%@t}=B4MPYA6LK zr{<;VfgPh4l$w{4T9lZSld9*In3AfbqhP3K0P`loV6ZpYqafbwgSwBW*kRVpnRZ;) zmG`v1zGydVW+amP6w8*uyeR^4pF=3kL9?+P@Sd3Y7bh#A(p9_;f literal 0 HcmV?d00001 diff --git a/tests/test_sessionrecording.cpp b/tests/test_sessionrecording.cpp new file mode 100644 index 0000000000..bf63167d23 --- /dev/null +++ b/tests/test_sessionrecording.cpp @@ -0,0 +1,854 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include +#include + +#include +#include +#include + +using namespace openspace::interaction; + +namespace { + std::filesystem::path test(std::string_view file) { + return absPath(std::format("${{TESTDIR}}/sessionrecording/{}", file)); + } +} // namespace + +TEST_CASE("SessionRecording: 01.00 Ascii Windows", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_ascii_windows.osrectxt")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 100.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 1.0); + CHECK(e.simulationTime == 101.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(101.123, 201.456, 301.789)); + CHECK(camera.rotation == glm::quat(0.82f, 0.06f, 0.26f, 0.49f)); + CHECK(camera.scale == 1.26e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 2.0); + CHECK(e.simulationTime == 102.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(102.123, 202.456, 302.789)); + CHECK(camera.rotation == glm::quat(0.84f, 0.12f, 0.19f, 0.48f)); + CHECK(camera.scale == 1.25e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK( + script == + "openspace.setPropertyValueSingle(\"Scene.Earth.Renderable.Fade\", 1.0)" + ); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(104.123, 204.456, 304.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.30f, -0.01f, 0.41f)); + CHECK(camera.scale == 1.27e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 5.0); + CHECK(e.simulationTime == 105.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(105.123, 205.456, 305.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.31f, -0.02f, 0.41f)); + CHECK(camera.scale == 1.28e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 01.00 Ascii Windows Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_ascii_windows.osrectxt")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 02.00 Ascii Windows", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_ascii_windows.osrectxt")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 100.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 1.0); + CHECK(e.simulationTime == 101.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(101.123, 201.456, 301.789)); + CHECK(camera.rotation == glm::quat(0.82f, 0.06f, 0.26f, 0.49f)); + CHECK(camera.scale == 1.26e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 2.0); + CHECK(e.simulationTime == 102.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(102.123, 202.456, 302.789)); + CHECK(camera.rotation == glm::quat(0.84f, 0.12f, 0.19f, 0.48f)); + CHECK(camera.scale == 1.25e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK( + script == + "openspace.setPropertyValueSingle(\"Scene.Earth.Renderable.Fade\", 1.0)" + ); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(104.123, 204.456, 304.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.30f, -0.01f, 0.41f)); + CHECK(camera.scale == 1.27e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 5.0); + CHECK(e.simulationTime == 105.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(105.123, 205.456, 305.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.31f, -0.02f, 0.41f)); + CHECK(camera.scale == 1.28e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 02.00 Ascii Windows Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_ascii_windows.osrectxt")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 01.00 <-> 02.00 Ascii Windows", "[sessionrecording]") { + SessionRecording v0100 = loadSessionRecording(test("0100_ascii_windows.osrectxt")); + SessionRecording v0200 = loadSessionRecording(test("0200_ascii_windows.osrectxt")); + CHECK(v0100 == v0200); +} + +TEST_CASE("SessionRecording: 01.00 Binary Windows", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_binary_windows.osrec")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 100.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 1.0); + CHECK(e.simulationTime == 101.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(101.123, 201.456, 301.789)); + CHECK(camera.rotation == glm::quat(0.82f, 0.06f, 0.26f, 0.49f)); + CHECK(camera.scale == 1.26e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 2.0); + CHECK(e.simulationTime == 102.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(102.123, 202.456, 302.789)); + CHECK(camera.rotation == glm::quat(0.84f, 0.12f, 0.19f, 0.48f)); + CHECK(camera.scale == 1.25e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK( + script == + "openspace.setPropertyValueSingle(\"Scene.Earth.Renderable.Fade\", 1.0)" + ); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(104.123, 204.456, 304.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.30f, -0.01f, 0.41f)); + CHECK(camera.scale == 1.27e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 5.0); + CHECK(e.simulationTime == 105.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(105.123, 205.456, 305.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.31f, -0.02f, 0.41f)); + CHECK(camera.scale == 1.28e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 01.00 Binary Windows Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_binary_windows.osrec")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 02.00 Binary Windows", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_binary_windows.osrec")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 100.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 1.0); + CHECK(e.simulationTime == 101.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(101.123, 201.456, 301.789)); + CHECK(camera.rotation == glm::quat(0.82f, 0.06f, 0.26f, 0.49f)); + CHECK(camera.scale == 1.26e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 2.0); + CHECK(e.simulationTime == 102.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(102.123, 202.456, 302.789)); + CHECK(camera.rotation == glm::quat(0.84f, 0.12f, 0.19f, 0.48f)); + CHECK(camera.scale == 1.25e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK( + script == + "openspace.setPropertyValueSingle(\"Scene.Earth.Renderable.Fade\", 1.0)" + ); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 4.0); + CHECK(e.simulationTime == 104.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(104.123, 204.456, 304.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.30f, -0.01f, 0.41f)); + CHECK(camera.scale == 1.27e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 5.0); + CHECK(e.simulationTime == 105.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK(camera.position == glm::dvec3(105.123, 205.456, 305.789)); + CHECK(camera.rotation == glm::quat(0.85f, 0.31f, -0.02f, 0.41f)); + CHECK(camera.scale == 1.28e-03f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 02.00 Binary Windows Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_binary_windows.osrec")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 01.00 <-> 02.00 Binary Windows", "[sessionrecording]") { + SessionRecording v0100 = loadSessionRecording(test("0100_binary_windows.osrec")); + SessionRecording v0200 = loadSessionRecording(test("0200_binary_windows.osrec")); + CHECK(v0100 == v0200); +} + +TEST_CASE("SessionRecording: 02.00 Ascii <-> Binary Windows", "[sessionrecording]") { + SessionRecording ascii = loadSessionRecording(test("0200_ascii_windows.osrectxt")); + SessionRecording binary = loadSessionRecording(test("0200_binary_windows.osrec")); + CHECK(ascii == binary); +} + +TEST_CASE("SessionRecording: 01.00 Ascii Linux", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_ascii_linux.osrectxt")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 762933560.401); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.setPropertyValueSingle(\"Modules.CefWebGui.Visible\", true)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 0.0107105); + CHECK(e.simulationTime == 762933560.412); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-13521714.7579869, -18987604.3886074, -1780842.2573154) + ); + CHECK( + camera.rotation == + glm::quat(0.6503710f, 0.6953145f, -0.2337213f, -0.1973068f) + ); + CHECK(camera.scale == 0.00126470590475947f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 0.0210999); + CHECK(e.simulationTime == 762933560.423); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-13521714.7579868, -18987604.3885993, -1780842.2573175) + ); + CHECK( + camera.rotation == + glm::quat(0.6503710f, 0.6953145f, -0.2337212f, -0.1973068f) + ); + CHECK(camera.scale == 0.00126470590475947f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 2.48093); + CHECK(e.simulationTime == 762933562.877); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.pathnavigation.flyTo(\"Mars\")"); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 2.48222); + CHECK(e.simulationTime == 762933562.886); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-13521714.7579865, -18987604.3886450, -1780842.2572641) + ); + CHECK( + camera.rotation == + glm::quat(0.6503708f, 0.6953144f, -0.2337212f, -0.1973068f) + ); + CHECK(camera.scale == 0.00126470590475947f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 2.49067); + CHECK(e.simulationTime == 762933562.886); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-13523362.1802436, -18989917.8348593, -1781059.2290947) + ); + CHECK( + camera.rotation == + glm::quat(0.6503708f, 0.6953144f, -0.2337212f, -0.1973068f) + ); + CHECK(camera.scale == 0.00126449402887374f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 01.00 Ascii Linux Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_ascii_linux.osrectxt")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 01.00 Binary Linux", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_binary_linux.osrec")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 763463598.23217); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false)"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 0.01045432000000801); + CHECK(e.simulationTime == 763463598.2421138); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-15942288.9063634, 8217794.03633486, -14994951.65751712) + ); + CHECK( + camera.rotation == + glm::quat( + -0.05070944130420685f, + 0.8491553664207458f, + -0.31565767526626587f, + -0.42038553953170776f + ) + ); + CHECK(camera.scale == 0.0012647059047594666f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 0.21673793700000488); + CHECK(e.simulationTime == 763463598.44845); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-15942288.906364493, 8217794.036353504, -14994951.657487243) + ); + CHECK( + camera.rotation == + glm::quat( + -0.05070945620536804f, + 0.8491553664207458f, + -0.31565767526626587f, + -0.420385479927063f + ) + ); + CHECK(camera.scale == 0.0012647059047594666f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 9.940045733000005); + CHECK(e.simulationTime == 763463608.1701536); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.setPropertyValueSingle(\"Scene.Earth.Renderable.Fade\",0,1)"); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 9.94726788300001); + CHECK(e.simulationTime == 763463608.1800178); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-15741144.947559996, 14747679.124696942, -9014411.005969524) + ); + CHECK( + camera.rotation == + glm::quat( + 0.23194797337055206f, + -0.7898141741752625f, + 0.2626582682132721f, + 0.5033928751945496f + ) + ); + CHECK(camera.scale == 0.001264723134227097f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 11.485186747); + CHECK(e.simulationTime == 763463609.7179065); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-15741006.774582125, 14747877.286725827, -9014328.087388767) + ); + CHECK( + camera.rotation == + glm::quat( + 0.2319525182247162f, + -0.7898122668266296f, + 0.26266056299209595f, + 0.5033925771713257f + ) + ); + CHECK(camera.scale == 0.001264723134227097f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Earth"); + } +} + +TEST_CASE("SessionRecording: 01.00 Binary Linux Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0100_binary_linux.osrec")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 02.00 Ascii Linux", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_ascii_linux.osrectxt")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 0.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false);openspace.time.setDeltaTime(1);"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 0.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.setPropertyValueSingle(\"Scene.hoverCircle.Renderable.Fade\", 0)"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 0.002065126085653901); + CHECK(e.simulationTime == 779267322.4886187); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(123389190187.6973, -987938150497.865, 346357762351.6706) + ); + CHECK( + camera.rotation == + glm::quat(-0.4365736f, -0.36478543f, 0.4468942f, -0.6903772f) + ); + CHECK(camera.scale == 2.0395897e-08f); + CHECK(!camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 0.004236564040184021); + CHECK(e.simulationTime == 779267322.4907901); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(123389190187.6973, -987938150497.865, 346357762351.6706) + ); + CHECK( + camera.rotation == + glm::quat(-0.4365736f, -0.36478543f, 0.4468942f, -0.6903772f) + ); + CHECK(camera.scale == 2.0395897e-08f); + CHECK(!camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 8.958355146809481); + CHECK(e.simulationTime == 779267331.4449104); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.setPropertyValueSingle('Scene.hoverCircle.Renderable.Fade', 0.0);"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 26.381116207921878); + CHECK(e.simulationTime == 779267348.8676736); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3( + 62165003943664156672.0, + 482784744565750824960.0, + 1117492338753629847552.0 + ) + ); + CHECK( + camera.rotation == + glm::quat(0.7107617f, -0.16281216f, -0.117395274f, -0.6741872f) + ); + CHECK(camera.scale == 1.7638751e-17f); + CHECK(!camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } +} + +TEST_CASE("SessionRecording: 02.00 Ascii Linux Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_ascii_linux.osrectxt")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +} + +TEST_CASE("SessionRecording: 02.00 Binary Linux", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_binary_linux.osrec")); + REQUIRE(rec.entries.size() == 6); + + { + const SessionRecording::Entry& e = rec.entries[0]; + CHECK(e.timestamp == 0.0); + CHECK(e.simulationTime == 0.0); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.time.setPause(false);openspace.time.setDeltaTime(1);"); + } + { + const SessionRecording::Entry& e = rec.entries[1]; + CHECK(e.timestamp == 0.0029818089678883553); + CHECK(e.simulationTime == 779267268.772417); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(2560146.332327013, -5779694.5689156465, -852442.7158538934) + ); + CHECK( + camera.rotation == + glm::quat( + 0.6254600882530212f, + 0.5630295276641846f, + 0.502471387386322f, + -0.19829261302947998f + ) + ); + CHECK(camera.scale == 0.06643116474151611f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } + { + const SessionRecording::Entry& e = rec.entries[2]; + CHECK(e.timestamp == 0.005884706974029541); + CHECK(e.simulationTime == 779267268.7753198); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(2560146.3323334977, -5779694.568918271, -852442.7158528116) + ); + CHECK( + camera.rotation == + glm::quat( + 0.6254600882530212f, + 0.5630295276641846f, + 0.502471387386322f, + -0.19829261302947998f + ) + ); + CHECK(camera.scale == 0.06643116474151611f); + CHECK(camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } + { + const SessionRecording::Entry& e = rec.entries[3]; + CHECK(e.timestamp == 10.153814885416068); + CHECK(e.simulationTime == 779267278.9232514); + REQUIRE(std::holds_alternative(e.value)); + const auto& script = std::get(e.value); + CHECK(script == "openspace.setPropertyValueSingle(\"Scene.Venus.Renderable.Layers.ColorLayers.Clouds_Magellan_Combo_Utah.Fade\",0)"); + } + { + const SessionRecording::Entry& e = rec.entries[4]; + CHECK(e.timestamp == 10.155818674364127); + CHECK(e.simulationTime == 779267278.9252552); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-57303121.02243042, 8346999.591819763, -128851858.36139679) + ); + CHECK( + camera.rotation == + glm::quat( + 0.1360674947500229f, + 0.658237636089325f, + -0.7229072451591492f, + -0.16004367172718048f + ) + ); + CHECK(camera.scale == 0.0001590096508152783f); + CHECK(!camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } + { + const SessionRecording::Entry& e = rec.entries[5]; + CHECK(e.timestamp == 27.533842067583464); + CHECK(e.simulationTime == 779267296.303281); + REQUIRE(std::holds_alternative(e.value)); + const auto& camera = std::get(e.value); + CHECK( + camera.position == + glm::dvec3(-4573226225.966583, 667900806.931778, -10309469556.229485) + ); + CHECK( + camera.rotation == + glm::quat( + 0.13572217524051666f, + 0.6582902073860168f, + -0.7229912281036377f, + -0.15974101424217224f + ) + ); + CHECK(camera.scale == 0.0000019040056713492959f); + CHECK(!camera.followFocusNodeRotation); + CHECK(camera.focusNode == "Venus"); + } +} + +TEST_CASE("SessionRecording: 02.00 Binary Linux Roundtrip", "[sessionrecording]") { + SessionRecording rec = loadSessionRecording(test("0200_binary_linux.osrec")); + saveSessionRecording(absPath("${TEMPORARY}/ascii"), rec, DataMode::Ascii); + saveSessionRecording(absPath("${TEMPORARY}/binary"), rec, DataMode::Binary); + SessionRecording a = loadSessionRecording(absPath("${TEMPORARY}/ascii")); + SessionRecording b = loadSessionRecording(absPath("${TEMPORARY}/binary")); + + CHECK(rec == a); + CHECK(rec == b); + CHECK(a == b); +}