diff --git a/apps/OpenSpace/ext/launcher/include/profile/cameradialog.h b/apps/OpenSpace/ext/launcher/include/profile/cameradialog.h index d600f67f0f..aa6c018e4a 100644 --- a/apps/OpenSpace/ext/launcher/include/profile/cameradialog.h +++ b/apps/OpenSpace/ext/launcher/include/profile/cameradialog.h @@ -52,6 +52,7 @@ private slots: private: void createWidgets(); + QWidget* createNodeWidget(); QWidget* createNavStateWidget(); QWidget* createGeoWidget(); @@ -60,6 +61,12 @@ private: std::optional* _camera = nullptr; QTabWidget* _tabWidget = nullptr; + + struct { + QLineEdit* anchor = nullptr; + QLineEdit* height = nullptr; + } _nodeState; + struct { QLineEdit* anchor = nullptr; QLineEdit* aim = nullptr; diff --git a/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss b/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss index 855fdf6340..cae89e68cf 100644 --- a/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss +++ b/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss @@ -149,6 +149,13 @@ CameraDialog QLabel#error-message { min-width: 10em; } +CameraDialog QLabel#camera-description { + color:rgb(96, 96, 96); + margin: 1em; + margin-top: 0.2em; + margin-bottom: 0.4em; +} + /* * ScriptlogDialog */ diff --git a/apps/OpenSpace/ext/launcher/src/profile/cameradialog.cpp b/apps/OpenSpace/ext/launcher/src/profile/cameradialog.cpp index fdcf33ea8d..c44a83d063 100644 --- a/apps/OpenSpace/ext/launcher/src/profile/cameradialog.cpp +++ b/apps/OpenSpace/ext/launcher/src/profile/cameradialog.cpp @@ -29,14 +29,16 @@ #include #include #include +#include #include #include -#include -#include +#include +#include namespace { - constexpr int CameraTypeNav = 0; - constexpr int CameraTypeGeo = 1; + constexpr int CameraTypeNode = 0; + constexpr int CameraTypeNav = 1; + constexpr int CameraTypeGeo = 2; template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; @@ -53,6 +55,19 @@ namespace { } return true; } + + bool isNumericalLargerThan(QLineEdit* le, float limit) { + QString s = le->text(); + bool validConversion = false; + float value = s.toFloat(&validConversion); + if (!validConversion) { + return false; + } + if (value > limit) { + return true; + } + return false; + } } // namespace CameraDialog::CameraDialog(QWidget* parent, @@ -66,6 +81,14 @@ CameraDialog::CameraDialog(QWidget* parent, if (_camera->has_value()) { const openspace::Profile::CameraType& type = **_camera; std::visit(overloaded { + [this](const openspace::Profile::CameraGoToNode& node) { + _tabWidget->setCurrentIndex(CameraTypeNode); + _nodeState.anchor->setText(QString::fromStdString(node.anchor)); + if (node.height.has_value()) { + _nodeState.height->setText(QString::number(*node.height, 'g', 17)); + } + tabSelect(CameraTypeNode); + }, [this](const openspace::Profile::CameraNavState& nav) { _tabWidget->setCurrentIndex(CameraTypeNav); _navState.anchor->setText(QString::fromStdString(nav.anchor)); @@ -114,7 +137,11 @@ CameraDialog::CameraDialog(QWidget* parent, }, type); } else { - _tabWidget->setCurrentIndex(CameraTypeNav); + _tabWidget->setCurrentIndex(CameraTypeNode); + + _nodeState.anchor->clear(); + _nodeState.height->clear(); + _navState.anchor->clear(); _navState.aim->clear(); _navState.refFrame->clear(); @@ -138,6 +165,7 @@ void CameraDialog::createWidgets() { QBoxLayout* layout = new QVBoxLayout(this); _tabWidget = new QTabWidget; connect(_tabWidget, &QTabWidget::tabBarClicked, this, &CameraDialog::tabSelect); + _tabWidget->addTab(createNodeWidget(), "Scene Graph Node"); _tabWidget->addTab(createNavStateWidget(), "Navigation State"); _tabWidget->addTab(createGeoWidget(), "Geo State"); layout->addWidget(_tabWidget); @@ -162,135 +190,235 @@ void CameraDialog::createWidgets() { } } -QWidget* CameraDialog::createNavStateWidget() { - QWidget* box = new QWidget; - QGridLayout* layout = new QGridLayout(box); +QWidget* CameraDialog::createNodeWidget() { + QWidget* tab = new QWidget; + QVBoxLayout* mainLayout = new QVBoxLayout(tab); - layout->addWidget(new QLabel("Anchor:"), 0, 0); - _navState.anchor = new QLineEdit; - _navState.anchor->setToolTip("Anchor camera to this node"); - layout->addWidget(_navState.anchor, 0, 1); - - layout->addWidget(new QLabel("Aim:"), 1, 0); - _navState.aim = new QLineEdit; - _navState.aim->setToolTip( - "If specified, camera will be aimed at this node while keeping the anchor node " - "in the same view location" + QLabel* description = new QLabel; + description->setWordWrap(true); + description->setAlignment(Qt::AlignCenter); + description->setObjectName("camera-description"); + description->setText( + "Set the camera position based on a given scene graph node. \n \n " + "Automatically computes a position that frames the object, and if possible, " + "shows it from Sun-lit side. The exact position might change between start-ups." ); - _navState.aim->setPlaceholderText("optional"); - layout->addWidget(_navState.aim, 1, 1); - layout->addWidget(new QLabel("Reference Frame:"), 2, 0); - _navState.refFrame = new QLineEdit; - _navState.refFrame->setToolTip("Camera location in reference to this frame"); - _navState.refFrame->setPlaceholderText("optional"); - layout->addWidget(_navState.refFrame, 2, 1); + mainLayout->addWidget(description); - layout->addWidget(new QLabel("Position:"), 3, 0); { - QWidget* posBox = new QWidget; - QBoxLayout* posLayout = new QHBoxLayout(posBox); - posLayout->setContentsMargins(0, 0, 0, 0); - posLayout->addWidget(new QLabel("X [m]")); - _navState.positionX = new QLineEdit; - _navState.positionX->setValidator(new QDoubleValidator); - _navState.positionX->setToolTip("Camera position vector (x) [m]"); - posLayout->addWidget(_navState.positionX); + QWidget* box = new QWidget; + QGridLayout* layout = new QGridLayout(box); - posLayout->addWidget(new QLabel("Y [m]")); - _navState.positionY = new QLineEdit; - _navState.positionY->setValidator(new QDoubleValidator); - _navState.positionY->setToolTip("Camera position vector (y) [m]"); - posLayout->addWidget(_navState.positionY); + layout->addWidget(new QLabel("Anchor Node:"), 0, 0); + _nodeState.anchor = new QLineEdit; + _nodeState.anchor->setToolTip("Anchor camera to this scene graph node"); + layout->addWidget(_nodeState.anchor, 0, 1); - posLayout->addWidget(new QLabel("Z [m]")); - _navState.positionZ = new QLineEdit; - _navState.positionZ->setValidator(new QDoubleValidator); - _navState.positionZ->setToolTip("Camera position vector (z) [m]"); - posLayout->addWidget(_navState.positionZ); - layout->addWidget(posBox, 3, 1); + layout->addWidget(new QLabel("Height [m]:"), 1, 0); + _nodeState.height = new QLineEdit; + _nodeState.height->setValidator(new QDoubleValidator); + _nodeState.height->setToolTip( + "If specified, the camera will placed at the given height away from object. " + "The height is computed from the bounding sphere of the anchor node" + ); + _nodeState.height->setPlaceholderText("optional"); + layout->addWidget(_nodeState.height, 1, 1); + + mainLayout->addWidget(box); } - layout->addWidget(new QLabel("Up:"), 4, 0); + // Add spacer at the end to prevent previous components from growing as much + mainLayout->addSpacerItem( + new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding) + ); + + return tab; +} + +QWidget* CameraDialog::createNavStateWidget() { + QWidget* tab = new QWidget; + QVBoxLayout* mainLayout = new QVBoxLayout(tab); + + QLabel* description = new QLabel; + description->setWordWrap(true); + description->setAlignment(Qt::AlignCenter); + description->setObjectName("camera-description"); + description->setText( + "Set the camera position from a navigation state. \n" + "Allows for setting an exact view or camera position at start-up." + ); + + mainLayout->addWidget(description); + { - QWidget* upBox = new QWidget; - QBoxLayout* upLayout = new QHBoxLayout(upBox); - upLayout->setContentsMargins(0, 0, 0, 0); - upLayout->addWidget(new QLabel("X")); - _navState.upX = new QLineEdit; - _navState.upX->setValidator(new QDoubleValidator); - _navState.upX->setToolTip("Camera up vector (x)"); - _navState.upX->setPlaceholderText("semioptional"); - upLayout->addWidget(_navState.upX); + QWidget* box = new QWidget; + QGridLayout* layout = new QGridLayout(box); - upLayout->addWidget(new QLabel("Y")); - _navState.upY = new QLineEdit; - _navState.upY->setValidator(new QDoubleValidator); - _navState.upY->setToolTip("Camera up vector (y)"); - _navState.upY->setPlaceholderText("semioptional"); - upLayout->addWidget(_navState.upY); + layout->addWidget(new QLabel("Anchor:"), 0, 0); + _navState.anchor = new QLineEdit; + _navState.anchor->setToolTip("Anchor camera to this node"); + layout->addWidget(_navState.anchor, 0, 1); - upLayout->addWidget(new QLabel("Z")); - _navState.upZ = new QLineEdit; - _navState.upZ->setValidator(new QDoubleValidator); - _navState.upZ->setToolTip("Camera up vector (z)"); - _navState.upZ->setPlaceholderText("semioptional"); - upLayout->addWidget(_navState.upZ); - layout->addWidget(upBox, 4, 1); + layout->addWidget(new QLabel("Aim:"), 1, 0); + _navState.aim = new QLineEdit; + _navState.aim->setToolTip( + "If specified, camera will be aimed at this node while keeping the anchor node " + "in the same view location" + ); + _navState.aim->setPlaceholderText("optional"); + layout->addWidget(_navState.aim, 1, 1); + + layout->addWidget(new QLabel("Reference Frame:"), 2, 0); + _navState.refFrame = new QLineEdit; + _navState.refFrame->setToolTip("Camera location in reference to this frame"); + _navState.refFrame->setPlaceholderText("optional"); + layout->addWidget(_navState.refFrame, 2, 1); + + layout->addWidget(new QLabel("Position:"), 3, 0); + { + QWidget* posBox = new QWidget; + QBoxLayout* posLayout = new QHBoxLayout(posBox); + posLayout->setContentsMargins(0, 0, 0, 0); + posLayout->addWidget(new QLabel("X [m]")); + _navState.positionX = new QLineEdit; + _navState.positionX->setValidator(new QDoubleValidator); + _navState.positionX->setToolTip("Camera position vector (x) [m]"); + posLayout->addWidget(_navState.positionX); + + posLayout->addWidget(new QLabel("Y [m]")); + _navState.positionY = new QLineEdit; + _navState.positionY->setValidator(new QDoubleValidator); + _navState.positionY->setToolTip("Camera position vector (y) [m]"); + posLayout->addWidget(_navState.positionY); + + posLayout->addWidget(new QLabel("Z [m]")); + _navState.positionZ = new QLineEdit; + _navState.positionZ->setValidator(new QDoubleValidator); + _navState.positionZ->setToolTip("Camera position vector (z) [m]"); + posLayout->addWidget(_navState.positionZ); + layout->addWidget(posBox, 3, 1); + } + + layout->addWidget(new QLabel("Up:"), 4, 0); + { + QWidget* upBox = new QWidget; + QBoxLayout* upLayout = new QHBoxLayout(upBox); + upLayout->setContentsMargins(0, 0, 0, 0); + upLayout->addWidget(new QLabel("X")); + _navState.upX = new QLineEdit; + _navState.upX->setValidator(new QDoubleValidator); + _navState.upX->setToolTip("Camera up vector (x)"); + _navState.upX->setPlaceholderText("semioptional"); + upLayout->addWidget(_navState.upX); + + upLayout->addWidget(new QLabel("Y")); + _navState.upY = new QLineEdit; + _navState.upY->setValidator(new QDoubleValidator); + _navState.upY->setToolTip("Camera up vector (y)"); + _navState.upY->setPlaceholderText("semioptional"); + upLayout->addWidget(_navState.upY); + + upLayout->addWidget(new QLabel("Z")); + _navState.upZ = new QLineEdit; + _navState.upZ->setValidator(new QDoubleValidator); + _navState.upZ->setToolTip("Camera up vector (z)"); + _navState.upZ->setPlaceholderText("semioptional"); + upLayout->addWidget(_navState.upZ); + layout->addWidget(upBox, 4, 1); + } + + layout->addWidget(new QLabel("Yaw angle:"), 5, 0); + _navState.yaw = new QLineEdit; + _navState.yaw->setValidator(new QDoubleValidator); + _navState.yaw->setToolTip("Yaw angle +/- 360 degrees"); + _navState.yaw->setPlaceholderText("optional"); + layout->addWidget(_navState.yaw, 5, 1); + + layout->addWidget(new QLabel("Pitch angle:"), 6, 0); + _navState.pitch = new QLineEdit; + _navState.pitch->setValidator(new QDoubleValidator); + _navState.pitch->setToolTip("Pitch angle +/- 360 degrees"); + _navState.pitch->setPlaceholderText("optional"); + layout->addWidget(_navState.pitch, 6, 1); + + mainLayout->addWidget(box); } - layout->addWidget(new QLabel("Yaw angle:"), 5, 0); - _navState.yaw = new QLineEdit; - _navState.yaw->setValidator(new QDoubleValidator); - _navState.yaw->setToolTip("Yaw angle +/- 360 degrees"); - _navState.yaw->setPlaceholderText("optional"); - layout->addWidget(_navState.yaw, 5, 1); - - layout->addWidget(new QLabel("Pitch angle:"), 6, 0); - _navState.pitch = new QLineEdit; - _navState.pitch->setValidator(new QDoubleValidator); - _navState.pitch->setToolTip("Pitch angle +/- 360 degrees"); - _navState.pitch->setPlaceholderText("optional"); - layout->addWidget(_navState.pitch, 6, 1); - - return box; + return tab; } QWidget* CameraDialog::createGeoWidget() { - QWidget* box = new QWidget; - QGridLayout* layout = new QGridLayout(box); + QWidget* tab = new QWidget; + QVBoxLayout* mainLayout = new QVBoxLayout(tab); - layout->addWidget(new QLabel("Anchor:"), 0, 0); - _geoState.anchor = new QLineEdit; - _geoState.anchor->setToolTip("Anchor camera to this globe (planet/moon)"); - layout->addWidget(_geoState.anchor, 0, 1); + QLabel* description = new QLabel; + description->setWordWrap(true); + description->setAlignment(Qt::AlignCenter); + description->setObjectName("camera-description"); + description->setText( + "Sets the camera position from a geodetic position of a globe (e.g a planet/moon)." + ); - layout->addWidget(new QLabel("Latitude"), 1, 0); - _geoState.latitude = new QLineEdit; - _geoState.latitude->setValidator(new QDoubleValidator); - _geoState.latitude->setToolTip("Latitude of camera focus point (+/- 90 degrees)"); - layout->addWidget(_geoState.latitude, 1, 1); + mainLayout->addWidget(description); - layout->addWidget(new QLabel("Longitude"), 2, 0); - _geoState.longitude = new QLineEdit; - _geoState.longitude->setValidator(new QDoubleValidator); - _geoState.longitude->setToolTip("Longitude of camera focus point (+/- 180 degrees)"); - layout->addWidget(_geoState.longitude, 2, 1); + { - layout->addWidget(new QLabel("Altitude [m]"), 3, 0); - _geoState.altitude = new QLineEdit; - _geoState.altitude->setValidator(new QDoubleValidator); - _geoState.altitude->setToolTip("Altitude of camera (meters)"); - _geoState.altitude->setPlaceholderText("optional"); - layout->addWidget(_geoState.altitude, 3, 1); + QWidget* box = new QWidget; + QGridLayout* layout = new QGridLayout(box); - return box; + layout->addWidget(new QLabel("Anchor:"), 0, 0); + _geoState.anchor = new QLineEdit; + _geoState.anchor->setToolTip("Anchor camera to this globe (planet/moon)"); + layout->addWidget(_geoState.anchor, 0, 1); + + layout->addWidget(new QLabel("Latitude"), 1, 0); + _geoState.latitude = new QLineEdit; + _geoState.latitude->setValidator(new QDoubleValidator); + _geoState.latitude->setToolTip("Latitude of camera focus point (+/- 90 degrees)"); + layout->addWidget(_geoState.latitude, 1, 1); + + layout->addWidget(new QLabel("Longitude"), 2, 0); + _geoState.longitude = new QLineEdit; + _geoState.longitude->setValidator(new QDoubleValidator); + _geoState.longitude->setToolTip("Longitude of camera focus point (+/- 180 degrees)"); + layout->addWidget(_geoState.longitude, 2, 1); + + layout->addWidget(new QLabel("Altitude [m]"), 3, 0); + _geoState.altitude = new QLineEdit; + _geoState.altitude->setValidator(new QDoubleValidator); + _geoState.altitude->setToolTip("Altitude of camera (meters)"); + _geoState.altitude->setPlaceholderText("optional"); + layout->addWidget(_geoState.altitude, 3, 1); + + mainLayout->addWidget(box); + } + + // Add spacer at the end to prevent previous components from growing as much + mainLayout->addSpacerItem( + new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding) + ); + + return tab; } bool CameraDialog::areRequiredFormsFilledAndValid() { bool allFormsOk = true; _errorMsg->clear(); + if (_tabWidget->currentIndex() == CameraTypeNode) { + if (_nodeState.anchor->text().isEmpty()) { + allFormsOk = false; + addErrorMsg("Anchor is empty"); + } + if (!_nodeState.height->text().isEmpty()) { + if (!isNumericalLargerThan(_nodeState.height, 0.f)) { + allFormsOk = false; + addErrorMsg("Height must be larger than zero"); + } + } + } + if (_tabWidget->currentIndex() == CameraTypeNav) { if (_navState.anchor->text().isEmpty()) { allFormsOk = false; @@ -355,6 +483,7 @@ bool CameraDialog::areRequiredFormsFilledAndValid() { addErrorMsg("Longitude value is not in +/- 180.0 range"); } } + return allFormsOk; } @@ -372,7 +501,15 @@ void CameraDialog::approved() { return; } - if (_tabWidget->currentIndex() == CameraTypeNav) { + if (_tabWidget->currentIndex() == CameraTypeNode) { + openspace::Profile::CameraGoToNode node; + node.anchor = _nodeState.anchor->text().toStdString(); + if (!_nodeState.height->text().isEmpty()) { + node.height = _nodeState.height->text().toDouble(); + } + *_camera = std::move(node); + } + else if (_tabWidget->currentIndex() == CameraTypeNav) { openspace::Profile::CameraNavState nav; nav.anchor = _navState.anchor->text().toStdString(); nav.aim = _navState.aim->text().toStdString(); @@ -425,10 +562,13 @@ void CameraDialog::approved() { void CameraDialog::tabSelect(int tabIndex) { _errorMsg->clear(); - if (tabIndex == 0) { + if (tabIndex == CameraTypeNode) { + _nodeState.anchor->setFocus(Qt::OtherFocusReason); + } + else if (tabIndex == CameraTypeNav) { _navState.anchor->setFocus(Qt::OtherFocusReason); } - else if (tabIndex == 1) { + else if (tabIndex == CameraTypeGeo) { _geoState.anchor->setFocus(Qt::OtherFocusReason); } else { diff --git a/include/openspace/navigation/navigationhandler.h b/include/openspace/navigation/navigationhandler.h index fff06f53e6..fa32df0e4d 100644 --- a/include/openspace/navigation/navigationhandler.h +++ b/include/openspace/navigation/navigationhandler.h @@ -53,6 +53,7 @@ namespace openspace::interaction { struct JoystickInputStates; struct NavigationState; +struct NodeCameraStateSpec; struct WebsocketInputStates; class KeyframeNavigator; class OrbitalNavigator; @@ -146,7 +147,23 @@ public: void loadNavigationState(const std::string& filepath); - void setNavigationStateNextFrame(NavigationState state); + /** + * Set camera state from a provided navigation state next frame. The actual position + * will computed from the scene in the same frame as it is set. + * + * \param state the navigation state to compute a camera positon from + */ + void setNavigationStateNextFrame(const NavigationState& state); + + /** + * Set camera state from a provided node based camera specification structure, next + * frame. The camera position will be computed to look at the node provided in the + * node info. The actual position will computed from the scene in the same frame as + * it is set. + * + * \param spec the node specification from which to compute the resulting camera pose + */ + void setCameraFromNodeSpecNextFrame(NodeCameraStateSpec spec); /** * \return The Lua library that contains all Lua functions available to affect the @@ -155,7 +172,7 @@ public: static scripting::LuaLibrary luaLibrary(); private: - void applyNavigationState(const NavigationState& ns); + void applyPendingState(); void updateCameraTransitions(); void clearGlobalJoystickStates(); @@ -172,7 +189,7 @@ private: KeyframeNavigator _keyframeNavigator; PathNavigator _pathNavigator; - std::optional _pendingNavigationState; + std::optional> _pendingState; properties::BoolProperty _disableKeybindings; properties::BoolProperty _disableMouseInputs; diff --git a/include/openspace/navigation/waypoint.h b/include/openspace/navigation/waypoint.h index 918e66ec7d..34c6e4151e 100644 --- a/include/openspace/navigation/waypoint.h +++ b/include/openspace/navigation/waypoint.h @@ -27,6 +27,7 @@ #include #include +#include #include namespace openspace { class SceneGraphNode; } @@ -51,10 +52,42 @@ public: private: CameraPose _pose; std::string _nodeIdentifier; - // to be able to handle nodes with faulty bounding spheres + // To be able to handle nodes with faulty bounding spheres double _validBoundingSphere = 0.0; }; +/** + * Compute a waypoint from the current camera position. + * + * \return the computed WayPoint + */ +Waypoint waypointFromCamera(); + +struct NodeCameraStateSpec { + std::string identifier; + std::optional position; + std::optional height; + bool useTargetUpDirection = false; +}; + +// @TODO (2023-05-16, emmbr) Allow different light sources, not only the 'Sun' +/** + * Compute a waypoint from information about a scene graph node and a previous waypoint, + * where the camera will be facing the given node. If there is a 'Sun' node in the scene, + * it will possibly be used to compute a position on the lit side of the object. + * + * \param spec details about the node and state to create the waypoint from. Minimal + * information is the identifier of the node, but a position or height + * above the bounding sphere may also be given. + * \param startPoint an optional previous waypoint. If not specified, the current camera + * position will be used. + * \param userLinear if true, the new waypoint will be computed along a straight line + * from the start waypoint to the scene graph node or position. + * \return the computed WayPoint + */ +Waypoint computeWaypointFromNodeInfo(const NodeCameraStateSpec& spec, + std::optional startPoint = std::nullopt, bool useLinear = false); + } // namespace openspace::interaction #endif // __OPENSPACE_CORE___WAYPOINT___H__ diff --git a/include/openspace/scene/profile.h b/include/openspace/scene/profile.h index 25fff81556..db3e13d848 100644 --- a/include/openspace/scene/profile.h +++ b/include/openspace/scene/profile.h @@ -106,6 +106,13 @@ public: bool startPaused = false; }; + struct CameraGoToNode { + static constexpr std::string_view Type = "goToNode"; + + std::string anchor; + std::optional height; + }; + struct CameraNavState { static constexpr std::string_view Type = "setNavigationState"; @@ -127,7 +134,7 @@ public: std::optional altitude; }; - using CameraType = std::variant; + using CameraType = std::variant; Profile() = default; explicit Profile(const std::string& content); @@ -147,7 +154,7 @@ public: /// Removes an asset unless the `ignoreUpdates` member is set to `true` void removeAsset(const std::string& path); - static constexpr Version CurrentVersion = Version{ 1, 2 }; + static constexpr Version CurrentVersion = Version{ 1, 3 }; Version version = CurrentVersion; std::vector modules; diff --git a/modules/base/lightsource/scenegraphlightsource.cpp b/modules/base/lightsource/scenegraphlightsource.cpp index 29bdb62cda..8e2457a873 100644 --- a/modules/base/lightsource/scenegraphlightsource.cpp +++ b/modules/base/lightsource/scenegraphlightsource.cpp @@ -42,7 +42,7 @@ namespace { openspace::properties::Property::Visibility::NoviceUser }; - constexpr openspace::properties::Property::PropertyInfo NodeInfo = { + constexpr openspace::properties::Property::PropertyInfo NodeCameraStateInfo = { "Node", "Node", "The identifier of the scene graph node to follow", @@ -53,7 +53,7 @@ namespace { // [[codegen::verbatim(IntensityInfo.description)]] std::optional intensity; - // [[codegen::verbatim(NodeInfo.description)]] + // [[codegen::verbatim(NodeCameraStateInfo.description)]] std::string node [[codegen::identifier()]]; }; #include "scenegraphlightsource_codegen.cpp" @@ -67,7 +67,7 @@ documentation::Documentation SceneGraphLightSource::Documentation() { SceneGraphLightSource::SceneGraphLightSource() : _intensity(IntensityInfo, 1.f, 0.f, 1.f) - , _sceneGraphNodeReference(NodeInfo, "") + , _sceneGraphNodeReference(NodeCameraStateInfo, "") { addProperty(_intensity); _sceneGraphNodeReference.onChange([this]() { diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index bb86514c37..6e526a3f7d 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -1757,6 +1758,15 @@ void setCameraFromProfile(const Profile& p) { geoScript, scripting::ScriptEngine::RemoteScripting::Yes ); + }, + [](const Profile::CameraGoToNode& node) { + using namespace interaction; + NodeCameraStateSpec spec; + spec.identifier = node.anchor; + spec.height = node.height; + spec.useTargetUpDirection = true; + + global::navigationHandler->setCameraFromNodeSpecNextFrame(spec); } }, p.camera.value() diff --git a/src/navigation/navigationhandler.cpp b/src/navigation/navigationhandler.cpp index 966cd0ef6c..637bdba6cd 100644 --- a/src/navigation/navigationhandler.cpp +++ b/src/navigation/navigationhandler.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -140,8 +141,12 @@ void NavigationHandler::setCamera(Camera* camera) { _orbitalNavigator.setCamera(camera); } -void NavigationHandler::setNavigationStateNextFrame(NavigationState state) { - _pendingNavigationState = std::move(state); +void NavigationHandler::setNavigationStateNextFrame(const NavigationState& state) { + _pendingState = state; +} + +void NavigationHandler::setCameraFromNodeSpecNextFrame(NodeCameraStateSpec spec) { + _pendingState = std::move(spec); } OrbitalNavigator& NavigationHandler::orbitalNavigator() { @@ -175,9 +180,9 @@ void NavigationHandler::setInterpolationTime(float durationInSeconds) { void NavigationHandler::updateCamera(double deltaTime) { ghoul_assert(_camera != nullptr, "Camera must not be nullptr"); - // If there is a navigation state to set, do so immediately and then return - if (_pendingNavigationState.has_value()) { - applyNavigationState(*_pendingNavigationState); + // If there is a state to set, do so immediately and then return + if (_pendingState.has_value()) { + applyPendingState(); return; } @@ -214,13 +219,27 @@ void NavigationHandler::updateCamera(double deltaTime) { _orbitalNavigator.updateCameraScalingFromAnchor(deltaTime); } -void NavigationHandler::applyNavigationState(const NavigationState& ns) { - _orbitalNavigator.setAnchorNode(ns.anchor); - _orbitalNavigator.setAimNode(ns.aim); - _camera->setPose(ns.cameraPose()); +void NavigationHandler::applyPendingState() { + ghoul_assert(_pendingState.has_value(), "Pending pose must have a value"); + + std::variant pending = *_pendingState; + if (std::holds_alternative(pending)) { + NavigationState ns = std::get(pending); + _orbitalNavigator.setAnchorNode(ns.anchor); + _orbitalNavigator.setAimNode(ns.aim); + _camera->setPose(ns.cameraPose()); + } + else if (std::holds_alternative(pending)) { + NodeCameraStateSpec spec = std::get(pending); + Waypoint wp = computeWaypointFromNodeInfo(spec); + + _orbitalNavigator.setAnchorNode(wp.nodeIdentifier()); + _orbitalNavigator.setAimNode(""); + _camera->setPose(wp.pose()); + } resetNavigationUpdateVariables(); - _pendingNavigationState.reset(); + _pendingState.reset(); } void NavigationHandler::updateCameraTransitions() { diff --git a/src/navigation/navigationstate.cpp b/src/navigation/navigationstate.cpp index 81c23ab927..a1f11ba337 100644 --- a/src/navigation/navigationstate.cpp +++ b/src/navigation/navigationstate.cpp @@ -115,6 +115,9 @@ CameraPose NavigationState::cameraPose() const { const glm::dmat3 referenceFrameTransform = referenceFrameNode->modelTransform(); + // @TODO (2023-05-16, emmbr) This computation is wrong and has to be fixed! Only + // works if the reference frame is also the anchor node. I remember that fixing it + // was not as easy as using referenceFrameNode instead of anchor node though.. resultingPose.position = anchorNode->worldPosition() + referenceFrameTransform * glm::dvec3(position); diff --git a/src/navigation/path.cpp b/src/navigation/path.cpp index 95a7f51830..0d741bbf89 100644 --- a/src/navigation/path.cpp +++ b/src/navigation/path.cpp @@ -45,8 +45,6 @@ namespace { constexpr std::string_view _loggerCat = "Path"; constexpr float LengthEpsilon = 1e-5f; - constexpr const char* SunIdentifier = "Sun"; - // TODO: where should this documentation be? // It's nice to have these to interpret the dictionary when creating the path, but // maybe it's not really necessary @@ -458,138 +456,6 @@ double Path::speedAlongPath(double traveledDistance) const { return _speedFactorFromDuration * speed * dampeningFactor; } -Waypoint waypointFromCamera() { - Camera* camera = global::navigationHandler->camera(); - const glm::dvec3 pos = camera->positionVec3(); - const glm::dquat rot = camera->rotationQuaternion(); - const std::string node = global::navigationHandler->anchorNode()->identifier(); - return Waypoint{ pos, rot, node }; -} - -// Compute a target position close to the specified target node, using knowledge of -// the start point and a desired distance from the node's center -glm::dvec3 computeGoodStepDirection(const SceneGraphNode* targetNode, - const Waypoint& startPoint) -{ - const glm::dvec3 nodePos = targetNode->worldPosition(); - const SceneGraphNode* sun = sceneGraphNode(SunIdentifier); - const SceneGraphNode* closeNode = PathNavigator::findNodeNearTarget(targetNode); - - // @TODO (2021-07-09, emmbr): Not nice to depend on a specific scene graph node, - // as it might not exist. Ideally, each SGN could know about their preferred - // direction to be viewed from (their "good side"), and then that could be queried - // and used instead. - if (closeNode) { - // If the node is close to another node in the scene, set the direction in a way - // that minimizes risk of collision - return glm::normalize(nodePos - closeNode->worldPosition()); - } - else if (!sun) { - // Can't compute position from Sun position, so just use any direction. Z will do - return glm::dvec3(0.0, 0.0, 1.0); - } - else if (targetNode->identifier() == SunIdentifier) { - // Special case for when the target is the Sun, in which we want to avoid a zero - // vector. The Z axis is chosen to provide an overview of the solar system, and - // not stay in the orbital plane - return glm::dvec3(0.0, 0.0, 1.0); - } - else { - // Go to a point that is lit up by the sun, slightly offsetted from sun direction - const glm::dvec3 sunPos = sun->worldPosition(); - - const glm::dvec3 prevPos = startPoint.position(); - const glm::dvec3 targetToPrev = prevPos - nodePos; - const glm::dvec3 targetToSun = sunPos - nodePos; - - // Check against zero vectors, as this will lead to nan-values from cross product - if (glm::length(targetToSun) < LengthEpsilon || - glm::length(targetToPrev) < LengthEpsilon) - { - // Same situation as if sun does not exist. Any direction will do - return glm::dvec3(0.0, 0.0, 1.0); - } - - constexpr float defaultPositionOffsetAngle = -30.f; // degrees - constexpr float angle = glm::radians(defaultPositionOffsetAngle); - const glm::dvec3 axis = glm::normalize(glm::cross(targetToPrev, targetToSun)); - const glm::dquat offsetRotation = angleAxis(static_cast(angle), axis); - - return glm::normalize(offsetRotation * targetToSun); - } -} - -struct NodeInfo { - std::string identifier; - std::optional position; - std::optional height; - bool useTargetUpDirection; -}; - -Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& startPoint, - Path::Type type) -{ - const SceneGraphNode* targetNode = sceneGraphNode(info.identifier); - if (!targetNode) { - LERROR(fmt::format("Could not find target node '{}'", info.identifier)); - return Waypoint(); - } - - glm::dvec3 stepDir; - glm::dvec3 targetPos; - if (info.position.has_value()) { - // The position in instruction is given in the targetNode's local coordinates. - // Convert to world coordinates - targetPos = glm::dvec3( - targetNode->modelTransform() * glm::dvec4(*info.position, 1.0) - ); - } - else { - const PathNavigator& navigator = global::navigationHandler->pathNavigator(); - - const double radius = navigator.findValidBoundingSphere(targetNode); - const double defaultHeight = radius * navigator.arrivalDistanceFactor(); - const double height = info.height.value_or(defaultHeight); - const double distanceFromNodeCenter = radius + height; - - if (type == Path::Type::Linear) { - // If linear path, compute position along line form start to end point - glm::dvec3 endNodePos = targetNode->worldPosition(); - stepDir = glm::normalize(startPoint.position() - endNodePos); - } - else { - stepDir = computeGoodStepDirection(targetNode, startPoint); - } - - targetPos = targetNode->worldPosition() + stepDir * distanceFromNodeCenter; - } - - glm::dvec3 up = global::navigationHandler->camera()->lookUpVectorWorldSpace(); - if (info.useTargetUpDirection) { - // @TODO (emmbr 2020-11-17) For now, this is hardcoded to look good for Earth, - // which is where it matters the most. A better solution would be to make each - // sgn aware of its own 'up' and query - up = targetNode->worldRotationMatrix() * glm::dvec3(0.0, 0.0, 1.0); - } - - // Compute rotation so the camera is looking at the targetted node - glm::dvec3 lookAtPos = targetNode->worldPosition(); - - // Check if we can distinguish between targetpos and lookAt pos. Otherwise, move it further away - const glm::dvec3 diff = targetPos - lookAtPos; - double distSquared = glm::dot(diff, diff); - if (std::isnan(distSquared) || distSquared < LengthEpsilon) { - double startToEndDist = glm::length( - startPoint.position() - targetNode->worldPosition() - ); - lookAtPos = targetPos - stepDir * 0.1 * startToEndDist; - } - - const glm::dquat targetRot = ghoul::lookAtQuaternion(targetPos, lookAtPos, up); - - return Waypoint(targetPos, targetRot, info.identifier); -} - void checkVisibilityAndShowMessage(const SceneGraphNode* node) { auto isEnabled = [](const Renderable* r) { std::any propertyValueAny = r->property("Enabled")->get(); @@ -639,7 +505,7 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, bool hasStart = p.startState.has_value(); const Waypoint startPoint = hasStart ? Waypoint(NavigationState(p.startState.value())) : - waypointFromCamera(); + interaction::waypointFromCamera(); std::vector waypoints; switch (p.targetType) { @@ -668,7 +534,7 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, )); } - NodeInfo info { + interaction::NodeCameraStateSpec info { nodeIdentifier, p.position, p.height, @@ -692,7 +558,8 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, type = Path::Type::Linear; } - waypoints = { computeWaypointFromNodeInfo(info, startPoint, type) }; + bool isLinear = type == Path::Type::Linear; + waypoints = { computeWaypointFromNodeInfo(info, startPoint, isLinear) }; break; } default: diff --git a/src/navigation/waypoint.cpp b/src/navigation/waypoint.cpp index 2a4c81235a..c7f1b86286 100644 --- a/src/navigation/waypoint.cpp +++ b/src/navigation/waypoint.cpp @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -35,6 +36,9 @@ namespace { constexpr std::string_view _loggerCat = "Waypoint"; + + constexpr float LengthEpsilon = 1e-5f; + constexpr const char* SunIdentifier = "Sun"; } // namespace namespace openspace::interaction { @@ -92,4 +96,133 @@ double Waypoint::validBoundingSphere() const { return _validBoundingSphere; } +Waypoint waypointFromCamera() { + Camera* camera = global::navigationHandler->camera(); + const glm::dvec3 pos = camera->positionVec3(); + const glm::dquat rot = camera->rotationQuaternion(); + const std::string node = global::navigationHandler->anchorNode()->identifier(); + return Waypoint{ pos, rot, node }; +} + +// Compute a target position close to the specified target node, using knowledge of +// the start point and a desired distance from the node's center +glm::dvec3 computeGoodStepDirection(const SceneGraphNode* targetNode, + const Waypoint& startPoint) +{ + const glm::dvec3 nodePos = targetNode->worldPosition(); + const SceneGraphNode* sun = sceneGraphNode(SunIdentifier); + const SceneGraphNode* closeNode = PathNavigator::findNodeNearTarget(targetNode); + + // @TODO (2021-07-09, emmbr): Not nice to depend on a specific scene graph node, + // as it might not exist. Ideally, each SGN could know about their preferred + // direction to be viewed from (their "good side"), and then that could be queried + // and used instead. + if (closeNode) { + // If the node is close to another node in the scene, set the direction in a way + // that minimizes risk of collision + return glm::normalize(nodePos - closeNode->worldPosition()); + } + else if (!sun) { + // Can't compute position from Sun position, so just use any direction. Z will do + return glm::dvec3(0.0, 0.0, 1.0); + } + else if (targetNode->identifier() == SunIdentifier) { + // Special case for when the target is the Sun, in which we want to avoid a zero + // vector. The Z axis is chosen to provide an overview of the solar system, and + // not stay in the orbital plane + return glm::dvec3(0.0, 0.0, 1.0); + } + else { + // Go to a point that is lit up by the sun, slightly offsetted from sun direction + const glm::dvec3 sunPos = sun->worldPosition(); + + const glm::dvec3 prevPos = startPoint.position(); + const glm::dvec3 targetToPrev = prevPos - nodePos; + const glm::dvec3 targetToSun = sunPos - nodePos; + + // Check against zero vectors, as this will lead to nan-values from cross product + if (glm::length(targetToSun) < LengthEpsilon || + glm::length(targetToPrev) < LengthEpsilon) + { + // Same situation as if sun does not exist. Any direction will do + return glm::dvec3(0.0, 0.0, 1.0); + } + + constexpr float defaultPositionOffsetAngle = -30.f; // degrees + constexpr float angle = glm::radians(defaultPositionOffsetAngle); + const glm::dvec3 axis = glm::normalize(glm::cross(targetToPrev, targetToSun)); + const glm::dquat offsetRotation = angleAxis(static_cast(angle), axis); + + return glm::normalize(offsetRotation * targetToSun); + } +} + +Waypoint computeWaypointFromNodeInfo(const NodeCameraStateSpec& spec, + std::optional startPoint, + bool useLinear) +{ + const SceneGraphNode* targetNode = sceneGraphNode(spec.identifier); + if (!targetNode) { + LERROR(fmt::format("Could not find target node '{}'", spec.identifier)); + return Waypoint(); + } + + // Use current camera position if no previous point was specified + Waypoint prevPoint = startPoint.value_or(waypointFromCamera()); + + glm::dvec3 stepDir; + glm::dvec3 targetPos; + if (spec.position.has_value()) { + // The position in instruction is given in the targetNode's local coordinates. + // Convert to world coordinates + targetPos = glm::dvec3( + targetNode->modelTransform() * glm::dvec4(*spec.position, 1.0) + ); + } + else { + const PathNavigator& navigator = global::navigationHandler->pathNavigator(); + + const double radius = navigator.findValidBoundingSphere(targetNode); + const double defaultHeight = radius * navigator.arrivalDistanceFactor(); + const double height = spec.height.value_or(defaultHeight); + const double distanceFromNodeCenter = radius + height; + + if (useLinear) { + // If linear path, compute position along line form start to end point + glm::dvec3 endNodePos = targetNode->worldPosition(); + stepDir = glm::normalize(prevPoint.position() - endNodePos); + } + else { + stepDir = computeGoodStepDirection(targetNode, prevPoint); + } + + targetPos = targetNode->worldPosition() + stepDir * distanceFromNodeCenter; + } + + glm::dvec3 up = global::navigationHandler->camera()->lookUpVectorWorldSpace(); + if (spec.useTargetUpDirection) { + // @TODO (emmbr 2020-11-17) For now, this is hardcoded to look good for Earth, + // which is where it matters the most. A better solution would be to make each + // sgn aware of its own 'up' and query + up = targetNode->worldRotationMatrix() * glm::dvec3(0.0, 0.0, 1.0); + } + + // Compute rotation so the camera is looking at the targetted node + glm::dvec3 lookAtPos = targetNode->worldPosition(); + + // Check if we can distinguish between targetpos and lookAt pos. Otherwise, move it further away + const glm::dvec3 diff = targetPos - lookAtPos; + double distSquared = glm::dot(diff, diff); + if (std::isnan(distSquared) || distSquared < LengthEpsilon) { + double startToEndDist = glm::length( + prevPoint.position() - targetNode->worldPosition() + ); + lookAtPos = targetPos - stepDir * 0.1 * startToEndDist; + } + + const glm::dquat targetRot = ghoul::lookAtQuaternion(targetPos, lookAtPos, up); + + return Waypoint(targetPos, targetRot, spec.identifier); +} + } // namespace openspace::interaction diff --git a/src/scene/profile.cpp b/src/scene/profile.cpp index fb0c3c355e..e2d72bfd9d 100644 --- a/src/scene/profile.cpp +++ b/src/scene/profile.cpp @@ -346,6 +346,34 @@ void from_json(const nlohmann::json& j, Profile::Time& v) { j["is_paused"].get_to(v.startPaused); } +void to_json(nlohmann::json& j, const Profile::CameraGoToNode& v) { + j["type"] = Profile::CameraGoToNode::Type; + j["anchor"] = v.anchor; + if (v.height.has_value()) { + j["height"] = *v.height; + } +} + +void from_json(const nlohmann::json& j, Profile::CameraGoToNode& v) { + ghoul_assert( + j.at("type").get() == Profile::CameraGoToNode::Type, + "Wrong type for Camera" + ); + + checkValue(j, "anchor", &nlohmann::json::is_string, "camera", false); + checkValue(j, "height", &nlohmann::json::is_number, "camera", true); + checkExtraKeys( + j, + "camera", + { "type", "anchor", "height"} + ); + + j["anchor"].get_to(v.anchor); + if (j.find("height") != j.end()) { + v.height = j["height"].get(); + } +} + void to_json(nlohmann::json& j, const Profile::CameraNavState& v) { j["type"] = Profile::CameraNavState::Type; j["anchor"] = v.anchor; @@ -586,6 +614,14 @@ void convertVersion11to12(nlohmann::json& profile) { } // namespace version11 +namespace version12 { + +void convertVersion12to13(nlohmann::json& profile) { + // Version 1.3 introduced to GoToNode camera initial position + profile["version"] = Profile::Version{ 1, 3 }; +} + +} // namespace version12 Profile::ParsingError::ParsingError(Severity severity_, std::string msg) : ghoul::RuntimeError(std::move(msg), "profile") @@ -687,8 +723,9 @@ std::string Profile::serialize() const { if (camera.has_value()) { r["camera"] = std::visit( overloaded { + [](const CameraGoToNode& c) { return nlohmann::json(c); }, [](const CameraNavState& c) { return nlohmann::json(c); }, - [](const Profile::CameraGoToGeo& c) { return nlohmann::json(c); } + [](const CameraGoToGeo& c) { return nlohmann::json(c); } }, *camera ); @@ -720,6 +757,11 @@ Profile::Profile(const std::string& content) { profile["version"].get_to(version); } + if (version.major == 1 && version.minor == 2) { + version12::convertVersion12to13(profile); + profile["version"].get_to(version); + } + if (profile.find("modules") != profile.end()) { profile["modules"].get_to(modules); @@ -749,7 +791,10 @@ Profile::Profile(const std::string& content) { } if (profile.find("camera") != profile.end()) { nlohmann::json c = profile.at("camera"); - if (c["type"].get() == CameraNavState::Type) { + if (c["type"].get() == CameraGoToNode::Type) { + camera = c.get(); + } + else if (c["type"].get() == CameraNavState::Type) { camera = c.get(); } else if (c["type"].get() == CameraGoToGeo::Type) { diff --git a/tests/profile/basic/camera_gotonode.profile b/tests/profile/basic/camera_gotonode.profile new file mode 100644 index 0000000000..f950613410 --- /dev/null +++ b/tests/profile/basic/camera_gotonode.profile @@ -0,0 +1,7 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": "anchor" + } +} diff --git a/tests/profile/basic/camera_gotonode_height.profile b/tests/profile/basic/camera_gotonode_height.profile new file mode 100644 index 0000000000..dafd6ce075 --- /dev/null +++ b/tests/profile/basic/camera_gotonode_height.profile @@ -0,0 +1,8 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": "anchor", + "height": 100.0 + } +} diff --git a/tests/profile/error/camera/gotonode_invalidvalue_negative_height.profile b/tests/profile/error/camera/gotonode_invalidvalue_negative_height.profile new file mode 100644 index 0000000000..0ec118e3e8 --- /dev/null +++ b/tests/profile/error/camera/gotonode_invalidvalue_negative_height.profile @@ -0,0 +1,8 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": "anchor", + "height": -10.0 + } +} diff --git a/tests/profile/error/camera/gotonode_invalidvalue_zero_height.profile b/tests/profile/error/camera/gotonode_invalidvalue_zero_height.profile new file mode 100644 index 0000000000..245f8ed6e7 --- /dev/null +++ b/tests/profile/error/camera/gotonode_invalidvalue_zero_height.profile @@ -0,0 +1,8 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": "anchor", + "height": 0.0 + } +} diff --git a/tests/profile/error/camera/gotonode_missing_anchor.profile b/tests/profile/error/camera/gotonode_missing_anchor.profile new file mode 100644 index 0000000000..21381edcaa --- /dev/null +++ b/tests/profile/error/camera/gotonode_missing_anchor.profile @@ -0,0 +1,6 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode" + } +} diff --git a/tests/profile/error/camera/gotonode_wrongtype_anchor.profile b/tests/profile/error/camera/gotonode_wrongtype_anchor.profile new file mode 100644 index 0000000000..36b9d78b78 --- /dev/null +++ b/tests/profile/error/camera/gotonode_wrongtype_anchor.profile @@ -0,0 +1,7 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": 1.0 + } +} diff --git a/tests/profile/error/camera/gotonode_wrongtype_height.profile b/tests/profile/error/camera/gotonode_wrongtype_height.profile new file mode 100644 index 0000000000..ba3312fca0 --- /dev/null +++ b/tests/profile/error/camera/gotonode_wrongtype_height.profile @@ -0,0 +1,8 @@ +{ + "version": { "major": 1, "minor": 3 }, + "camera": { + "type": "goToNode", + "anchor": "anchor", + "height": "0.0" + } +} diff --git a/tests/test_profile.cpp b/tests/test_profile.cpp index ffe21b86c2..d5bc06bb5b 100644 --- a/tests/test_profile.cpp +++ b/tests/test_profile.cpp @@ -93,6 +93,12 @@ namespace openspace { return lhs.type == rhs.type && lhs.value == rhs.value; } + bool operator==(const openspace::Profile::CameraGoToNode& lhs, + const openspace::Profile::CameraGoToNode& rhs) noexcept + { + return lhs.anchor == rhs.anchor && lhs.height == rhs.height; + } + bool operator==(const openspace::Profile::CameraNavState& lhs, const openspace::Profile::CameraNavState& rhs) noexcept { @@ -166,8 +172,8 @@ TEST_CASE("Minimal", "[profile]") { Profile profile = loadProfile(absPath(File)); Profile ref; - ref.version.major = 1; - ref.version.minor = 2; + ref.version = Profile::CurrentVersion; + CHECK(profile == ref); } @@ -652,6 +658,37 @@ TEST_CASE("Basic Camera GoToGeo (with altitude)", "[profile]") { CHECK(profile == ref); } +TEST_CASE("Basic Camera GoToNode", "[profile]") { + constexpr std::string_view File = + "${TESTDIR}/profile/basic/camera_gotonode.profile"; + Profile profile = loadProfile(absPath(File)); + + Profile ref; + ref.version = Profile::CurrentVersion; + + Profile::CameraGoToNode camera; + camera.anchor = "anchor"; + ref.camera = camera; + + CHECK(profile == ref); +} + +TEST_CASE("Basic Camera GoToNode (with height)", "[profile]") { + constexpr std::string_view File = + "${TESTDIR}/profile/basic/camera_gotonode_height.profile"; + Profile profile = loadProfile(absPath(File)); + + Profile ref; + ref.version = Profile::CurrentVersion; + + Profile::CameraGoToNode camera; + camera.anchor = "anchor"; + camera.height = 100.0; + ref.camera = camera; + + CHECK(profile == ref); +} + TEST_CASE("Basic Mark Nodes", "[profile]") { constexpr std::string_view File = "${TESTDIR}/profile/basic/mark_nodes.profile"; Profile profile = loadProfile(absPath(File)); @@ -1604,3 +1641,52 @@ TEST_CASE("(Error) Camera (GoToGeo): Wrong type 'altitude'", "[profile]") { Catch::Matchers::Equals("(profile) 'camera.altitude' must be a number") ); } + +TEST_CASE("(Error) Camera (GoToNode): Wrong type 'anchor'", "[profile]") { + constexpr std::string_view TestFile = + "${TESTDIR}/profile/error/camera/gotonode_wrongtype_anchor.profile"; + CHECK_THROWS_WITH( + loadProfile(absPath(TestFile)), + Catch::Matchers::Equals("(profile) 'camera.anchor' must be a string") + ); +} + +TEST_CASE("(Error) Camera (GoToNode): Wrong type 'height'", "[profile]") { + constexpr std::string_view TestFile = + "${TESTDIR}/profile/error/camera/gotonode_wrongtype_height.profile"; + CHECK_THROWS_WITH( + loadProfile(absPath(TestFile)), + Catch::Matchers::Equals("(profile) 'camera.height' must be a number") + ); +} + +TEST_CASE("(Error) Camera (GoToNode): Missing value 'anchor'", "[profile]") { + constexpr std::string_view TestFile = + "${TESTDIR}/profile/error/camera/gotonode_missing_anchor.profile"; + CHECK_THROWS_WITH( + loadProfile(absPath(TestFile)), + Catch::Matchers::Equals("(profile) 'camera.anchor' field is missing") + ); +} + +// @TODO (2023-05-17, emmbr) Seems like the profile loading actually do not validate +// profile values to a larger degree than type and whether the value is missing, currently. +// So this is left as future work +// +//TEST_CASE("(Error) Camera (GoToNode): Invalid value for 'height' - negative", "[profile]") { +// constexpr std::string_view TestFile = +// "${TESTDIR}/profile/error/camera/gotonode_invalidvalue_negative_height.profile"; +// CHECK_THROWS_WITH( +// loadProfile(absPath(TestFile)), +// Catch::Matchers::Equals("(profile) 'camera.height' must be a larger than zero") +// ); +//} +// +//TEST_CASE("(Error) Camera (GoToNode): Invalid value for 'height' - zero", "[profile]") { +// constexpr std::string_view TestFile = +// "${TESTDIR}/profile/error/camera/gotonode_invalidvalue_zero_height.profile"; +// CHECK_THROWS_WITH( +// loadProfile(absPath(TestFile)), +// Catch::Matchers::Equals("(profile) 'camera.height' must be a larger than zero") +// ); +//}