diff --git a/data/assets/recording_test.scene b/data/assets/recording_test.scene new file mode 100644 index 0000000000..470b8bca3b --- /dev/null +++ b/data/assets/recording_test.scene @@ -0,0 +1,34 @@ +local sceneHelper = asset.require('util/scene_helper') +local propertyHelper = asset.require('util/property_helper') + +-- Specifying which other assets should be loaded in this scene +asset.require('spice/base') +assetHelper.requestAll(asset, 'scene/solarsystem/sun') +asset.require('scene/solarsystem/planets/earth/earth') +asset.require('scene/digitaluniverse/constellationbounds') +asset.require('util/default_keybindings') +asset.require('util/default_dashboard') + +asset.onInitialize(function () + openspace.time.setTime("2010 AUG 01") + + + openspace.navigation.setCameraState({ + Focus = "Earth", + Position = { -1500000000000, 0, 0 }, + Rotation = { 0.0, 0.0, 1.0, 1.0 }, + }) + + openspace.navigation.setCameraState({ + Focus = "Sun", + Position = { 100000000000, 0, 0 }, + Rotation = { 0.0, 0.0, 1.0, 1.0 }, + }) + + openspace.setPropertyValue('Scene.ConstellationBounds.renderable.Enabled', true) + openspace.setPropertyValueSingle('Scene.Earth.RenderableGlobe.Enabled', false); + openspace.setPropertyValueSingle('Scene.EarthTrail.renderable.Enabled', false); + openspace.time.interpolateTogglePause() + +end) + diff --git a/include/openspace/engine/globals.h b/include/openspace/engine/globals.h index 8bb19ff8d6..0cb2327489 100644 --- a/include/openspace/engine/globals.h +++ b/include/openspace/engine/globals.h @@ -54,6 +54,7 @@ namespace interaction { struct JoystickInputStates; class KeybindingManager; class NavigationHandler; + class SessionRecording; class ShortcutManager; } // namespace interaction namespace performance { class PerformanceManager; } @@ -88,6 +89,7 @@ configuration::Configuration& gConfiguration(); interaction::JoystickInputStates& gJoystickInputStates(); interaction::KeybindingManager& gKeybindingManager(); interaction::NavigationHandler& gNavigationHandler(); +interaction::SessionRecording& gSessionRecording(); interaction::ShortcutManager& gShortcutManager(); performance::PerformanceManager& gPerformanceManager(); properties::PropertyOwner& gRootPropertyOwner(); @@ -120,6 +122,7 @@ static interaction::JoystickInputStates& joystickInputStates = detail::gJoystickInputStates(); static interaction::KeybindingManager& keybindingManager = detail::gKeybindingManager(); static interaction::NavigationHandler& navigationHandler = detail::gNavigationHandler(); +static interaction::SessionRecording& sessionRecording = detail::gSessionRecording(); static interaction::ShortcutManager& shortcutManager = detail::gShortcutManager(); static performance::PerformanceManager& performanceManager = detail::gPerformanceManager(); diff --git a/include/openspace/interaction/externinteraction.h b/include/openspace/interaction/externinteraction.h new file mode 100644 index 0000000000..32238f4fc2 --- /dev/null +++ b/include/openspace/interaction/externinteraction.h @@ -0,0 +1,82 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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___EXTERNINTERACTION___H__ +#define __OPENSPACE_CORE___EXTERNINTERACTION___H__ + +#include +#include +#include + +#include + +namespace openspace { + +class ExternInteraction : public properties::PropertyOwner { +public: + ExternInteraction(); + /** + * Method that generates a keyframeNavigator CameraPose from a CameraKeyframe + * object, and then adds this to the navigationHandler's keyframe navigator. + * \param kf The camera keyframe to add. + */ + void cameraInteraction(datamessagestructures::CameraKeyframe kf); + /** + * Method that generates a TimeKeyframeData from a TimeKeyframe object, and + * then adds this to the timeManager. + * \param kf The time keyframe to add. + */ + void timeInteraction(datamessagestructures::TimeKeyframe kf); + /** + * Method that passes a ScriptMessage object to the script engine, calling its + * queueScript method to add it for execution. + * \param sm The ScriptMessage object to queue in the script engine. + */ + void scriptInteraction(datamessagestructures::ScriptMessage sm); + /** + * Method that accepts a reference to a CameraKeyframe object, and populates + * it with the current properties of the camera from the navigation handler. + * \returns CameraKeyframe with current state from NavigationHandler. + */ + datamessagestructures::CameraKeyframe generateCameraKeyframe(); + /** + * Method that accepts a reference to a TimeKeyframe object, and populates + * it with the current time values from the application time manager. + * \returns TimeKeyframe The time keyframe. + */ + datamessagestructures::TimeKeyframe generateTimeKeyframe(); + /** + * Method that accepts a reference to a ScriptMessage object and a script + * string, and populates the ScriptMessage with the script and timestamp + * of the current application time. + * \param script The script to execute in std::string form. + * \returns ScriptMessage The ScriptMessage data structure with script. + */ + datamessagestructures::ScriptMessage generateScriptMessage(std::string script); +private: +}; + +} // namespace openspace + +#endif // __OPENSPACE_CORE___EXTERNINTERACTION___H__ diff --git a/include/openspace/interaction/keyframenavigator.h b/include/openspace/interaction/keyframenavigator.h index 8d9114effb..b9f47b67c1 100644 --- a/include/openspace/interaction/keyframenavigator.h +++ b/include/openspace/interaction/keyframenavigator.h @@ -31,10 +31,19 @@ #include #include -namespace openspace { class Camera; } +namespace openspace { + class Camera; + class TimeManager; +} // namespace openspace namespace openspace::interaction { +enum class KeyframeTimeRef { + Relative_applicationStart, + Relative_recordedStart, + Absolute_simTimeJ2000 +}; + class KeyframeNavigator { public: BooleanType(Inclusive); @@ -47,16 +56,31 @@ public: bool followFocusNodeRotation; }; - void updateCamera(Camera& camera); + /** + * Update camera position using the next camera pose keyframe from the timeline. + * Returns true if camera was set to a pose from the next keyframe. + * Returns false if no keyframes are available after the current time. + * \param camera A reference to the camera object to have its pose updated. + * \param ignoreFutureKeyframes true if only past keyframes are to be used. + * \returns true only if a new future keyframe is available to set camera pose. + */ + bool updateCamera(Camera& camera, bool ignoreFutureKeyframes); + static bool updateCamera(Camera* camera, const CameraPose prevPose, + const CameraPose nextPose, double t, bool ignoreFutureKeyframes); + Timeline& timeline(); - void addKeyframe(double timestamp, KeyframeNavigator::CameraPose pose); void removeKeyframesAfter(double timestamp, Inclusive inclusive = Inclusive::No); void clearKeyframes(); size_t nKeyframes() const; + const std::vector& keyframes() const; + double currentTime() const; + void setTimeReferenceMode(KeyframeTimeRef refType, double referenceTimestamp); private: Timeline _cameraPoseTimeline; + KeyframeTimeRef _timeframeMode = KeyframeTimeRef::Relative_applicationStart; + double _referenceTimestamp = 0.0; }; } // namespace openspace::interaction diff --git a/include/openspace/interaction/navigationhandler.h b/include/openspace/interaction/navigationhandler.h index eb287d0372..e064b1f379 100644 --- a/include/openspace/interaction/navigationhandler.h +++ b/include/openspace/interaction/navigationhandler.h @@ -61,8 +61,11 @@ public: void setInterpolationTime(float durationInSeconds); void setCameraStateFromDictionary(const ghoul::Dictionary& cameraDict); - void updateCamera(double deltaTime); + void setEnableKeyFrameInteraction(); + void setDisableKeyFrameInteraction(); + void triggerPlaybackStart(); + void stopPlayback(); // Accessors ghoul::Dictionary cameraStateDictionary(); @@ -73,6 +76,7 @@ public: const InputState& inputState() const; const OrbitalNavigator& orbitalNavigator() const; KeyframeNavigator& keyframeNavigator() const; + bool isKeyFrameInteractionEnabled() const; float interpolationTime() const; // Callback functions @@ -113,9 +117,11 @@ public: private: bool _cameraUpdatedFromScript = false; + bool _playbackModeEnabled = false; std::unique_ptr _inputState; Camera* _camera = nullptr; + std::function _playbackEndCallback; std::unique_ptr _orbitalNavigator; std::unique_ptr _keyframeNavigator; diff --git a/include/openspace/interaction/sessionrecording.h b/include/openspace/interaction/sessionrecording.h new file mode 100644 index 0000000000..14cccf89f4 --- /dev/null +++ b/include/openspace/interaction/sessionrecording.h @@ -0,0 +1,268 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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___SESSIONRECORDING___H__ +#define __OPENSPACE_CORE___SESSIONRECORDING___H__ + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace openspace::interaction { +#define RECORD_BINARY + +class KeyframeNavigator; + +class SessionRecording : public properties::PropertyOwner { +public: + enum class RecordedDataMode { + Ascii, + Binary + }; + + SessionRecording(); + ~SessionRecording(); + /** + * 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(); + + /** + * 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. + * \returns true if recording to file starts without errors. + */ + bool startRecording(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 filename file saved with recorded keyframes. + * \returns true if recording to file starts without errors. + */ + void setRecordDataFormat(RecordedDataMode 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. + * \returns 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 + * \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. + * \returns true if recording to file starts without errors. + */ + bool startPlayback(const std::string& filename, KeyframeTimeRef timeMode, + bool forceSimTimeAtStart); + + /** + * Used to stop a playback in progress. If open, the playback file will be closed, + * and all keyframes deleted from memory. + */ + void stopPlayback(); + + /** + * Used to check if a session playback is in progress. + * \returns true if playback is in progress. + */ + bool isPlayingBack() 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 saveCameraKeyframe(); + + /** + * 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 saveTimeKeyframe(); + + /** + * Used to trigger a save of a script to the recording file, but only if a recording + * is currently in progress. + * \param scriptToSave String of the Lua command to be saved. + */ + void saveScriptKeyframe(std::string scriptToSave); + + /** + * \return The Lua library that contains all Lua functions available to affect the + * interaction + */ + static openspace::scripting::LuaLibrary luaLibrary(); + +private: + enum class SessionState { + Idle = 0, + Recording, + Playback + }; + enum class RecordedType { + Camera = 0, + Time, + Script, + Invalid + }; + struct timelineEntry { + RecordedType keyframeType; + unsigned int idxIntoKeyframeTypeArray; + double timestamp; + }; + ExternInteraction _externInteract; + bool _isRecording = false; + double _timestampRecordStarted; + double _timestampPlaybackStarted_application; + double _timestampPlaybackStarted_simulation; + double _timestampApplicationStarted_simulation; + bool hasCameraChangedFromPrev(datamessagestructures::CameraKeyframe kfNew); + double appropriateTimestamp(double timeOs, double timeRec, double timeSim); + double equivalentSimulationTime(double timeOs, double timeRec, double timeSim); + double equivalentApplicationTime(double timeOs, double timeRec, double timeSim); + double currentTime() const; + + void playbackCamera(); + void playbackTimeChange(); + void playbackScript(); + bool playbackAddEntriesToTimeline(); + void signalPlaybackFinishedForComponent(RecordedType type); + void writeToFileBuffer(const double src); + void writeToFileBuffer(std::vector& cvec); + void writeToFileBuffer(const unsigned char c); + void writeToFileBuffer(bool b); + void saveStringToFile(const std::string s); + void saveKeyframeToFileBinary(unsigned char* bufferSource, size_t size); + void findFirstCameraKeyframeInTimeline(); + std::string readHeaderElement(size_t readLen_chars); + void readFromPlayback(unsigned char& result); + void readFromPlayback(double& result); + void readFromPlayback(float& result); + void readFromPlayback(size_t& result); + void readFromPlayback(bool& result); + void readFromPlayback(std::string& result); + void saveKeyframeToFile(std::string entry); + + void addKeyframe(double timestamp, interaction::KeyframeNavigator::CameraPose keyframe); + void addKeyframe(double timestamp, datamessagestructures::TimeKeyframe keyframe); + void addKeyframe(double timestamp, std::string scriptToQueue); + 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 isDataModeBinary(); + unsigned int findIndexOfLastCameraKeyframeInTimeline(); + bool doesTimelineEntryContainCamera(unsigned int index) const; + + RecordedType getNextKeyframeType(); + RecordedType getPrevKeyframeType(); + double getNextTimestamp(); + double getPrevTimestamp(); + void cleanUpPlayback(); + + const bool _usingTimeKeyframes = false; + const std::string _fileHeaderTitle = "OpenSpace_record/playback"; + static const size_t _fileHeaderVersionLength = 5; + const char _fileHeaderVersion[_fileHeaderVersionLength] = { '0', '0', '.', '8', '5' }; + const char dataFormatAsciiTag = 'A'; + const char dataFormatBinaryTag = 'B'; + + RecordedDataMode _recordingDataMode = RecordedDataMode::Binary; + SessionState _state = SessionState::Idle; + std::string _playbackFilename; + std::ifstream _playbackFile; + std::string _playbackLineParsing; + std::ofstream _recordFile; + int _playbackLineNum = 1; + KeyframeTimeRef _playbackTimeReferenceMode; + datamessagestructures::CameraKeyframe _prevRecordedCameraKeyframe; + bool _playbackActive_camera = false; + bool _playbackActive_time = false; + bool _playbackActive_script = false; + bool _hasHitEndOfCameraKeyframes = false; + bool _setSimulationTimeWithNextCameraKeyframe = false; + + static const size_t keyframeHeaderSize_bytes = 33; + static const size_t saveBufferCameraSize_min = 82; + static const size_t saveBufferStringSize_max = 500; + static const size_t _saveBufferMaxSize_bytes = keyframeHeaderSize_bytes + + + saveBufferCameraSize_min + + saveBufferStringSize_max; + unsigned char _keyframeBuffer[_saveBufferMaxSize_bytes]; + size_t _bufferIndex = 0; + + bool _cleanupNeeded = false; + + std::vector < interaction::KeyframeNavigator::CameraPose> _keyframesCamera; + std::vector _keyframesTime; + std::vector _keyframesScript; + std::vector _timeline; + + 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; +}; + +} // namespace openspace + +#include "sessionrecording.inl" + +#endif // __OPENSPACE_CORE___SESSIONRECORDING___H__ diff --git a/include/openspace/interaction/sessionrecording.inl b/include/openspace/interaction/sessionrecording.inl new file mode 100644 index 0000000000..70fef838fa --- /dev/null +++ b/include/openspace/interaction/sessionrecording.inl @@ -0,0 +1,57 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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(); + } +} + + +} // namespace openspace::interaction diff --git a/include/openspace/network/messagestructures.h b/include/openspace/network/messagestructures.h index b32e1109a5..9a92cc8e5b 100644 --- a/include/openspace/network/messagestructures.h +++ b/include/openspace/network/messagestructures.h @@ -29,6 +29,7 @@ #include #include #include +#include namespace openspace::datamessagestructures { @@ -142,6 +143,96 @@ struct CameraKeyframe { return offset; }; + + void write(std::ostream& out) const { + // Write position + out.write( + reinterpret_cast(&_position), + sizeof(_position) + ); + + // Write orientation + out.write( + reinterpret_cast(&_rotation), + sizeof(_rotation) + ); + + // Write follow focus node rotation? + out.write( + reinterpret_cast(&_followNodeRotation), + sizeof(_followNodeRotation) + ); + + int nodeNameLength = static_cast(_focusNode.size()); + + // Write focus node + out.write( + reinterpret_cast(&nodeNameLength), + sizeof(nodeNameLength) + ); + out.write( + _focusNode.c_str(), + _focusNode.size() + ); + + //Write scale + out.write( + reinterpret_cast(&_scale), + sizeof(_scale) + ); + + // Write timestamp + out.write( + reinterpret_cast(&_timestamp), + sizeof(_timestamp) + ); + }; + + void read(std::istream* in) { + // Read position + in->read( + reinterpret_cast(&_position), + sizeof(_position) + ); + + // Read orientation + in->read( + reinterpret_cast(&_rotation), + sizeof(_rotation) + ); + + // Read follow focus node rotation + unsigned char b; + in->read( + reinterpret_cast(&b), + sizeof(unsigned char) + ); + _followNodeRotation = (b == 1); + + // Read focus node + int nodeNameLength = static_cast(_focusNode.size()); + in->read( + reinterpret_cast(&nodeNameLength), + sizeof(nodeNameLength) + ); + std::vector temp(nodeNameLength + 1); + in->read(temp.data(), nodeNameLength); + + temp[nodeNameLength] = '\0'; + _focusNode = temp.data(); + + // Read scale + in->read( + reinterpret_cast(&_scale), + sizeof(_scale) + ); + + // Read timestamp + in->read( + reinterpret_cast(&_timestamp), + sizeof(_timestamp) + ); + }; }; struct TimeKeyframe { @@ -169,6 +260,20 @@ struct TimeKeyframe { offset += sizeof(TimeKeyframe); return offset; }; + + void write(std::ostream* out) const { + out->write( + reinterpret_cast(this), + sizeof(TimeKeyframe) + ); + }; + + void read(std::istream* in) { + in->read( + reinterpret_cast(this), + sizeof(TimeKeyframe) + ); + }; }; struct TimeTimeline { @@ -216,6 +321,38 @@ struct TimeTimeline { } return offset; }; + + void write(std::ostream* out) const { + out->write( + reinterpret_cast(&_clear), + sizeof(bool) + ); + + int64_t nKeyframes = _keyframes.size(); + out->write( + reinterpret_cast(&nKeyframes), + sizeof(int64_t) + ); + for (const auto& k : _keyframes) { + k.write(out); + } + }; + + void read(std::istream* in) { + in->read( + reinterpret_cast(&_clear), + sizeof(bool) + ); + + int64_t nKeyframes = _keyframes.size(); + in->read( + reinterpret_cast(&nKeyframes), + sizeof(int64_t) + ); + for (auto& k : _keyframes) { + k.read(in); + } + }; }; struct ScriptMessage { @@ -225,6 +362,7 @@ struct ScriptMessage { } std::string _script; + double _timestamp; void serialize(std::vector &buffer) const { buffer.insert(buffer.end(), _script.begin(), _script.end()); @@ -233,6 +371,23 @@ struct ScriptMessage { void deserialize(const std::vector &buffer) { _script.assign(buffer.begin(), buffer.end()); }; + + void write(std::ostream* out) const { + out->write(_script.c_str(), _script.size()); + }; + + void read(std::istream* in) { + size_t strLen; + //Read string length from file + in->read(reinterpret_cast(&strLen), sizeof(strLen)); + //Read back full string + std::vector temp(strLen + 1); + in->read(temp.data(), strLen); + temp[strLen] = '\0'; + + _script.erase(); + _script = temp.data(); + }; }; } // namespace openspace::messagestructures diff --git a/include/openspace/network/parallelpeer.h b/include/openspace/network/parallelpeer.h index 7815baf1e5..c5994f2f22 100644 --- a/include/openspace/network/parallelpeer.h +++ b/include/openspace/network/parallelpeer.h @@ -26,6 +26,7 @@ #define __OPENSPACE_CORE___PARALLELPEER___H__ #include +#include #include #include @@ -132,6 +133,8 @@ private: std::unique_ptr _receiveThread = nullptr; std::shared_ptr> _connectionEvent; + ExternInteraction _externInteract; + ParallelConnection _connection; TimeManager::CallbackHandle _timeJumpCallback = -1; diff --git a/include/openspace/scripting/scriptscheduler.h b/include/openspace/scripting/scriptscheduler.h index 7f6f29d712..5afd2a7ca6 100644 --- a/include/openspace/scripting/scriptscheduler.h +++ b/include/openspace/scripting/scriptscheduler.h @@ -25,8 +25,21 @@ #ifndef __OPENSPACE_CORE___SCRIPTSCHEDULER___H__ #define __OPENSPACE_CORE___SCRIPTSCHEDULER___H__ +#include +#include + + +#include #include #include +#include + +namespace { + constexpr const char* KeyTime = "Time"; + constexpr const char* KeyForwardScript = "ForwardScript"; + constexpr const char* KeyBackwardScript = "BackwardScript"; + constexpr const char* KeyUniversalScript = "Script"; +} // namespace namespace ghoul { class Dictionary; } namespace openspace::documentation { struct Documentation; } @@ -71,15 +84,16 @@ public: void clearSchedule(); /** - * Progresses the script schedulers time and returns all scripts that has been - * scheduled to run between \param newTime and the time provided in the last - * invocation of this method. - * - * \param newTime A j2000 time value specifying the new time stamp that - * the script scheduler should progress to. - * - * \returns the ordered queue of scripts . - */ + * Progresses the script schedulers time and returns all scripts that has been + * scheduled to run between \param newTime and the time provided in the last invocation + * of this method. + * + * \param newTime_simulation A j2000 time value specifying the new time stamp that + * the script scheduler should progress to. + * \param newTime_application The seconds elapsed since the application started + * + * \returns the ordered queue of scripts . + */ // std::queue progressTo(double newTime); /** @@ -103,8 +117,29 @@ public: */ std::vector allScripts() const; + /** + * Sets the mode for how each scheduled script's timestamp will be interpreted. + * \param refType reference mode (for exact syntax, see definition of + * openspace::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); + + /** + * Sets the mode for scripts being run from playback + */ + void triggerPlaybackStart(); + + /** + * Sets the flag for scripts no longer being run from playback + */ + void stopPlayback(); static LuaLibrary luaLibrary(); + void setModeApplicationTime(); + void setModeRecordedTime(); + void setModeSimulationTime(); static documentation::Documentation Documentation(); @@ -115,6 +150,10 @@ private: int _currentIndex = 0; double _currentTime = 0; + bool _playbackModeEnabled = false; + + openspace::interaction::KeyframeTimeRef _timeframeMode + = openspace::interaction::KeyframeTimeRef::Absolute_simTimeJ2000; }; } // namespace openspace::scripting diff --git a/include/openspace/util/timemanager.h b/include/openspace/util/timemanager.h index 7714bf89a4..80dc1faba7 100644 --- a/include/openspace/util/timemanager.h +++ b/include/openspace/util/timemanager.h @@ -103,6 +103,7 @@ public: void removeTimeChangeCallback(CallbackHandle handle); void removeDeltaTimeChangeCallback(CallbackHandle handle); + void triggerPlaybackStart(); void removeTimeJumpCallback(CallbackHandle handle); void removeTimelineChangeCallback(CallbackHandle handle); @@ -134,9 +135,11 @@ private: double _latestConsumedTimestamp = -std::numeric_limits::max(); int _nextCallbackHandle = 0; + bool _playbackModeEnabled = false; std::vector> _timeChangeCallbacks; std::vector> _deltaTimeChangeCallbacks; + std::vector> _timeJumpCallbacks; std::vector> _timelineChangeCallbacks; }; diff --git a/modules/imgui/imguimodule.cpp b/modules/imgui/imguimodule.cpp index 09db9dab73..b00adb0f32 100644 --- a/modules/imgui/imguimodule.cpp +++ b/modules/imgui/imguimodule.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ ImGUIModule::ImGUIModule() : OpenSpaceModule(Name) { []() { std::vector res = { &global::navigationHandler, + &global::sessionRecording, &global::timeManager, &global::renderEngine, &global::parallelPeer, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 43688e547e..0d459108ff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,9 @@ set(OPENSPACE_SOURCE ${OPENSPACE_BASE_DIR}/src/interaction/navigationhandler_lua.inl ${OPENSPACE_BASE_DIR}/src/interaction/mousecamerastates.cpp ${OPENSPACE_BASE_DIR}/src/interaction/orbitalnavigator.cpp + ${OPENSPACE_BASE_DIR}/src/interaction/externinteraction.cpp + ${OPENSPACE_BASE_DIR}/src/interaction/sessionrecording.cpp + ${OPENSPACE_BASE_DIR}/src/interaction/sessionrecording_lua.inl ${OPENSPACE_BASE_DIR}/src/interaction/shortcutmanager.cpp ${OPENSPACE_BASE_DIR}/src/interaction/shortcutmanager_lua.inl ${OPENSPACE_BASE_DIR}/src/mission/mission.cpp @@ -232,6 +235,8 @@ set(OPENSPACE_HEADER ${OPENSPACE_BASE_DIR}/include/openspace/interaction/mousecamerastates.h ${OPENSPACE_BASE_DIR}/include/openspace/interaction/navigationhandler.h ${OPENSPACE_BASE_DIR}/include/openspace/interaction/orbitalnavigator.h + ${OPENSPACE_BASE_DIR}/include/openspace/interaction/externinteraction.h + ${OPENSPACE_BASE_DIR}/include/openspace/interaction/sessionrecording.h ${OPENSPACE_BASE_DIR}/include/openspace/interaction/shortcutmanager.h ${OPENSPACE_BASE_DIR}/include/openspace/mission/mission.h ${OPENSPACE_BASE_DIR}/include/openspace/mission/missionmanager.h diff --git a/src/documentation/core_registration.cpp b/src/documentation/core_registration.cpp index 93d1f149f6..dc47ca16ed 100644 --- a/src/documentation/core_registration.cpp +++ b/src/documentation/core_registration.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,7 @@ void registerCoreClasses(scripting::ScriptEngine& engine) { engine.addLibrary(Time::luaLibrary()); engine.addLibrary(interaction::KeybindingManager::luaLibrary()); engine.addLibrary(interaction::NavigationHandler::luaLibrary()); + engine.addLibrary(interaction::SessionRecording::luaLibrary()); engine.addLibrary(interaction::ShortcutManager::luaLibrary()); engine.addLibrary(scripting::ScriptScheduler::luaLibrary()); engine.addLibrary(scripting::generalSystemCapabilities()); diff --git a/src/engine/globals.cpp b/src/engine/globals.cpp index b97011adeb..6d173dfa95 100644 --- a/src/engine/globals.cpp +++ b/src/engine/globals.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +164,12 @@ interaction::NavigationHandler& gNavigationHandler() { return g; } +interaction::SessionRecording& gSessionRecording() { + static interaction::SessionRecording g; + return g; +} + + interaction::ShortcutManager& gShortcutManager() { static interaction::ShortcutManager g; return g; @@ -201,6 +208,7 @@ void initialize() { global::navigationHandler.setPropertyOwner(&global::rootPropertyOwner); // New property subowners also have to be added to the ImGuiModule callback! global::rootPropertyOwner.addPropertySubOwner(global::navigationHandler); + global::rootPropertyOwner.addPropertySubOwner(global::sessionRecording); global::rootPropertyOwner.addPropertySubOwner(global::timeManager); global::rootPropertyOwner.addPropertySubOwner(global::renderEngine); diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index ab6aba63c7..c4142f46d7 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -785,6 +786,7 @@ void OpenSpaceEngine::deinitialize() { global::renderEngine.scene()->camera()->getSyncables() ); } + global::sessionRecording.deinitialize(); global::deinitialize(); @@ -931,6 +933,8 @@ void OpenSpaceEngine::writeSceneDocumentation() { void OpenSpaceEngine::preSynchronization() { LTRACE("OpenSpaceEngine::preSynchronization(begin)"); + //std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::unique_ptr perf; if (global::performanceManager.isEnabled()) { perf = std::make_unique( @@ -972,6 +976,7 @@ void OpenSpaceEngine::preSynchronization() { global::renderEngine.updateScene(); //_navigationHandler->updateCamera(dt); + if (_scene) { Camera* camera = _scene->camera(); if (camera) { @@ -979,6 +984,7 @@ void OpenSpaceEngine::preSynchronization() { camera->invalidateCache(); } } + global::sessionRecording.preSynchronization(); global::parallelPeer.preSynchronization(); } diff --git a/src/interaction/externinteraction.cpp b/src/interaction/externinteraction.cpp new file mode 100644 index 0000000000..054bab25b9 --- /dev/null +++ b/src/interaction/externinteraction.cpp @@ -0,0 +1,158 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 + +namespace { +const uint32_t ProtocolVersion = 3; +const size_t MaxLatencyDiffs = 64; +const char* _loggerCat = "ExternInteraction"; + +static const openspace::properties::Property::PropertyInfo BufferTimeInfo = { + "BufferTime", + "Buffer Time", + "" // @TODO Missing documentation +}; + +static const openspace::properties::Property::PropertyInfo TimeKeyFrameInfo = { + "TimeKeyframeInterval", + "Time keyframe interval", + "" // @TODO Missing documentation +}; + +static const openspace::properties::Property::PropertyInfo CameraKeyFrameInfo = { + "CameraKeyframeInterval", + "Camera Keyframe interval", + "" // @TODO Missing documentation +}; + +static const openspace::properties::Property::PropertyInfo TimeToleranceInfo = { + "TimeTolerance", + "Time tolerance", + "" // @TODO Missing documentation +}; + +} // namespace + +namespace openspace { + +ExternInteraction::ExternInteraction() + : properties::PropertyOwner({ "ExternInteration", "External Interaction" }) +{ +} + +void ExternInteraction::cameraInteraction(datamessagestructures::CameraKeyframe kf) { + interaction::KeyframeNavigator::CameraPose pose; + pose.focusNode = std::move(kf._focusNode); + pose.position = std::move(kf._position); + pose.rotation = std::move(kf._rotation); + pose.scale = std::move(kf._scale); + pose.followFocusNodeRotation = std::move(kf._followNodeRotation); + + global::navigationHandler.keyframeNavigator().addKeyframe(kf._timestamp, pose); +} + +void ExternInteraction::timeInteraction(datamessagestructures::TimeKeyframe kf) { + TimeKeyframeData timeKfData; + timeKfData.delta = std::move(kf._dt); + timeKfData.pause = std::move(kf._paused); + timeKfData.jump = std::move(kf._requiresTimeJump); + + global::timeManager.addKeyframe(kf._timestamp, timeKfData); +} + +void ExternInteraction::scriptInteraction(datamessagestructures::ScriptMessage sm) { + global::scriptEngine.queueScript( + std::move(sm._script), + scripting::ScriptEngine::RemoteScripting::No + ); +} + +datamessagestructures::CameraKeyframe ExternInteraction::generateCameraKeyframe() { + datamessagestructures::CameraKeyframe kf; + SceneGraphNode* focusNode = global::navigationHandler.focusNode(); + if (!focusNode) { + return kf; + } + + //kf._position = global::navigationHandler.camera()->positionVec3(); + kf._position = global::navigationHandler.focusNodeToCameraVector(); + + kf._followNodeRotation = + global::navigationHandler.orbitalNavigator().followingNodeRotation(); + if (kf._followNodeRotation) { + kf._position = glm::inverse(focusNode->worldRotationMatrix()) * kf._position; + kf._rotation = global::navigationHandler.focusNodeToCameraRotation(); + } + else { + kf._rotation = global::navigationHandler.camera()->rotationQuaternion(); + } + + kf._focusNode = focusNode->identifier(); + kf._scale = global::navigationHandler.camera()->scaling(); + + // Timestamp as current runtime of OpenSpace instance + kf._timestamp = global::windowDelegate.applicationTime(); + + return kf; +} + +datamessagestructures::TimeKeyframe ExternInteraction::generateTimeKeyframe() { + datamessagestructures::TimeKeyframe kf; + const Time& time = global::timeManager.time(); + + kf._dt = global::timeManager.deltaTime(); + kf._paused = global::timeManager.isPaused(); + kf._time = time.j2000Seconds(); + + // Timestamp as current runtime of OpenSpace instance + kf._timestamp = global::windowDelegate.applicationTime(); + return kf; +} + +datamessagestructures::ScriptMessage ExternInteraction::generateScriptMessage(std::string script) { + datamessagestructures::ScriptMessage sm; + sm._script = std::move(script); + // Timestamp as current runtime of OpenSpace instance + sm._timestamp = global::windowDelegate.applicationTime(); + return sm; +} + + +} // namespace openspace diff --git a/src/interaction/keyframenavigator.cpp b/src/interaction/keyframenavigator.cpp index 20151ffa98..f2fb5e5325 100644 --- a/src/interaction/keyframenavigator.cpp +++ b/src/interaction/keyframenavigator.cpp @@ -30,27 +30,35 @@ #include #include #include +#include #include +#include + namespace openspace::interaction { -void KeyframeNavigator::updateCamera(Camera& camera) { - double now = global::windowDelegate.applicationTime(); +bool KeyframeNavigator::updateCamera(Camera& camera, bool ignoreFutureKeyframes) { + double now = currentTime(); + bool foundPrevKeyframe = false; if (_cameraPoseTimeline.nKeyframes() == 0) { - return; + return false; } const Keyframe* nextKeyframe = - _cameraPoseTimeline.firstKeyframeAfter(now); + _cameraPoseTimeline.firstKeyframeAfter(now); const Keyframe* prevKeyframe = - _cameraPoseTimeline.lastKeyframeBefore(now); + _cameraPoseTimeline.lastKeyframeBefore(now); double nextTime = 0.0; if (nextKeyframe) { nextTime = nextKeyframe->timestamp; - } else { - return; + } + else { + if (ignoreFutureKeyframes) { + _cameraPoseTimeline.removeKeyframesBefore(now); + } + return false; } double prevTime = 0.0; @@ -58,24 +66,33 @@ void KeyframeNavigator::updateCamera(Camera& camera) { if (prevKeyframe) { prevTime = prevKeyframe->timestamp; t = (now - prevTime) / (nextTime - prevTime); - } else { + foundPrevKeyframe = true; + } + else { // If there is no keyframe before: Only use the next keyframe. prevTime = nextTime; prevKeyframe = nextKeyframe; t = 1; } + const CameraPose prevPose = prevKeyframe->data; + const CameraPose nextPose = nextKeyframe->data; _cameraPoseTimeline.removeKeyframesBefore(prevTime); - const CameraPose& prevPose = prevKeyframe->data; - const CameraPose& nextPose = nextKeyframe->data; + if (!foundPrevKeyframe && ignoreFutureKeyframes) { + return false; + } - Scene* scene = camera.parent()->scene(); + return updateCamera(&camera, prevKeyframe->data, nextKeyframe->data, t, ignoreFutureKeyframes); +} + +bool KeyframeNavigator::updateCamera(Camera* camera, const CameraPose prevPose, const CameraPose nextPose, double t, bool ignoreFutureKeyframes) { + Scene* scene = camera->parent()->scene(); SceneGraphNode* prevFocusNode = scene->sceneGraphNode(prevPose.focusNode); SceneGraphNode* nextFocusNode = scene->sceneGraphNode(nextPose.focusNode); if (!prevFocusNode || !nextFocusNode) { - return; + return false; } glm::dvec3 prevKeyframeCameraPosition = prevPose.position; @@ -107,22 +124,65 @@ void KeyframeNavigator::updateCamera(Camera& camera) { nextKeyframeCameraPosition += nextFocusNode->worldPosition(); // Linear interpolation - camera.setPositionVec3( + t = std::max(0.0, std::min(1.0, t)); + camera->setPositionVec3( prevKeyframeCameraPosition * (1 - t) + nextKeyframeCameraPosition * t ); - camera.setRotation( + camera->setRotation( glm::slerp(prevKeyframeCameraRotation, nextKeyframeCameraRotation, t) ); - + // We want to affect view scaling, such that we achieve // logarithmic interpolation of distance to an imagined focus node. // To do this, we interpolate the scale reciprocal logarithmically. - const float prevInvScaleExp = glm::log(1.f / prevPose.scale); - const float nextInvScaleExp = glm::log(1.f / nextPose.scale); - const float interpolatedInvScaleExp = static_cast( - prevInvScaleExp * (1 - t) + nextInvScaleExp * t - ); - camera.setScaling(1.f / glm::exp(interpolatedInvScaleExp)); + if (!ignoreFutureKeyframes) { + const float prevInvScaleExp = glm::log(1.f / prevPose.scale); + const float nextInvScaleExp = glm::log(1.f / nextPose.scale); + const float interpolatedInvScaleExp = static_cast( + prevInvScaleExp * (1 - t) + nextInvScaleExp * t + ); + camera->setScaling(1.f / glm::exp(interpolatedInvScaleExp)); + } + +#ifdef INTERPOLATION_DEBUG_PRINT + LINFO(fmt::format("Cam pos prev={}, next={}", std::to_string(prevKeyframeCameraPosition), + std::to_string(nextKeyframeCameraPosition))); + LINFO(fmt::format("Cam rot prev={} {} {} {} next={} {} {} {}", prevKeyframeCameraRotation.x, + prevKeyframeCameraRotation.y, prevKeyframeCameraRotation.z, prevKeyframeCameraRotation.w, + nextKeyframeCameraRotation.x, nextKeyframeCameraRotation.y, nextKeyframeCameraRotation.z, + nextKeyframeCameraRotation.w)); + LINFO(fmt::format("Cam interp = {}", t)); + LINFO(fmt::format("camera {} {} {} {} {} {}", global::windowDelegate.applicationTime(), + global::windowDelegate.applicationTime() - _timestampPlaybackStarted_application, + global::timeManager.time().j2000Seconds(), interpolatedCamera.x, + interpolatedCamera.y, interpolatedCamera.z)); + //Following is for direct print to save & compare camera positions against recorded file + printf("camera %8.4f %8.4f %13.3f %16.7f %16.7f %16.7f\n", global::windowDelegate.applicationTime(), + global::windowDelegate.applicationTime() - _timestampPlaybackStarted_application, + global::timeManager.time().j2000Seconds(), interpolatedCamera.x, + interpolatedCamera.y, interpolatedCamera.z); + +#endif + + + return true; +} + +double KeyframeNavigator::currentTime() const { + if (_timeframeMode == KeyframeTimeRef::Relative_recordedStart) { + return (global::windowDelegate.applicationTime() - _referenceTimestamp); + } + else if (_timeframeMode == KeyframeTimeRef::Absolute_simTimeJ2000) { + return global::timeManager.time().j2000Seconds(); + } + else { + return global::windowDelegate.applicationTime(); + } +} + +void KeyframeNavigator::setTimeReferenceMode(KeyframeTimeRef refType, double referenceTimestamp) { + _timeframeMode = refType; + _referenceTimestamp = referenceTimestamp; } Timeline& KeyframeNavigator::timeline() { diff --git a/src/interaction/navigationhandler.cpp b/src/interaction/navigationhandler.cpp index bf4d951a43..fd6892731a 100644 --- a/src/interaction/navigationhandler.cpp +++ b/src/interaction/navigationhandler.cpp @@ -135,6 +135,10 @@ KeyframeNavigator& NavigationHandler::keyframeNavigator() const { return *_keyframeNavigator; } +bool NavigationHandler::isKeyFrameInteractionEnabled() const { + return _useKeyFrameInteraction; +} + float NavigationHandler::interpolationTime() const { return _orbitalNavigator->rotateToFocusInterpolationTime(); } @@ -150,20 +154,36 @@ void NavigationHandler::updateCamera(double deltaTime) { if (_cameraUpdatedFromScript) { _cameraUpdatedFromScript = false; } - else { + else if ( ! _playbackModeEnabled ) { if (_camera && focusNode()) { if (_useKeyFrameInteraction) { - _keyframeNavigator->updateCamera(*_camera); + _keyframeNavigator->updateCamera(*_camera, _playbackModeEnabled); } else { _orbitalNavigator->updateStatesFromInput(*_inputState, deltaTime); _orbitalNavigator->updateCameraStateFromStates(*_camera, deltaTime); + _camera->setFocusPositionVec3(focusNode()->worldPosition()); } - _camera->setFocusPositionVec3(focusNode()->worldPosition()); } } } +void NavigationHandler::setEnableKeyFrameInteraction() { + _useKeyFrameInteraction = true; +} + +void NavigationHandler::setDisableKeyFrameInteraction() { + _useKeyFrameInteraction = false; +} + +void NavigationHandler::triggerPlaybackStart() { + _playbackModeEnabled = true; +} + +void NavigationHandler::stopPlayback() { + _playbackModeEnabled = false; +} + SceneGraphNode* NavigationHandler::focusNode() const { return _orbitalNavigator->focusNode(); } diff --git a/src/interaction/sessionrecording.cpp b/src/interaction/sessionrecording.cpp new file mode 100644 index 0000000000..fbd1c41637 --- /dev/null +++ b/src/interaction/sessionrecording.cpp @@ -0,0 +1,1273 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 + +#include +#include +#include +#include + +#include + +#include + +namespace { + const char* _loggerCat = "SessionRecording"; +} + +#include "sessionrecording_lua.inl" + + +namespace openspace::interaction { + +SessionRecording::SessionRecording() + : properties::PropertyOwner({ "SessionRecording" }) +{ +} + +SessionRecording::~SessionRecording() {} // NOLINT + +void SessionRecording::deinitialize() { + stopRecording(); + stopPlayback(); +} + +void SessionRecording::setRecordDataFormat(RecordedDataMode dataMode) { + _recordingDataMode = dataMode; +} + +bool SessionRecording::startRecording(std::string filename) { + if (_state == SessionState::Playback) { + _playbackFile.close(); + } + _state = SessionState::Recording; + _playbackActive_camera = false; + _playbackActive_time = false; + _playbackActive_script = false; + if (isDataModeBinary()) { + _recordFile.open(filename, std::ios::binary); + } + else { + _recordFile.open(filename); + } + + if (!_recordFile.is_open() || !_recordFile.good()) { + LERROR(fmt::format("Unable to open file {} for keyframe recording", filename.c_str())); + return false; + } + _recordFile << _fileHeaderTitle; + _recordFile.write(_fileHeaderVersion, _fileHeaderVersionLength); + if (isDataModeBinary()) { + _recordFile << dataFormatBinaryTag; + } + else { + _recordFile << dataFormatAsciiTag; + } + _recordFile << '\n'; + + LINFO("Session recording started"); + _timestampRecordStarted = global::windowDelegate.applicationTime(); + return true; +} + +void SessionRecording::stopRecording() { + if (_state == SessionState::Recording) { + _state = SessionState::Idle; + LINFO("Session recording stopped"); + } + //Close the recording file + _recordFile.close(); +} + +bool SessionRecording::startPlayback(const std::string& filename, KeyframeTimeRef timeMode, + bool forceSimTimeAtStart) +{ + if (_state == SessionState::Recording) { + LERROR("Unable to start playback while in session recording mode"); + return false; + } + else if (_state == SessionState::Playback) { + if (_playbackFilename == filename) { + LERROR(fmt::format( + "Unable to start playback on file {} since it is already in playback", + filename) + ); + return false; + } + } + + if (!FileSys.fileExists(filename)) { + LERROR("Cannot find the specified playback file."); + cleanUpPlayback(); + return false; + } + + _playbackLineNum = 1; + _playbackFilename = filename; + + //Open in ASCII first + _playbackFile.open(_playbackFilename, std::ifstream::in); + //Read header + std::string readBackHeaderString = readHeaderElement(_fileHeaderTitle.length()); + if (readBackHeaderString != _fileHeaderTitle) { + LERROR("Specified playback file does not contain expected header."); + cleanUpPlayback(); + return false; + } + readHeaderElement(_fileHeaderVersionLength); + std::string readDataMode = readHeaderElement(1); + if (readDataMode[0] == dataFormatAsciiTag) { + _recordingDataMode = RecordedDataMode::Ascii; + } + else if (readDataMode[0] == dataFormatBinaryTag) { + _recordingDataMode = RecordedDataMode::Binary; + } + else { + LERROR("Unknown data type in header (should be Ascii or Binary)"); + cleanUpPlayback(); + } + std::string throwawayNewlineChar = readHeaderElement(1); + + if (isDataModeBinary()) { + //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); + size_t throwAwayHeaderReadSize = _fileHeaderTitle.length() + + _fileHeaderVersionLength + + sizeof(dataFormatBinaryTag) + + sizeof('\n'); + _playbackFile.read(reinterpret_cast(&_keyframeBuffer), + throwAwayHeaderReadSize); + } + + if (!_playbackFile.is_open() || !_playbackFile.good()) { + LERROR(fmt::format("Unable to open file {} for keyframe playback", + filename.c_str())); + stopPlayback(); + cleanUpPlayback(); + return false; + } + //Set time reference mode + double now = global::windowDelegate.applicationTime(); + _timestampPlaybackStarted_application = now; + _timestampPlaybackStarted_simulation = global::timeManager.time().j2000Seconds(); + _timestampApplicationStarted_simulation = _timestampPlaybackStarted_simulation - now; + _playbackTimeReferenceMode = timeMode; + + //Set playback flags to true for all modes + _playbackActive_camera = true; + _playbackActive_script = true; + if (_usingTimeKeyframes) { + _playbackActive_time = true; + } + + global::navigationHandler.keyframeNavigator().setTimeReferenceMode(timeMode, now); + global::scriptScheduler.setTimeReferenceMode(timeMode); + + _setSimulationTimeWithNextCameraKeyframe = forceSimTimeAtStart; + if (!playbackAddEntriesToTimeline()) { + cleanUpPlayback(); + return false; + } + + _hasHitEndOfCameraKeyframes = false; + findFirstCameraKeyframeInTimeline(); + + LINFO(fmt::format("Playback session started @ ({:8.3f},0.0,{:13.3f}) with {}/{}/{} entries, forceTime={}", + now, _timestampPlaybackStarted_simulation, _keyframesCamera.size(), + _keyframesTime.size(), _keyframesScript.size(), (forceSimTimeAtStart ? 1 : 0))); + + global::navigationHandler.triggerPlaybackStart(); + global::scriptScheduler.triggerPlaybackStart(); + global::timeManager.triggerPlaybackStart(); + _state = SessionState::Playback; + + return true; +} + +void 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 + = _timeline[_idxTimeline_cameraFirstInTimeline].timestamp; + foundCameraKeyframe = true; + break; + } + } + + if (!foundCameraKeyframe) { + signalPlaybackFinishedForComponent(RecordedType::Camera); + } +} + +std::string SessionRecording::readHeaderElement(size_t readLen_chars) { + std::vector readTemp(readLen_chars); + _playbackFile.read(&readTemp[0], readLen_chars); + return std::string(readTemp.begin(), readTemp.end()); + //return std::string(&readTemp[0], &readTemp[readLen_chars - 1]); +} + +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 ) { + _state = SessionState::Idle; + _cleanupNeeded = true; + LINFO("Playback session finished"); + } +} + +void SessionRecording::stopPlayback() { + if (_state == SessionState::Playback) { + _state = SessionState::Idle; + _cleanupNeeded = true; + LINFO("Session playback stopped"); + } +} + +void SessionRecording::cleanUpPlayback() { + global::navigationHandler.stopPlayback(); + + Camera* camera = global::navigationHandler.camera(); + ghoul_assert(camera != nullptr, "Camera must not be nullptr"); + Scene* scene = camera->parent()->scene(); + if (!_timeline.empty()) { + unsigned int prevIdx = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; + global::navigationHandler.setFocusNode(scene->sceneGraphNode(_keyframesCamera[prevIdx].focusNode)); + } + global::scriptScheduler.stopPlayback(); + + _playbackFile.close(); + + //Clear all timelines and keyframes + _timeline.clear(); + _keyframesCamera.clear(); + _keyframesTime.clear(); + _keyframesScript.clear(); + _idxTimeline_nonCamera = 0; + _idxTime = 0; + _idxScript = 0; + _idxTimeline_cameraPtrNext = 0; + _idxTimeline_cameraPtrPrev = 0; + _hasHitEndOfCameraKeyframes = false; + + _cleanupNeeded = false; +} + +bool SessionRecording::isDataModeBinary() { + if (_recordingDataMode == RecordedDataMode::Binary) { + return true; + } + else { + return false; + } +} + +void SessionRecording::writeToFileBuffer(const double src) { + const size_t writeSize_bytes = sizeof(double); + unsigned char const *p = reinterpret_cast(&src); + memcpy((_keyframeBuffer + _bufferIndex), p, writeSize_bytes); + _bufferIndex += writeSize_bytes; +} + +void SessionRecording::writeToFileBuffer(std::vector& cvec) { + const size_t writeSize_bytes = cvec.size() * sizeof(char); + memcpy((_keyframeBuffer + _bufferIndex), cvec.data(), writeSize_bytes); + _bufferIndex += writeSize_bytes; +} + +void SessionRecording::writeToFileBuffer(const unsigned char c) { + const size_t writeSize_bytes = sizeof(char); + _keyframeBuffer[_bufferIndex] = c; + _bufferIndex += writeSize_bytes; +} + +void SessionRecording::writeToFileBuffer(const bool b) { + const size_t writeSize_bytes = sizeof(char); + if (b) { + _keyframeBuffer[_bufferIndex] = 1; + } + else { + _keyframeBuffer[_bufferIndex] = 0; + } + _bufferIndex += writeSize_bytes; +} + +void SessionRecording::saveStringToFile(const std::string s) { + size_t strLen = s.size(); + size_t writeSize_bytes = sizeof(size_t); + + _bufferIndex = 0; + unsigned char const *p = reinterpret_cast(&strLen); + memcpy((_keyframeBuffer + _bufferIndex), p, writeSize_bytes); + _bufferIndex += (unsigned int)writeSize_bytes; + saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex); + + _recordFile.write(s.c_str(), s.size()); +} + +void SessionRecording::readFromPlayback(unsigned char& result) { + _playbackFile.read(reinterpret_cast(&result), sizeof(unsigned char)); +} + +void SessionRecording::readFromPlayback(double& result) { + _playbackFile.read(reinterpret_cast(&result), sizeof(double)); +} + +void SessionRecording::readFromPlayback(float& result) { + _playbackFile.read(reinterpret_cast(&result), sizeof(float)); +} + +void SessionRecording::readFromPlayback(size_t& result) { + _playbackFile.read(reinterpret_cast(&result), sizeof(size_t)); +} + +void SessionRecording::readFromPlayback(bool& result) { + unsigned char b; + _playbackFile.read(reinterpret_cast(&b), sizeof(unsigned char)); + if (b == 0) { + result = false; + } + else if (b == 1) { + result = true; + } + else { + LERROR(fmt::format("Invalid bool read value at {}", _playbackLineNum - 1)); + } +} + +void SessionRecording::readFromPlayback(std::string& result) { + result.erase(); + size_t strLen; + //Read string length from file + _playbackFile.read(reinterpret_cast(&strLen), sizeof(strLen)); + //Read back full string + std::vector temp(strLen + 1); + _playbackFile.read(temp.data(), strLen); + temp[strLen] = '\0'; + result = temp.data(); +} + +bool SessionRecording::hasCameraChangedFromPrev(datamessagestructures::CameraKeyframe kfNew) { + const double threshold = 1e-2; + bool hasChanged = false; + + glm::dvec3 positionDiff = kfNew._position - _prevRecordedCameraKeyframe._position; + if (glm::length(positionDiff) > threshold) { + hasChanged = true; + } + + double rotationDiff = dot(kfNew._rotation, _prevRecordedCameraKeyframe._rotation); + if (std::abs(rotationDiff - 1.0) > threshold) { + hasChanged = true; + } + + _prevRecordedCameraKeyframe = kfNew; + return hasChanged; +} + +void SessionRecording::saveCameraKeyframe() { + if (_state != SessionState::Recording) { + return; + } + + SceneGraphNode* focusNode = global::navigationHandler.focusNode(); + if (!focusNode) { + return; + } + + //Create a camera keyframe, then call to populate it with current position + // & orientation of camera + datamessagestructures::CameraKeyframe kf = _externInteract.generateCameraKeyframe(); + + if (/*hasCameraChangedFromPrev(kf)*/true) { + if (isDataModeBinary()) { + _bufferIndex = 0; + _keyframeBuffer[_bufferIndex++] = 'c'; + + //Writing to internal buffer, and then to file, for performance reasons + writeToFileBuffer(kf._timestamp); + writeToFileBuffer(kf._timestamp - _timestampRecordStarted); + writeToFileBuffer(global::timeManager.time().j2000Seconds()); + std::vector kfBuffer; + kf.serialize(kfBuffer); + writeToFileBuffer(kfBuffer); + + saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex); + } else { + std::stringstream keyframeLine = std::stringstream(); + //Add simulation timestamp, timestamp relative, simulation time to recording start + keyframeLine << "camera "; + keyframeLine << kf._timestamp << " "; + keyframeLine << (kf._timestamp - _timestampRecordStarted) << " "; + keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds(); + keyframeLine << " "; + //Add camera position + keyframeLine << std::fixed << std::setprecision(7) << kf._position.x << " " + << std::fixed << std::setprecision(7) << kf._position.y << " " + << std::fixed << std::setprecision(7) << kf._position.z << " "; + //Add camera rotation + keyframeLine << std::fixed << std::setprecision(7) << kf._rotation.x << " " + << std::fixed << std::setprecision(7) << kf._rotation.y << " " + << std::fixed << std::setprecision(7) << kf._rotation.z << " " + << std::fixed << std::setprecision(7) << kf._rotation.w << " "; + keyframeLine << std::fixed << std::setprecision(7) << kf._scale << " "; + if (kf._followNodeRotation) { + keyframeLine << "F "; + } + else { + keyframeLine << "- "; + } + keyframeLine << kf._focusNode; + + saveKeyframeToFile(keyframeLine.str()); + } + } +} + +void SessionRecording::saveTimeKeyframe() { + if (_state != SessionState::Recording) { + return; + } + + //Create a time keyframe, then call to populate it with current time props + datamessagestructures::TimeKeyframe kf = _externInteract.generateTimeKeyframe(); + + if (isDataModeBinary()) { + _bufferIndex = 0; + _keyframeBuffer[_bufferIndex++] = 't'; + writeToFileBuffer(kf._timestamp); + writeToFileBuffer(kf._timestamp - _timestampRecordStarted); + writeToFileBuffer(kf._time); + writeToFileBuffer(kf._dt); + writeToFileBuffer(kf._paused); + writeToFileBuffer(kf._requiresTimeJump); + + saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex); + } else { + std::stringstream keyframeLine = std::stringstream(); + //Add simulation timestamp, timestamp relative, simulation time to recording start + keyframeLine << "time "; + keyframeLine << kf._timestamp << " "; + keyframeLine << (kf._timestamp - _timestampRecordStarted) << " "; + + //keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds(); + keyframeLine << std::fixed << std::setprecision(3) << kf._time; + + keyframeLine << " " << kf._dt; + if (kf._paused) { + keyframeLine << " P"; + } + else { + keyframeLine << " R"; + } + if (kf._requiresTimeJump) { + keyframeLine << " J"; + } + else { + keyframeLine << " -"; + } + saveKeyframeToFile(keyframeLine.str()); + } +} + +void SessionRecording::saveScriptKeyframe(std::string scriptToSave) { + if (_state != SessionState::Recording) { + return; + } + + datamessagestructures::ScriptMessage sm + = _externInteract.generateScriptMessage(scriptToSave); + if (isDataModeBinary()) { + _bufferIndex = 0; + _keyframeBuffer[_bufferIndex++] = 's'; + writeToFileBuffer(sm._timestamp); + writeToFileBuffer(sm._timestamp - _timestampRecordStarted); + writeToFileBuffer(global::timeManager.time().j2000Seconds()); + //Write header to file + saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex); + + saveStringToFile(scriptToSave); + } + else { + unsigned int numLinesInScript = static_cast(std::count(scriptToSave.begin(), + scriptToSave.end(), '\n')); + std::stringstream keyframeLine = std::stringstream(); + //Add simulation timestamp, timestamp relative, simulation time to recording start + keyframeLine << "script "; + keyframeLine << sm._timestamp << " "; + keyframeLine << (sm._timestamp - _timestampRecordStarted) << " "; + keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds(); + keyframeLine << " "; + keyframeLine << (numLinesInScript + 1) << " "; + keyframeLine << scriptToSave; + + saveKeyframeToFile(keyframeLine.str()); + } +} + +void SessionRecording::preSynchronization() { + if (_state == SessionState::Recording) { + saveCameraKeyframe(); + if (_usingTimeKeyframes) { + saveTimeKeyframe(); + } + } + else if (_state == SessionState::Playback) { + moveAheadInTime(); + } + else if (_cleanupNeeded) { + cleanUpPlayback(); + } +} + +bool SessionRecording::isRecording() const { + return (_state == SessionState::Recording); +} + +bool SessionRecording::isPlayingBack() const { + return (_state == SessionState::Playback); +} + +bool SessionRecording::playbackAddEntriesToTimeline() { + bool parsingErrorsFound = false; + + if (isDataModeBinary()) { + unsigned char frameType; + bool fileReadOk = true; + + while (fileReadOk) { + readFromPlayback(frameType); + //Check if have reached EOF + if (!_playbackFile) { + LINFO(fmt::format("Finished parsing {} entries from playback file {}", + _playbackLineNum - 1, _playbackFilename.c_str())); + fileReadOk = false; + break; + } + if (frameType == 'c') { + playbackCamera(); + } + else if (frameType == 't') { + playbackTimeChange(); + } + else if (frameType == 's') { + playbackScript(); + } + else { + LERROR(fmt::format("Unknown frame type {} @ index {} of playback file {}", + frameType, _playbackLineNum - 1, _playbackFilename.c_str())); + parsingErrorsFound = true; + break; + } + + _playbackLineNum++; + } + } else { + while (std::getline(_playbackFile, _playbackLineParsing)) { + _playbackLineNum++; + + std::istringstream iss(_playbackLineParsing); + std::string entryType; + if (!(iss >> entryType)) { + LERROR(fmt::format("Error reading entry type @ line {} of playback file {}", + _playbackLineNum, _playbackFilename.c_str())); + break; + } + + if (entryType == "camera") { + playbackCamera(); + } + else if (entryType == "time") { + playbackTimeChange(); + } + else if (entryType == "script") { + playbackScript(); + } + else { + LERROR(fmt::format("Unknown frame type {} @ line {} of playback file {}", + entryType, _playbackLineNum, _playbackFilename.c_str())); + parsingErrorsFound = true; + break; + } + } + LINFO(fmt::format("Finished parsing {} entries from playback file {}", + _playbackLineNum, _playbackFilename.c_str())); + } + + return !parsingErrorsFound; +} + +double SessionRecording::appropriateTimestamp(double timeOs, + double timeRec, + double timeSim) +{ + if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { + return timeRec; + } + else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { + return timeSim; + } + else { + return 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 (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { + return (global::windowDelegate.applicationTime() - _timestampPlaybackStarted_application); + } + else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { + return global::timeManager.time().j2000Seconds(); + } + else { + return global::windowDelegate.applicationTime(); + } +} + +void SessionRecording::playbackCamera() { + double timeOs, timeRec, timeSim; + std::string rotationFollowing; + interaction::KeyframeNavigator::CameraPose pbFrame; + datamessagestructures::CameraKeyframe kf; + if (isDataModeBinary()) { + readFromPlayback(timeOs); + readFromPlayback(timeRec); + readFromPlayback(timeSim); + try { + kf.read(&_playbackFile); + } + catch (std::bad_alloc&) { + LERROR(fmt::format("Allocation error with camera playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + catch (std::length_error&) { + LERROR(fmt::format("length_error with camera playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + + timeOs = kf._timestamp; + + pbFrame.focusNode = kf._focusNode; + pbFrame.position = kf._position; + pbFrame.rotation = kf._rotation; + pbFrame.scale = kf._scale; + pbFrame.followFocusNodeRotation = kf._followNodeRotation; + + if (!_playbackFile) { + LINFO(fmt::format("Error reading camera playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + } else { + std::istringstream iss(_playbackLineParsing); + std::string entryType; + iss >> entryType; + iss >> timeOs >> timeRec >> timeSim; + iss >> pbFrame.position.x + >> pbFrame.position.y + >> pbFrame.position.z + >> pbFrame.rotation.x + >> pbFrame.rotation.y + >> pbFrame.rotation.z + >> pbFrame.rotation.w + >> pbFrame.scale + >> rotationFollowing + >> pbFrame.focusNode; + if (iss.fail() || !iss.eof()) { + LERROR(fmt::format("Error parsing camera line {} of playback file", _playbackLineNum)); + return; + } + if (rotationFollowing == "F") + pbFrame.followFocusNodeRotation = true; + else + pbFrame.followFocusNodeRotation = false; + } + if (_setSimulationTimeWithNextCameraKeyframe) { + global::timeManager.setTimeNextFrame(timeSim); + _setSimulationTimeWithNextCameraKeyframe = false; + } + double timeRef = appropriateTimestamp(timeOs, timeRec, timeSim); + + //global::navigationHandler.keyframeNavigator().addKeyframe(timeRef, pbFrame); + addKeyframe(timeRef, pbFrame); +} + +void SessionRecording::playbackTimeChange() { + double timeOs, timeRec, timeSim; + datamessagestructures::TimeKeyframe pbFrame; + if (isDataModeBinary()) { + readFromPlayback(timeOs); + readFromPlayback(timeRec); + readFromPlayback(timeSim); + readFromPlayback(pbFrame._dt); + readFromPlayback(pbFrame._paused); + readFromPlayback(pbFrame._requiresTimeJump); + if (!_playbackFile) { + LERROR(fmt::format("Error reading time playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + } else { + std::istringstream iss(_playbackLineParsing); + std::string entryType; + //double timeRef; + std::string paused, jump; + iss >> entryType; + iss >> timeOs >> timeRec >> timeSim; + iss >> pbFrame._dt + >> paused + >> jump; + if (iss.fail() || !iss.eof()) { + LERROR(fmt::format("Error parsing time line {} of playback file", _playbackLineNum)); + return; + } + if (paused == "P") + pbFrame._paused = true; + else + pbFrame._paused = false; + if (jump == "J") + pbFrame._requiresTimeJump = true; + else + pbFrame._requiresTimeJump = false; + } + pbFrame._timestamp = equivalentApplicationTime(timeOs, timeRec, timeSim); + + pbFrame._time = pbFrame._timestamp + _timestampApplicationStarted_simulation; + //global::timeManager.addKeyframe(timeRef, pbFrame._timestamp); + //_externInteract.timeInteraction(pbFrame); + addKeyframe(pbFrame._timestamp, pbFrame); +} + +void SessionRecording::playbackScript() { + double timeOs, timeRec, timeSim; + unsigned int numScriptLines; + datamessagestructures::ScriptMessage pbFrame; + + if (isDataModeBinary()) { + readFromPlayback(timeOs); + readFromPlayback(timeRec); + readFromPlayback(timeSim); + try { + readFromPlayback(pbFrame._script); + } + catch (std::bad_alloc&) { + LERROR(fmt::format("Allocation error with script playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + catch (std::length_error&) { + LERROR(fmt::format("length_error with script playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + + if (!_playbackFile) { + LERROR(fmt::format("Error reading script playback from keyframe entry {}", + _playbackLineNum - 1)); + return; + } + } else { + std::istringstream iss(_playbackLineParsing); + std::string entryType; + std::string tmpReadbackScript; + + iss >> entryType; + iss >> timeOs >> timeRec >> timeSim; + iss >> numScriptLines; + std::getline(iss, tmpReadbackScript); //iss >> tmpReadbackScript; + pbFrame._script.append(tmpReadbackScript); + if (iss.fail()) { + LERROR(fmt::format("Error parsing script line {} of playback file", _playbackLineNum)); + return; + } else if (!iss.eof()) { + LERROR(fmt::format("Did not find an EOL at line {} of playback file", _playbackLineNum)); + return; + } + if (numScriptLines > 1) { + //Now loop to read any subsequent lines if is a multi-line script + for (unsigned int i = 1; i < numScriptLines; ++i) { + pbFrame._script.append("\n"); + std::getline(_playbackFile, tmpReadbackScript); + pbFrame._script.append(tmpReadbackScript); + } + } + } + double timeRef = appropriateTimestamp(timeOs, timeRec, timeSim); + //timeRef = getEquivalentSimulationTime(timeOs, timeRec, timeSim); + + //Call script scheduler with this new script entry + //std::string timeDescription = SpiceManager::ref().dateFromEphemerisTime(timeRef, "YYYY MON DD HR:MN:SC.###"); + //ghoul::Dictionary scriptDict(ghoul::Dictionary{ {KeyTime, timeDescription }, + // {KeyForwardScript, pbFrame._script} } + // ); + //global::scriptScheduler.loadScripts({ { "1", scriptDict } }); + addKeyframe(timeRef, pbFrame._script); +} + +void SessionRecording::addKeyframe(double timestamp, interaction::KeyframeNavigator::CameraPose keyframe) +{ + unsigned int indexIntoCameraKeyframesFromMainTimeline = static_cast(_keyframesCamera.size()); + _keyframesCamera.push_back(std::move(keyframe)); + _timeline.push_back({ + RecordedType::Camera, + indexIntoCameraKeyframesFromMainTimeline, + timestamp + }); +} + +void SessionRecording::addKeyframe(double timestamp, datamessagestructures::TimeKeyframe keyframe) +{ + unsigned int indexIntoTimeKeyframesFromMainTimeline = static_cast(_keyframesTime.size()); + _keyframesTime.push_back(std::move(keyframe)); + _timeline.push_back({ + RecordedType::Time, + indexIntoTimeKeyframesFromMainTimeline, + timestamp + }); +} + +void SessionRecording::addKeyframe(double timestamp, std::string scriptToQueue) +{ + unsigned int indexIntoScriptKeyframesFromMainTimeline = static_cast(_keyframesScript.size()); + _keyframesScript.push_back(std::move(scriptToQueue)); + _timeline.push_back({ + RecordedType::Script, + indexIntoScriptKeyframesFromMainTimeline, + timestamp + }); +} + +void SessionRecording::moveAheadInTime() { + double currTime = currentTime(); + lookForNonCameraKeyframesThatHaveComeDue(currTime); + updateCameraWithOrWithoutNewKeyframes(currTime); +} + +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) { + bool didFindFutureCameraKeyframes = findNextFutureCameraIndex(currTime); + + bool isPrevAtFirstKeyframe + = (_idxTimeline_cameraPtrPrev == _idxTimeline_cameraFirstInTimeline); + bool isFirstTimelineCameraKeyframeInFuture + = (currTime < _cameraFirstInTimeline_timestamp); + + if (! (isPrevAtFirstKeyframe && isFirstTimelineCameraKeyframeInFuture)) { + processCameraKeyframe(currTime); + } + if (!didFindFutureCameraKeyframes) { + signalPlaybackFinishedForComponent(RecordedType::Camera); + } + } +} + +bool SessionRecording::isTimeToHandleNextNonCameraKeyframe(double currTime) { + bool isNonCameraPlaybackActive = (_playbackActive_time || _playbackActive_script); + if ((currTime > getNextTimestamp()) && isNonCameraPlaybackActive) { + return true; + } + else { + return false; + } +} + +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)) { + unsigned int indexIntoCameraKeyframes + = _timeline[seekAheadIndex].idxIntoKeyframeTypeArray; + double seekAheadKeyframeTimestamp = _timeline[seekAheadIndex].timestamp; + + 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; + } + } + + double interpolationUpperBoundTimestamp + = _timeline[_idxTimeline_cameraPtrNext].timestamp; + if ((currTime > interpolationUpperBoundTimestamp) && _hasHitEndOfCameraKeyframes) { + _idxTimeline_cameraPtrPrev = _idxTimeline_cameraPtrNext; + return false; + break; + } + + if (seekAheadIndex == (_timeline.size() - 1)) { + break; + } + } + return true; +} + +bool SessionRecording::doesTimelineEntryContainCamera(unsigned int index) const { + return (_timeline[index].keyframeType == RecordedType::Camera); +} + +bool SessionRecording::processNextNonCameraKeyframeAheadInTime() { + bool returnValue = false; + + //LINFO(fmt::format("Keyframe at {} frame={} timelineIndex={}", now, global::renderEngine._frameNumber, _idxTimeline)); + + switch (getNextKeyframeType()) { + case RecordedType::Camera: + //Just return true since this function no longer handles camera keyframes + returnValue = true; + break; + + case RecordedType::Time: + _idxTime = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; + if (_keyframesTime.size() == 0) { + return false; + } + LINFO("Time keyframe type"); + //TBD: the TimeManager restricts setting time directly + break; + + case RecordedType::Script: + _idxScript = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; + returnValue = processScriptKeyframe(); + break; + + default: + LERROR(fmt::format("Bad keyframe type encountered during playback at index {}.", + _idxTimeline_nonCamera)); + break; + } + return returnValue; +} + +//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, nextIdx; + 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]; + } + + double prevTime = _timeline[_idxTimeline_cameraPtrPrev].timestamp; //getPrevTimestamp(); + double nextTime = _timeline[_idxTimeline_cameraPtrNext].timestamp; //getNextTimestamp(); + + double t; + if ((nextTime - prevTime) < 1e-7) { + t = 0; + } + else { + 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(); + global::navigationHandler.setFocusNode(scene->sceneGraphNode(_keyframesCamera[prevIdx].focusNode)); + + return interaction::KeyframeNavigator::updateCamera(global::navigationHandler.camera(), prevPose, nextPose, t, false); +} + +bool SessionRecording::processScriptKeyframe() { + std::string nextScript; + + if (!_playbackActive_script) { + return false; + } else if (_keyframesScript.empty()) { + return false; + } else { + nextScript = nextKeyframeObj(_idxScript, _keyframesScript, + ([&]() { signalPlaybackFinishedForComponent(RecordedType::Script); })); + global::scriptEngine.queueScript(nextScript, + scripting::ScriptEngine::RemoteScripting::Yes); + } + //LINFO("Script"); + + return true; +} + +double SessionRecording::getNextTimestamp() { + if (_timeline.empty()) { + return 0.0; + } else if (_idxTimeline_nonCamera < _timeline.size()) { + return _timeline[_idxTimeline_nonCamera].timestamp; + } else { + return _timeline.back().timestamp; + } +} + +double SessionRecording::getPrevTimestamp() { + if (_timeline.empty()) { + return 0.0; + } + else if (_idxTimeline_nonCamera == 0) { + return _timeline.front().timestamp; + } + else if (_idxTimeline_nonCamera < _timeline.size()) { + return _timeline[_idxTimeline_nonCamera - 1].timestamp; + } + else { + return _timeline.back().timestamp; + } +} + +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* bufferSource, + size_t size) +{ + _recordFile.write((char*)bufferSource, size); +} + +void SessionRecording::saveKeyframeToFile(std::string entry) { + _recordFile << entry << std::endl; +} + +scripting::LuaLibrary SessionRecording::luaLibrary() { + return { + "sessionRecording", + { + { + "startRecording", + &luascriptfunctions::startRecording, + {}, + "string", + "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." + }, + { + "startRecordingAscii", + &luascriptfunctions::startRecordingAscii, + {}, + "string", + "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." + }, + { + "stopRecording", + &luascriptfunctions::stopRecording, + {}, + "void", + "Stops a recording session" + }, + { + "startPlayback", + &luascriptfunctions::startPlaybackDefault, + {}, + "string", + "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." + }, + { + "startPlaybackApplicationTime", + &luascriptfunctions::startPlaybackApplicationTime, + {}, + "string", + "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." + }, + { + "startPlaybackRecordedTime", + &luascriptfunctions::startPlaybackRecordedTime, + {}, + "string", + "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." + }, + { + "startPlaybackSimulationTime", + &luascriptfunctions::startPlaybackSimulationTime, + {}, + "string", + "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." + }, + { + "stopPlayback", + &luascriptfunctions::stopPlayback, + {}, + "void", + "Stops a playback session before playback of all keyframes is complete" + } + } + }; +} + +} // namespace openspace::interaction diff --git a/src/interaction/sessionrecording_lua.inl b/src/interaction/sessionrecording_lua.inl new file mode 100644 index 0000000000..0ceebc8426 --- /dev/null +++ b/src/interaction/sessionrecording_lua.inl @@ -0,0 +1,125 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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::luascriptfunctions { + + +int startRecording(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startRecording"); + + using ghoul::lua::luaTypeToString; + + const std::string recordFilePath = ghoul::lua::value(L, 1, ghoul::lua::PopValue::Yes); + + if (recordFilePath.empty()) { + return luaL_error(L, "filepath string is empty"); + } + global::sessionRecording.setRecordDataFormat(openspace::interaction::SessionRecording::RecordedDataMode::Binary); + global::sessionRecording.startRecording(recordFilePath); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int startRecordingAscii(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startRecordingAscii"); + + using ghoul::lua::luaTypeToString; + + const std::string recordFilePath = ghoul::lua::value(L, 1, ghoul::lua::PopValue::Yes); + + if (recordFilePath.empty()) { + return luaL_error(L, "filepath string is empty"); + } + global::sessionRecording.setRecordDataFormat(openspace::interaction::SessionRecording::RecordedDataMode::Ascii); + global::sessionRecording.startRecording(recordFilePath); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int stopRecording(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::stopRecording"); + + global::sessionRecording.stopRecording(); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int startPlayback(lua_State* L, openspace::interaction::KeyframeTimeRef timeMode, + bool forceSimTimeAtStart) +{ + using ghoul::lua::luaTypeToString; + + const std::string playbackFilePath = ghoul::lua::value(L, 1, ghoul::lua::PopValue::Yes); + + if (playbackFilePath.empty()) { + return luaL_error(L, "filepath string is empty"); + } + + global::sessionRecording.startPlayback(playbackFilePath, timeMode, forceSimTimeAtStart); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int startPlaybackDefault(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startPlaybackDefault"); + using openspace::interaction::KeyframeNavigator; + return startPlayback(L, + openspace::interaction::KeyframeTimeRef::Relative_recordedStart, true); +} + +int startPlaybackApplicationTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startPlaybackApplicationTime"); + + return startPlayback(L, + openspace::interaction::KeyframeTimeRef::Relative_applicationStart, false); +} + +int startPlaybackRecordedTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startPlaybackRecordedTime"); + using openspace::interaction::KeyframeNavigator; + return startPlayback(L, + openspace::interaction::KeyframeTimeRef::Relative_recordedStart, false); +} + +int startPlaybackSimulationTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::startPlaybackSimulationTime"); + using openspace::interaction::KeyframeNavigator; + return startPlayback(L, + openspace::interaction::KeyframeTimeRef::Absolute_simTimeJ2000, false); +} + +int stopPlayback(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::stopPlayback"); + + global::sessionRecording.stopPlayback(); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +} // namespace openspace::luascriptfunctions diff --git a/src/network/parallelpeer.cpp b/src/network/parallelpeer.cpp index e8876625b3..6955cf8ad1 100644 --- a/src/network/parallelpeer.cpp +++ b/src/network/parallelpeer.cpp @@ -242,6 +242,7 @@ double ParallelPeer::convertTimestamp(double messageTimestamp) { return messageTimestamp + _initialTimeDiff + _bufferTime; } + double ParallelPeer::latencyStandardDeviation() const { double accumulatedLatencyDiffSquared = 0; double accumulatedLatencyDiff = 0; diff --git a/src/rendering/renderengine.cpp b/src/rendering/renderengine.cpp index 497cacf0be..58adbf322e 100644 --- a/src/rendering/renderengine.cpp +++ b/src/rendering/renderengine.cpp @@ -662,6 +662,21 @@ void RenderEngine::renderDashboard() { ); global::dashboard.render(penPosition); + +#ifdef REALTIME_CAMERA_POS_DISPLAY + penPosition += glm::vec2(0.f, -50.f); + + glm::dvec3 p = _camera->positionVec3(); + glm::dquat rot = _camera->rotationQuaternion(); + std::string fc = global::navigationHandler.focusNode()->identifier(); + RenderFont( + *_fontInfo, + penPosition, + fmt::format("Pos: {} {} {}\nOrientation: {} {} {} {}\nFocus: {}", + p.x, p.y, p.z, rot[0], rot[1], rot[2], rot[3], fc + ) + ); +#endif } void RenderEngine::postDraw() { diff --git a/src/scripting/scriptengine.cpp b/src/scripting/scriptengine.cpp index 39a206687e..8a1fb2f6c1 100644 --- a/src/scripting/scriptengine.cpp +++ b/src/scripting/scriptengine.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -636,6 +637,9 @@ void ScriptEngine::preSync(bool isMaster) { if (global::parallelPeer.isHost() && remoteScripting) { global::parallelPeer.sendScript(_currentSyncedScript); } + if (global::sessionRecording.isRecording()) { + global::sessionRecording.saveScriptKeyframe(_currentSyncedScript); + } } _mutex.unlock(); } diff --git a/src/scripting/scriptscheduler.cpp b/src/scripting/scriptscheduler.cpp index f398c0f593..728821e2d0 100644 --- a/src/scripting/scriptscheduler.cpp +++ b/src/scripting/scriptscheduler.cpp @@ -30,13 +30,6 @@ #include #include -namespace { - constexpr const char* KeyTime = "Time"; - constexpr const char* KeyForwardScript = "ForwardScript"; - constexpr const char* KeyBackwardScript = "BackwardScript"; - constexpr const char* KeyUniversalScript = "Script"; -} // namespace - #include "scriptscheduler_lua.inl" namespace openspace::scripting { @@ -93,6 +86,8 @@ documentation::Documentation ScriptScheduler::Documentation() { }; } +using namespace openspace::interaction; + ScriptScheduler::ScheduledScript::ScheduledScript(const ghoul::Dictionary& dictionary) { const std::string& timeStr = dictionary.value(KeyTime); time = Time::convertTime(timeStr); @@ -235,6 +230,18 @@ ScriptScheduler::progressTo(double newTime) } } +void ScriptScheduler::setTimeReferenceMode(KeyframeTimeRef refType) { + _timeframeMode = refType; +} + +void ScriptScheduler::triggerPlaybackStart() { + _playbackModeEnabled = true; +} + +void ScriptScheduler::stopPlayback() { + _playbackModeEnabled = false; +} + double ScriptScheduler::currentTime() const { return _currentTime; } @@ -252,6 +259,18 @@ std::vector ScriptScheduler::allScripts() cons return result; } +void ScriptScheduler::setModeApplicationTime() { + _timeframeMode = KeyframeTimeRef::Relative_applicationStart; +} + +void ScriptScheduler::setModeRecordedTime() { + _timeframeMode = KeyframeTimeRef::Relative_recordedStart; +} + +void ScriptScheduler::setModeSimulationTime() { + _timeframeMode = KeyframeTimeRef::Absolute_simTimeJ2000; +} + LuaLibrary ScriptScheduler::luaLibrary() { return { "scriptScheduler", @@ -276,6 +295,30 @@ LuaLibrary ScriptScheduler::luaLibrary() { "last argument is the universal script, executed in either direction." }, + { + "setModeApplicationTime", + &luascriptfunctions::setModeApplicationTime, + {}, + "", + "Sets the time reference for scheduled scripts to application time " + "(seconds since OpenSpace application started)." + }, + { + "setModeRecordedTime", + &luascriptfunctions::setModeRecordedTime, + {}, + "", + "Sets the time reference for scheduled scripts to the time since the " + "recording was started (the same relative time applies to playback)." + }, + { + "setModeSimulationTime", + &luascriptfunctions::setModeSimulationTime, + {}, + "", + "Sets the time reference for scheduled scripts to the simulated " + "date & time (J2000 epoch seconds)." + }, { "clear", &luascriptfunctions::clear, diff --git a/src/scripting/scriptscheduler_lua.inl b/src/scripting/scriptscheduler_lua.inl index 14afad852e..87e1839839 100644 --- a/src/scripting/scriptscheduler_lua.inl +++ b/src/scripting/scriptscheduler_lua.inl @@ -103,6 +103,33 @@ int loadScheduledScript(lua_State* L) { return 0; } +int setModeApplicationTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::setModeApplicationTime"); + + global::scriptScheduler.setModeApplicationTime(); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int setModeRecordedTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::setModeRecordedTime"); + + global::scriptScheduler.setModeRecordedTime(); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int setModeSimulationTime(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::setModeSimulationTime"); + + global::scriptScheduler.setModeSimulationTime(); + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + int clear(lua_State* L) { ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::clear"); diff --git a/src/util/timemanager.cpp b/src/util/timemanager.cpp index 58ec369132..e03b5ae1c3 100644 --- a/src/util/timemanager.cpp +++ b/src/util/timemanager.cpp @@ -258,6 +258,7 @@ void TimeManager::progressTime(double dt) { // and time is not paused, just advance time. _deltaTime = _targetDeltaTime; _currentTime.data().advanceTime(dt * _deltaTime); + _playbackModeEnabled = false; } if (hasPastKeyframes) { @@ -434,6 +435,10 @@ void TimeManager::removeDeltaTimeChangeCallback(CallbackHandle handle) { _deltaTimeChangeCallbacks.erase(it); } +void TimeManager::triggerPlaybackStart() { + _playbackModeEnabled = true; +} + void TimeManager::removeTimeJumpCallback(CallbackHandle handle) { const auto it = std::find_if( _timeJumpCallbacks.begin(),