From c949a9892c009f87d47e4f8c1fa7d1d272353e7d Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Wed, 24 May 2023 11:22:41 +0200 Subject: [PATCH] Issue/2227 - Option to just specify a scene graph node in profile editor camera dialog (#2699) * Add profile edit to start camera at a given scene graph node * Restructure some navigation code to allow computing camera pose from node * Delay computation of camera pose for node spec as well... And give NodeInfo a more extensive name (The objects may move during the frame, making the computed pose invalid) * Update to make scene graph node first option in editor * Add some description to each tab in the camera dialog * Add operator== for CameraGoToNode struct to make the unit tests compile * Add version handling for new profile version * Add option to specify an optional height * Update current version constant, for old test cases to go through successfully * Add some test files Note that apparently, the profile loading does not check the values of the individual fields, just existence, and type. So added two test cases that are not currently checked. --------- Co-authored-by: Alexander Bock --- .../launcher/include/profile/cameradialog.h | 7 + .../ext/launcher/resources/qss/launcher.qss | 7 + .../ext/launcher/src/profile/cameradialog.cpp | 358 ++++++++++++------ .../openspace/navigation/navigationhandler.h | 23 +- include/openspace/navigation/waypoint.h | 35 +- include/openspace/scene/profile.h | 11 +- .../lightsource/scenegraphlightsource.cpp | 6 +- src/engine/openspaceengine.cpp | 10 + src/navigation/navigationhandler.cpp | 39 +- src/navigation/navigationstate.cpp | 3 + src/navigation/path.cpp | 141 +------ src/navigation/waypoint.cpp | 133 +++++++ src/scene/profile.cpp | 49 ++- tests/profile/basic/camera_gotonode.profile | 7 + .../basic/camera_gotonode_height.profile | 8 + ...onode_invalidvalue_negative_height.profile | 8 + .../gotonode_invalidvalue_zero_height.profile | 8 + .../camera/gotonode_missing_anchor.profile | 6 + .../camera/gotonode_wrongtype_anchor.profile | 7 + .../camera/gotonode_wrongtype_height.profile | 8 + tests/test_profile.cpp | 90 ++++- 21 files changed, 695 insertions(+), 269 deletions(-) create mode 100644 tests/profile/basic/camera_gotonode.profile create mode 100644 tests/profile/basic/camera_gotonode_height.profile create mode 100644 tests/profile/error/camera/gotonode_invalidvalue_negative_height.profile create mode 100644 tests/profile/error/camera/gotonode_invalidvalue_zero_height.profile create mode 100644 tests/profile/error/camera/gotonode_missing_anchor.profile create mode 100644 tests/profile/error/camera/gotonode_wrongtype_anchor.profile create mode 100644 tests/profile/error/camera/gotonode_wrongtype_height.profile 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") +// ); +//}