diff --git a/include/openspace/query/query.h b/include/openspace/query/query.h index 02c0fdd689..9cee40a2ea 100644 --- a/include/openspace/query/query.h +++ b/include/openspace/query/query.h @@ -43,7 +43,7 @@ SceneGraphNode* sceneGraphNode(const std::string& name); const Renderable* renderable(const std::string& name); properties::Property* property(const std::string& uri); properties::PropertyOwner* propertyOwner(const std::string& uri); -std::vector allProperties(); +const std::vector& allProperties(); } // namespace openspace diff --git a/src/query/query.cpp b/src/query/query.cpp index 001650a4e6..8499b21ad1 100644 --- a/src/query/query.cpp +++ b/src/query/query.cpp @@ -61,7 +61,7 @@ properties::PropertyOwner* propertyOwner(const std::string& uri) { return property; } -std::vector allProperties() { +const std::vector& allProperties() { return global::openSpaceEngine->allProperties(); } diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp index b2108d30a7..0cbc7ab58c 100644 --- a/src/scene/scene.cpp +++ b/src/scene/scene.cpp @@ -110,7 +110,7 @@ namespace { catch (...) { return false; } - }; + }; if (value == "true" || value == "false") { return openspace::PropertyValueType::Boolean; @@ -754,10 +754,10 @@ void Scene::setPropertiesFromProfile(const Profile& p) { continue; } std::string uriOrRegex = prop.name; - std::string groupName; - if (doesUriContainGroupTag(uriOrRegex, groupName)) { + std::string groupName = groupTag(uriOrRegex); + if (!groupName.empty()) { // Remove group name from start of regex and replace with '*' - uriOrRegex = removeGroupNameFromUri(uriOrRegex); + uriOrRegex = removeGroupTagFromUri(uriOrRegex); } _profilePropertyName = uriOrRegex; ghoul::lua::push(L, uriOrRegex); @@ -771,7 +771,6 @@ void Scene::setPropertiesFromProfile(const Profile& p) { applyRegularExpression( L, uriOrRegex, - allProperties(), 0.0, groupName, ghoul::EasingFunction::Linear, @@ -813,7 +812,7 @@ void Scene::propertyPushProfileValueToLua(ghoul::lua::LuaState& L, std::vector Scene::propertiesMatchingRegex( std::string_view propertyString) { - return findMatchesInAllProperties(propertyString, allProperties(), ""); + return findMatchesInAllProperties(propertyString, ""); } std::vector Scene::allTags() const { @@ -848,69 +847,119 @@ scripting::LuaLibrary Scene::luaLibrary() { { "setPropertyValue", &luascriptfunctions::propertySetValue, - {}, + { + { "uri", "String" }, + { "value", "String | Number | Boolean | Table" }, + { "duration", "Number?", "0.0" }, + { "easing", "EasingFunction?", "Linear" }, + { "postscript", "String?", "" } + }, "", - "Sets all property(s) identified by the URI (with potential wildcards) " - "in the first argument. The second argument can be any type, but it has " - "to match the type that the property (or properties) expect. If the " - "third is not present or is '0', the value changes instantly, otherwise " - "the change will take that many seconds and the value is interpolated at " - "each step in between. The fourth parameter is an optional easing " - "function if a 'duration' has been specified. If 'duration' is 0, this " - "parameter value is ignored. Otherwise, it can be one of many supported " - "easing functions. See easing.h for available functions. The fifth " - "argument is another Lua script that will be executed when the " - "interpolation provided in parameter 3 finishes.\n" - "The URI is interpreted using a wildcard in which '*' is expanded to " - "'(.*)' and bracketed components '{ }' are interpreted as group tag " - "names. Then, the passed value will be set on all properties that fit " - "the regex + group name combination.", - {} + R"(Sets the property or properties identified by the URI to the specified +value. The `uri` identifies which property or properties are affected by this function +call and can include both wildcards `*` which match anything, as well as tags (`{tag}`) +which match scene graph nodes that have this tag. There is also the ability to combine two +tags through the `&`, `|`, and `~` operators. `{tag1&tag2}` will match anything that has +the tag1 and the tag2. `{tag1|tag2}` will match anything that has the tag1 or the tag 2, +and `{tag1~tag2}` will match anything that has tag1 but not tag2. If no wildcards or tags +are provided at most one property value will be changed. With wildcards or tags all +properties that match the URI are changed instead. The second argument's type must match +the type of the property or properties or an error is raised. If a duration is provided, +the requested change will occur over the provided number of seconds. If no duration is +provided or the duration is 0, the change occurs instantaneously. + +For example `openspace.setPropertyValue("*Trail.Renderable.Enabled", true)` will enable +any property that ends with "Trail.Renderable.Enabled", for example +"StarTrail.Renderable.Enabled", "EarthTrail.Renderable.Enabled", but not +"EarthTrail.Renderable.Size". + +`openspace.setPropertyValue("{tag1}.Renderable.Enabled", true)` will enable any node in +the scene that has the "tag1" assigned to it. + +If you only want to change a single property value, also see the #setPropertyValueSingle +function as it will do so in a more efficient way. The `setPropertyValue` function will +work for individual property value, but is more computationally expensive. + +\\param uri The URI that identifies the property or properties whose values should be +changed. The URI can contain 0 or 1 wildcard `*` characters or a tag expression (`{tag}`) +that identifies a property owner +\\param value The new value to which the property/properties identified by the `uri` +should be changed to. The type of this parameter must agree with the type of the selected +property +\\param duration The number of seconds over which the change will occur. If not provided +or the provided value is 0, the change is instantaneously. +\\param easing If a duration larger than 0 is provided, this parameter controls the manner +in which the parameter is interpolated. Has to be one of "Linear", "QuadraticEaseIn", +"QuadraticEaseOut", "QuadraticEaseInOut", "CubicEaseIn", "CubicEaseOut", "CubicEaseInOut", +"QuarticEaseIn", "QuarticEaseOut", "QuarticEaseInOut", "QuinticEaseIn", "QuinticEaseOut", +"QuinticEaseInOut", "SineEaseIn", "SineEaseOut", "SineEaseInOut", "CircularEaseIn", +"CircularEaseOut", "CircularEaseInOut", "ExponentialEaseIn", "ExponentialEaseOut", +"ExponentialEaseInOut", "ElasticEaseIn", "ElasticEaseOut", "ElasticEaseInOut", +"BounceEaseIn", "BounceEaseOut", "BounceEaseInOut" +\\param postscript A Lua script that will be executed once the change of property value +is completed. If a duration larger than 0 was provided, it is at the end of the +interpolation. If 0 was provided, the script runs immediately. +)" }, { "setPropertyValueSingle", &luascriptfunctions::propertySetValue, - {}, + { + { "uri", "String" }, + { "value", "String | Number | Boolean | Table" }, + { "duration", "Number?", "0.0" }, + { "easing", "EasingFunction?", "Linear" }, + { "postscript", "String?", "" } + }, "", - "Sets the property identified by the URI in the first argument. The " - "second argument can be any type, but it has to match the type that the " - "property expects. If the third is not present or is '0', the value " - "changes instantly, otherwise the change will take that many seconds and " - "the value is interpolated at each step in between. The fourth " - "parameter is an optional easing function if a 'duration' has been " - "specified. If 'duration' is 0, this parameter value is ignored. " - "Otherwise, it has to be one of the easing functions defined in the list " - "below. This is the same as calling the setValue method and passing " - "'single' as the fourth argument to setPropertyValue. The fifth argument " - "is another Lua script that will be executed when the interpolation " - "provided in parameter 3 finishes.\n Avaiable easing functions: " - "Linear, QuadraticEaseIn, QuadraticEaseOut, QuadraticEaseInOut, " - "CubicEaseIn, CubicEaseOut, CubicEaseInOut, QuarticEaseIn, " - "QuarticEaseOut, QuarticEaseInOut, QuinticEaseIn, QuinticEaseOut, " - "QuinticEaseInOut, SineEaseIn, SineEaseOut, SineEaseInOut, " - "CircularEaseIn, CircularEaseOut, CircularEaseInOut, ExponentialEaseIn, " - "ExponentialEaseOut, ExponentialEaseInOut, ElasticEaseIn, " - "ElasticEaseOut, ElasticEaseInOut, BounceEaseIn, BounceEaseOut, " - "BounceEaseInOut", - {} + R"(Sets the single property identified by the URI to the specified value. +The `uri` identifies which property is affected by this function call. The second +argument's type must match the type of the property or an error is raised. If a duration +is provided, the requested change will occur over the provided number of seconds. If no +duration is provided or the duration is 0, the change occurs instantaneously. + +If you want to change multiple property values simultaneously, also see the +#setPropertyValue function. The `setPropertyValueSingle` function however will work more +efficiently for individual property values. + +\\param uri The URI that identifies the property +\\param value The new value to which the property identified by the `uri` should be +changed to. The type of this parameter must agree with the type of the selected property +\\param duration The number of seconds over which the change will occur. If not provided +or the provided value is 0, the change is instantaneously. +\\param easing If a duration larger than 0 is provided, this parameter controls the manner +in which the parameter is interpolated. Has to be one of "Linear", "QuadraticEaseIn", +"QuadraticEaseOut", "QuadraticEaseInOut", "CubicEaseIn", "CubicEaseOut", "CubicEaseInOut", +"QuarticEaseIn", "QuarticEaseOut", "QuarticEaseInOut", "QuinticEaseIn", "QuinticEaseOut", +"QuinticEaseInOut", "SineEaseIn", "SineEaseOut", "SineEaseInOut", "CircularEaseIn", +"CircularEaseOut", "CircularEaseInOut", "ExponentialEaseIn", "ExponentialEaseOut", +"ExponentialEaseInOut", "ElasticEaseIn", "ElasticEaseOut", "ElasticEaseInOut", +"BounceEaseIn", "BounceEaseOut", "BounceEaseInOut" +\\param postscript This parameter specifies a Lua script that will be executed once the +change of property value is completed. If a duration larger than 0 was provided, it is +at the end of the interpolation. If 0 was provided, the script runs immediately. +)" }, { "getPropertyValue", &luascriptfunctions::propertyGetValueDeprecated, - {}, - "", + { + { "uri", "String" } + }, + "String | Number | Boolean | Table", "Returns the value the property, identified by the provided URI. " - "Deprecated in favor of the 'propertyValue' function", - {} + "Deprecated in favor of the 'propertyValue' function" }, { "propertyValue", &luascriptfunctions::propertyGetValue, - {}, - "", - "Returns the value the property, identified by the provided URI. " - "Deprecated in favor of the 'propertyValue' function", - {} + { + { "uri", "String" } + }, + "String | Number | Boolean | Table", + "Returns the value of the property identified by the provided URI. This " + "function will provide an error message if no property matching the URI " + "is found." }, codegen::lua::HasProperty, codegen::lua::PropertyDeprecated, diff --git a/src/scene/scene_lua.inl b/src/scene/scene_lua.inl index eba58e031e..e1ee01b5c1 100644 --- a/src/scene/scene_lua.inl +++ b/src/scene/scene_lua.inl @@ -63,138 +63,240 @@ namespace { -template -const openspace::properties::PropertyOwner* findPropertyOwnerWithMatchingGroupTag(T* prop, - const std::string& tagToMatch) +/** + * Returns the Property that matches the provided tag. First the provided owner is checked + * and if that does not contain the requested tag, its own owners are checked recursively. + */ +bool ownerMatchesGroupTag(const openspace::properties::PropertyOwner* owner, + std::string_view tagToMatch) { using namespace openspace; - const properties::PropertyOwner* tagMatchOwner = nullptr; - const properties::PropertyOwner* owner = prop->owner(); + constexpr char Intersection = '&'; + constexpr char Negation = '~'; + constexpr char Union = '|'; - if (owner) { - const std::vector& tags = owner->tags(); - for (const std::string& currTag : tags) { - if (tagToMatch == currTag) { - tagMatchOwner = owner; - break; - } + if (!owner) { + return false; + } + + const std::vector& tags = owner->tags(); + + if (size_t i = tagToMatch.find(Intersection); i != std::string_view::npos) { + // We have an intersection instruction + if (tagToMatch.find(Negation) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found an " + "intersection ('{}') and a negation instruction ('{}') in the query: " + "'{}'", + Intersection, Negation, tagToMatch + )); + } + if (tagToMatch.find(Union) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found an " + "intersection ('{}') and a union instruction ('{}') in the query: " + "'{}'", + Intersection, Union, tagToMatch + )); } - // Call recursively until we find an owner with matching tag or the top of the - // ownership list - if (tagMatchOwner == nullptr) { - tagMatchOwner = findPropertyOwnerWithMatchingGroupTag(owner, tagToMatch); + std::string_view t1 = tagToMatch.substr(0, i); + auto t1It = std::find(tags.begin(), tags.end(), t1); + std::string_view t2 = tagToMatch.substr(i + 1); + auto t2It = std::find(tags.begin(), tags.end(), t2); + if (t1It != tags.end() && t2It != tags.end()) { + return true; } } - return tagMatchOwner; + if (size_t i = tagToMatch.find(Negation); i != std::string_view::npos) { + // We have an negation instruction + if (tagToMatch.find(Intersection) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found a " + "negation ('{}') and an intersection instruction ('{}') in the query: " + "'{}'", + Negation, Intersection, tagToMatch + )); + } + if (tagToMatch.find(Union) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found a " + "negation ('{}') and a union instruction ('{}') in the query: " + "'{}'", + Negation, Union, tagToMatch + )); + } + + std::string_view t1 = tagToMatch.substr(0, i); + auto t1It = std::find(tags.begin(), tags.end(), t1); + std::string_view t2 = tagToMatch.substr(i + 1); + auto t2It = std::find(tags.begin(), tags.end(), t2); + if (t1It != tags.end() && t2It == tags.end()) { + return true; + } + } + if (size_t i = tagToMatch.find(Union); i != std::string_view::npos) { + // We have an union instruction + if (tagToMatch.find(Negation) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found a union " + "('{}') and a negation instruction ('{}') in the query: " + "'{}'", + Union, Negation, tagToMatch + )); + } + if (tagToMatch.find(Intersection) != std::string_view::npos) { + throw ghoul::RuntimeError(std::format( + "Only a single instruction to combine tags is supported. Found a union " + "('{}') and an intersection instruction ('{}') in the query: " + "'{}'", + Union, Intersection, tagToMatch + )); + } + + std::string_view t1 = tagToMatch.substr(0, i); + auto t1It = std::find(tags.begin(), tags.end(), t1); + std::string_view t2 = tagToMatch.substr(i + 1); + auto t2It = std::find(tags.begin(), tags.end(), t2); + if (t1It != tags.end() || t2It != tags.end()) { + return true; + } + } + + // We are dealing with a tag without any combinations + auto match = std::find(tags.begin(), tags.end(), tagToMatch); + if (match != tags.end()) { + return true; + } + + // If we got this far, we have an owner and we haven't found a match, so we have to + // try one level higher + ghoul_assert(owner, "Owner does not exist"); + return ownerMatchesGroupTag(owner->owner(), tagToMatch); +} + +// Checks to see if URI contains a group tag (with { } around the first term) +std::string groupTag(const std::string& command) { + const std::string name = command.substr(0, command.find_first_of(".")); + if (name.front() == '{' && name.back() == '}') { + return name.substr(1, name.length() - 2); + } + else { + return ""; + } +} + +std::string_view removeGroupTagFromUri(std::string_view uri) { + size_t pos = uri.find_first_of("."); + return pos == std::string::npos ? uri : uri.substr(pos); +} + +/** + * Parses the provided regex and splits it based on the location of the optional wildcard + * character (*). If a wildcard existed in the regex, the returned tuple will be the + * substring prior to the wildcard, the substring following to the wildcard, and `false` + * as the first value. If there was no wildcard, the first return value is the empty + * string, the second value is the full regular expression, and the third value is `true`, + * indicating that it was a literal value. + */ +std::tuple parseRegex(std::string_view regex) { + if (size_t wildPos = regex.find_first_of("*"); wildPos != std::string::npos) { + std::string_view preName = regex.substr(0, wildPos); + std::string_view postName = regex.substr(wildPos + 1); + + // If none then malformed regular expression + if (preName.empty() && postName.empty()) [[unlikely]] { + throw ghoul::lua::LuaError(std::format( + "Malformed regular expression: '{}': Empty both before and after '*'", + regex + )); + } + + // Currently do not support several wildcards + if (regex.find_first_of("*", wildPos + 1) != std::string::npos) [[unlikely]] { + throw ghoul::lua::LuaError(std::format( + "Malformed regular expression: '{}': Currently only one '*' is supported", + regex + )); + } + + return { preName, postName, false }; + } + else { + // Literal or tag + return { "", regex, true }; + } } std::vector findMatchesInAllProperties( std::string_view regex, - const std::vector& properties, - const std::string& groupName) + std::string_view groupTag) { using namespace openspace; + using namespace properties; - std::vector matches; - const bool isGroupMode = !groupName.empty(); - bool isLiteral = false; + auto [nodeName, propertyIdentifier, isLiteral] = parseRegex(regex); - // Extract the property and node name to be searched for from regex - std::string propertyName; - std::string nodeName; - size_t wildPos = regex.find_first_of("*"); - if (wildPos != std::string::npos) { - nodeName = regex.substr(0, wildPos); - propertyName = regex.substr(wildPos + 1, regex.length()); - - // If none then malformed regular expression - if (propertyName.empty() && nodeName.empty()) { - LERRORC( - "findMatchesInAllProperties", - std::format( - "Malformed regular expression: '{}': Empty both before and after '*'", - regex - ) - ); - return matches; - } - - // Currently do not support several wildcards - if (regex.find_first_of("*", wildPos + 1) != std::string::npos) { - LERRORC( - "findMatchesInAllProperties", - std::format( - "Malformed regular expression: '{}': Currently only one '*' is " - "supported", regex - ) - ); - return matches; - } - } - // Literal or tag - else { - propertyName = regex; - if (!isGroupMode) { - isLiteral = true; - } + const bool isGroupMode = !groupTag.empty(); + if (nodeName.empty() && isGroupMode) { + isLiteral = false; } + const std::vector& properties = allProperties(); + + std::vector matches; + std::mutex mutex; std::for_each( std::execution::par_unseq, properties.cbegin(), properties.cend(), - [&](properties::Property* prop) { - // Check the regular expression for all properties + [&](Property* prop) { const std::string_view uri = prop->uri(); - if (isLiteral && uri != propertyName) { + if (isLiteral && uri != propertyIdentifier) { return; } - else if (!propertyName.empty()) { - size_t propertyPos = uri.find(propertyName); + + if (!propertyIdentifier.empty()) { + const size_t propertyPos = uri.find(propertyIdentifier); if ( - // Check if the propertyName appears in the URI at all + // Check if the propertyIdentifier appears in the URI at all (propertyPos == std::string::npos) || - // Check that the propertyName fully matches the property in uri - ((propertyPos + propertyName.length() + 1) < uri.length()) || + // Check that the propertyIdentifier fully matches the property in URI + ((propertyPos + propertyIdentifier.length() + 1) < uri.length()) || // Match node name (!nodeName.empty() && uri.find(nodeName) == std::string::npos)) { return; } - // Check tag - if (isGroupMode) { - const properties::PropertyOwner* matchingTaggedOwner = - findPropertyOwnerWithMatchingGroupTag(prop, groupName); - if (!matchingTaggedOwner) { - return; - } - } - } - else if (!nodeName.empty()) { - size_t nodePos = uri.find(nodeName); - if (nodePos != std::string::npos) { - // Check tag - if (isGroupMode) { - const properties::PropertyOwner* matchingTaggedOwner = - findPropertyOwnerWithMatchingGroupTag(prop, groupName); - if (!matchingTaggedOwner) { - return; - } - } - // Check that the nodeName fully matches the node in id - else if (nodePos != 0) { - return; - } - } - else { + // At this point we know that the property name matches, so another way + // this property to fail is if we provided a tag and the owner doesn't + // match it + if (isGroupMode && !ownerMatchesGroupTag(prop->owner(), groupTag)) { return; } } + else if (!nodeName.empty()) { + const size_t nodePos = uri.find(nodeName); + if (nodePos == std::string::npos) { + return; + } + + // Check tag + if (isGroupMode) { + if (!ownerMatchesGroupTag(prop->owner(), groupTag)) { + return; + } + } + else if (nodePos != 0) { + // Check that the node identifier fully matches the node in URI + return; + } + } + std::lock_guard g(mutex); matches.push_back(prop); } @@ -203,60 +305,72 @@ std::vector findMatchesInAllProperties( return matches; } -void applyRegularExpression(lua_State* L, const std::string& regex, - const std::vector& properties, - double interpolationDuration, - const std::string& groupName, - ghoul::EasingFunction easingFunction, - std::string postScript) +void applyRegularExpression(lua_State* L, std::string_view regex, + double interpolationDuration, std::string_view groupTag, + ghoul::EasingFunction easingFunction, std::string postScript) { using namespace openspace; - using ghoul::lua::errorLocation; - using ghoul::lua::luaTypeToString; + using namespace properties; - const ghoul::lua::LuaTypes type = ghoul::lua::fromLuaType(lua_type(L, -1)); + // + // 1. Retrieve all properties that match the regex + std::vector matchingProps = findMatchesInAllProperties(regex, groupTag); - std::vector matchingProps = findMatchesInAllProperties( - regex, - properties, - groupName + // + // 2. Remove all properties that don't match the provided type + std::erase_if( + matchingProps, + [L, type = ghoul::lua::fromLuaType(lua_type(L, -1))](Property* prop) { + const bool typeMatches = typeMatch(type, prop->typeLua()); + if (!typeMatches) [[unlikely]] { + LERRORC( + "property_setValue", + std::format( + "{}: Property '{}' does not accept input of type '{}'. Requested " + "type: {}", + ghoul::lua::errorLocation(L), prop->uri(), + luaTypeToString(type), luaTypeToString(prop->typeLua()) + ) + ); + } + + return !typeMatches; + } ); - // Stores whether we found at least one matching property. If this is false at the - // end of the loop, the property name regex was probably misspelled. - bool foundMatching = false; - for (properties::Property* prop : matchingProps) { - // Check that the types match - if (!typeMatch(type, prop->typeLua())) { - LERRORC( - "property_setValue", - std::format( - "{}: Property '{}' does not accept input of type '{}'. Requested " - "type: {}", - errorLocation(L), prop->uri(), - luaTypeToString(type), luaTypeToString(prop->typeLua()) - ) - ); + if (matchingProps.empty()) [[unlikely]] { + LERRORC( + "property_setValue", + std::format( + "{}: No property matched the requested URI '{}'", + ghoul::lua::errorLocation(L), regex + ) + ); + return; + } + + for (Property* prop : matchingProps) { + // If the fully qualified id matches the regular expression, we queue the + // value change if the types agree + + // The setLuaInterpolationTarget and setLuaValue functions will remove the + // value from the stack, so we need to push it to the end + lua_pushvalue(L, -1); + + if (global::sessionRecordingHandler->isRecording()) { + global::sessionRecordingHandler->savePropertyBaseline(*prop); + } + + if (interpolationDuration == 0.0) { + if (Scene* scene = global::renderEngine->scene(); scene) { + scene->removePropertyInterpolation(prop); + } + prop->setLuaValue(L); } else { - // If the fully qualified id matches the regular expression, we queue the - // value change if the types agree - foundMatching = true; - - // The setLuaInterpolationTarget and setLuaValue functions will remove the - // value from the stack, so we need to push it to the end - lua_pushvalue(L, -1); - - if (global::sessionRecordingHandler->isRecording()) { - global::sessionRecordingHandler->savePropertyBaseline(*prop); - } - if (interpolationDuration == 0.0) { - global::renderEngine->scene()->removePropertyInterpolation(prop); - prop->setLuaValue(L); - } - else { - prop->setLuaInterpolationTarget(L); - global::renderEngine->scene()->addPropertyInterpolation( + prop->setLuaInterpolationTarget(L); + if (Scene* scene = global::renderEngine->scene(); scene) { + scene->addPropertyInterpolation( prop, static_cast(interpolationDuration), postScript, @@ -265,69 +379,37 @@ void applyRegularExpression(lua_State* L, const std::string& regex, } } } - - if (!foundMatching) { - LERRORC( - "property_setValue", - std::format( - "{}: No property matched the requested URI '{}'", errorLocation(L), regex - ) - ); - } } -// Checks to see if URI contains a group tag (with { } around the first term). If so, -// returns true and sets groupName with the tag -bool doesUriContainGroupTag(const std::string& command, std::string& groupName) { - const std::string name = command.substr(0, command.find_first_of(".")); - if (name.front() == '{' && name.back() == '}') { - groupName = name.substr(1, name.length() - 2); - return true; - } - else { - return false; - } -} - -std::string removeGroupNameFromUri(const std::string& uri) { - size_t pos = uri.find_first_of("."); - return pos == std::string::npos ? uri : uri.substr(pos); -} - -} // namespace - -namespace openspace::luascriptfunctions { - -int setPropertyCallSingle(properties::Property& prop, const std::string& uri, - lua_State* L, double duration, - ghoul::EasingFunction easingFunction, std::string postScript) +int setPropertyCallSingle(openspace::properties::Property& prop, const std::string& uri, + lua_State* L, double duration, + ghoul::EasingFunction easingFunction, std::string postScript) { + using namespace openspace; using ghoul::lua::errorLocation; using ghoul::lua::luaTypeToString; const ghoul::lua::LuaTypes type = ghoul::lua::fromLuaType(lua_type(L, -1)); if (!typeMatch(type, prop.typeLua())) { - LERRORC( - "property_setValue", - std::format( - "{}: Property '{}' does not accept input of type '{}'. " - "Requested type: {}", - errorLocation(L), uri, luaTypeToString(type), - luaTypeToString(prop.typeLua()) - ) - ); + throw ghoul::lua::LuaError(std::format( + "{}: Property '{}' does not accept input of type '{}'. Requested type: {}", + errorLocation(L), uri, luaTypeToString(type), luaTypeToString(prop.typeLua()) + )); + } + + if (global::sessionRecordingHandler->isRecording()) { + global::sessionRecordingHandler->savePropertyBaseline(prop); + } + if (duration == 0.0) { + if (Scene* scene = global::renderEngine->scene(); scene) { + scene->removePropertyInterpolation(&prop); + } + prop.setLuaValue(L); } else { - if (global::sessionRecordingHandler->isRecording()) { - global::sessionRecordingHandler->savePropertyBaseline(prop); - } - if (duration == 0.0) { - global::renderEngine->scene()->removePropertyInterpolation(&prop); - prop.setLuaValue(L); - } - else { - prop.setLuaInterpolationTarget(L); - global::renderEngine->scene()->addPropertyInterpolation( + prop.setLuaInterpolationTarget(L); + if (Scene* scene = global::renderEngine->scene(); scene) { + scene->addPropertyInterpolation( &prop, static_cast(duration), std::move(postScript), @@ -338,6 +420,50 @@ int setPropertyCallSingle(properties::Property& prop, const std::string& uri, return 0; } +template +void createCustomProperty(openspace::properties::Property::PropertyInfo info, + std::optional onChange) +{ + T* p = new T(info); + if (onChange.has_value() && !onChange->empty()) { + p->onChange( + [p, script = *onChange]() { + using namespace ghoul::lua; + LuaState s; + openspace::global::scriptEngine->initializeLuaState(s); + ghoul::lua::push(s, p->value()); + lua_setglobal(s, "value"); + ghoul::lua::runScript(s, script); + } + ); + } + openspace::global::userPropertyOwner->addProperty(p); +} + +template <> +void createCustomProperty( + openspace::properties::Property::PropertyInfo info, + std::optional onChange) +{ + using namespace openspace::properties; + TriggerProperty* p = new TriggerProperty(info); + if (onChange.has_value() && !onChange->empty()) { + p->onChange( + [script = *onChange]() { + using namespace ghoul::lua; + LuaState s; + openspace::global::scriptEngine->initializeLuaState(s); + ghoul::lua::runScript(s, script); + } + ); + } + openspace::global::userPropertyOwner->addProperty(p); +} + +} // namespace + +namespace openspace::luascriptfunctions { + template int propertySetValue(lua_State* L) { ZoneScoped; @@ -393,8 +519,7 @@ int propertySetValue(lua_State* L) { } if (nParameters == 5) { if (ghoul::lua::hasValue(L, 5)) { - postScript = - ghoul::lua::value(L, 5, ghoul::lua::PopValue::No); + postScript = ghoul::lua::value(L, 5, ghoul::lua::PopValue::No); } else { std::string msg = std::format( @@ -408,17 +533,15 @@ int propertySetValue(lua_State* L) { if (!easingMethodName.empty()) { bool correctName = ghoul::isValidEasingFunctionName(easingMethodName); if (!correctName) { - LWARNINGC( - "propertySetValue", - std::format("'{}' is not a valid easing method", easingMethodName) - ); - } - else { - easingMethod = ghoul::easingFunctionFromName(easingMethodName); + throw ghoul::lua::LuaError(std::format( + "'{}' is not a valid easing method", easingMethodName + )); } + + easingMethod = ghoul::easingFunctionFromName(easingMethodName); } - if (optimization) { + if constexpr (optimization) { properties::Property* prop = property(uriOrRegex); if (!prop) { LERRORC( @@ -430,6 +553,7 @@ int propertySetValue(lua_State* L) { ); return 0; } + return setPropertyCallSingle( *prop, uriOrRegex, @@ -440,18 +564,17 @@ int propertySetValue(lua_State* L) { ); } else { - std::string groupName; - if (doesUriContainGroupTag(uriOrRegex, groupName)) { - // Remove group name from start of regex and replace with '*' - uriOrRegex = removeGroupNameFromUri(uriOrRegex); + std::string tag = groupTag(uriOrRegex); + if (!tag.empty()) { + // Remove group tag from start of regex and replace with '*' + uriOrRegex = removeGroupTagFromUri(uriOrRegex); } applyRegularExpression( L, uriOrRegex, - allProperties(), interpolationDuration, - groupName, + tag, easingMethod, std::move(postScript) ); @@ -474,9 +597,8 @@ int propertyGetValue(lua_State* L) { ); return 0; } - else { - prop->getLuaValue(L); - } + + prop->getLuaValue(L); return 1; } @@ -495,7 +617,19 @@ int propertyGetValueDeprecated(lua_State* L) { namespace { /** - * Returns whether a property with the given URI exists + * Returns whether a property with the given URI exists. The `uri` identifies the property + * or properties that are checked by this function and can include both wildcards `*` + * which match anything, as well as tags (`{tag}`) which match scene graph nodes that have + * this tag. There is also the ability to combine two tags through the `&`, `|`, and `~` + * operators. `{tag1&tag2}` will match anything that has the tag1 and the tag2. + * `{tag1|tag2}` will match anything that has the tag1 or the tag 2, and `{tag1~tag2}` + * will match anything that has tag1 but not tag2. If no wildcards or tags are provided at + * most one property value will be changed. With wildcards or tags all properties that + * match the URI are changed instead. + * + * \param uri The URI that identifies the property or properties whose values should be + * changed. The URI can contain 0 or 1 wildcard `*` characters or a tag + * expression (`{tag}`) that identifies a property owner. */ [[codegen::luawrap]] bool hasProperty(std::string uri) { openspace::properties::Property* prop = openspace::property(uri); @@ -503,111 +637,37 @@ namespace { } /** - * Returns a list of property identifiers that match the passed regular expression + * Returns a list of property identifiers that match the passed regular expression. The + * `uri` identifies the property or properties that are returned by this function and can + * include both wildcards `*` which match anything, as well as tags (`{tag}`) which match + * scene graph nodes that have this tag. There is also the ability to combine two tags + * through the `&`, `|`, and `~` operators. `{tag1&tag2}` will match anything that has the + * tag1 and the tag2. `{tag1|tag2}` will match anything that has the tag1 or the tag 2, + * and `{tag1~tag2}` will match anything that has tag1 but not tag2. If no wildcards or + * tags are provided at most one property value will be changed. With wildcards or tags + * all properties that match the URI are changed instead. + * + * \param uri The URI that identifies the property or properties whose values should be + * changed. The URI can contain 0 or 1 wildcard `*` characters or a tag + * expression (`{tag}`) that identifies a property owner. */ -[[codegen::luawrap]] std::vector property(std::string regex) { +[[codegen::luawrap]] std::vector property(std::string uri) { using namespace openspace; - std::string groupName; - if (doesUriContainGroupTag(regex, groupName)) { + std::string tag = groupTag(uri); + if (!tag.empty()) { // Remove group name from start of regex and replace with '*' - regex = removeGroupNameFromUri(regex); + uri = removeGroupTagFromUri(uri); } - // Extract the property and node name to be searched for from regex - bool isLiteral = false; - std::string propertyName; - std::string nodeName; - size_t wildPos = regex.find_first_of("*"); - if (wildPos != std::string::npos) { - nodeName = regex.substr(0, wildPos); - propertyName = regex.substr(wildPos + 1, regex.length()); + std::vector props = findMatchesInAllProperties(uri, tag); - // If none then malformed regular expression - if (propertyName.empty() && nodeName.empty()) { - throw ghoul::lua::LuaError(std::format( - "Malformed regular expression: '{}': Empty both before and after '*'", - regex - )); - } - - // Currently do not support several wildcards - if (regex.find_first_of("*", wildPos + 1) != std::string::npos) { - throw ghoul::lua::LuaError(std::format( - "Malformed regular expression: '{}': Currently only one '*' is supported", - regex - )); - } - } - // Literal or tag - else { - propertyName = regex; - if (groupName.empty()) { - isLiteral = true; - } - } - - // Get all matching property uris and save to res - std::vector props = allProperties(); - std::vector res; + std::vector matches; + matches.reserve(props.size()); for (properties::Property* prop : props) { - // Check the regular expression for all properties - const std::string_view uri = prop->uri(); - - if (isLiteral && uri != propertyName) { - continue; - } - else if (!propertyName.empty()) { - size_t propertyPos = uri.find(propertyName); - if (propertyPos != std::string::npos) { - // Check that the propertyName fully matches the property in id - if ((propertyPos + propertyName.length() + 1) < uri.length()) { - continue; - } - - // Match node name - if (!nodeName.empty() && uri.find(nodeName) == std::string::npos) { - continue; - } - - // Check tag - if (!groupName.empty()) { - const properties::PropertyOwner* matchingTaggedOwner = - findPropertyOwnerWithMatchingGroupTag(prop, groupName); - if (!matchingTaggedOwner) { - continue; - } - } - } - else { - continue; - } - } - else if (!nodeName.empty()) { - size_t nodePos = uri.find(nodeName); - if (nodePos != std::string::npos) { - // Check tag - if (!groupName.empty()) { - const properties::PropertyOwner* matchingTaggedOwner = - findPropertyOwnerWithMatchingGroupTag(prop, groupName); - if (!matchingTaggedOwner) { - continue; - } - } - // Check that the nodeName fully matches the node in id - else if (nodePos != 0) { - continue; - } - } - else { - continue; - } - } - - res.push_back(std::string(uri)); + matches.emplace_back(prop->uri()); } - - return res; + return matches; } /** @@ -646,14 +706,14 @@ namespace { "Scene"; logError(e, cat); - throw ghoul::lua::LuaError( - std::format("Error loading scene graph node: {}", e.what()) - ); + throw ghoul::lua::LuaError(std::format( + "Error loading scene graph node: {}", e.what() + )); } catch (const ghoul::RuntimeError& e) { - throw ghoul::lua::LuaError( - std::format("Error loading scene graph node: {}", e.what()) - ); + throw ghoul::lua::LuaError(std::format( + "Error loading scene graph node: {}", e.what() + )); } } @@ -662,7 +722,7 @@ namespace { * the parameter is a table. */ [[codegen::luawrap]] void removeSceneGraphNode( - std::variant node) + std::variant node) { using namespace openspace; std::string identifier; @@ -731,99 +791,59 @@ namespace { /** * Removes all SceneGraphNodes with identifiers matching the input regular expression. */ -[[codegen::luawrap]] void removeSceneGraphNodesFromRegex(std::string name) { +[[codegen::luawrap]] void removeSceneGraphNodesFromRegex(std::string regex) { using namespace openspace; const std::vector& nodes = global::renderEngine->scene()->allSceneGraphNodes(); - // Extract the property and node name to be searched for from name - bool isLiteral = false; - std::string propertyName; - std::string nodeName; - size_t wildPos = name.find_first_of("*"); - if (wildPos != std::string::npos) { - nodeName = name.substr(0, wildPos); - propertyName = name.substr(wildPos + 1, name.length()); + auto [nodeIdentifier, propertyIdentifier, isLiteral] = parseRegex(regex); - // If none then malformed regular expression - if (propertyName.empty() && nodeName.empty()) { - throw ghoul::lua::LuaError( - std::format( - "Malformed regular expression: '{}': Empty both before and after '*'", - name - ) - ); - } - - // Currently do not support several wildcards - if (name.find_first_of("*", wildPos + 1) != std::string::npos) { - throw ghoul::lua::LuaError( - std::format( - "Malformed regular expression: '{}': " - "Currently only one '*' is supported", - name - ) - ); - } - } - // Literal or tag - else { - propertyName = name; - isLiteral = true; - } - - bool foundMatch = false; std::vector markedList; for (SceneGraphNode* node : nodes) { const std::string& identifier = node->identifier(); - if (isLiteral && identifier != propertyName) { + if (isLiteral && identifier != propertyIdentifier) { continue; } - else if (!propertyName.empty()) { - size_t propertyPos = identifier.find(propertyName); - if (propertyPos != std::string::npos) { - // Check that the propertyName fully matches the property in id - if ((propertyPos + propertyName.length() + 1) < identifier.length()) { - continue; - } + if (!propertyIdentifier.empty()) { + const size_t propertyPos = identifier.find(propertyIdentifier); + if ( + // Check if the propertyIdentifier appears in the URI at all + (propertyPos == std::string::npos) || + // Check that the propertyIdentifier fully matches the property in uri + ((propertyPos + propertyIdentifier.length() + 1) < identifier.length()) || // Match node name - if (!nodeName.empty() && identifier.find(nodeName) == std::string::npos) { - continue; - } - } - else { + (!nodeIdentifier.empty() && + identifier.find(nodeIdentifier) == std::string::npos)) + { continue; } } - else if (!nodeName.empty()) { - size_t nodePos = identifier.find(nodeName); - if (nodePos != std::string::npos) { - // Check that the nodeName fully matches the node in id - if (nodePos != 0) { - continue; - } + else if (!nodeIdentifier.empty()) { + size_t nodePos = identifier.find(nodeIdentifier); + if (nodePos == std::string::npos) { + continue; } - else { + + // Check that the nodeName fully matches the node in id + if (nodePos != 0) { continue; } } - foundMatch = true; SceneGraphNode* parent = node->parent(); if (!parent) { throw ghoul::lua::LuaError("Cannot remove root node"); } - else { - markedList.push_back(node); - } + + markedList.push_back(node); } - if (!foundMatch) { - throw ghoul::lua::LuaError( - std::format("Did not find a match for identifier: {}", name) - ); + if (markedList.empty()) { + throw ghoul::lua::LuaError(std::format( + "Did not find a match for identifier: {}", nodeIdentifier + )); } // Add all the children @@ -940,9 +960,9 @@ namespace { using namespace openspace; SceneGraphNode* node = sceneGraphNode(identifier); if (!node) { - throw ghoul::lua::LuaError( - std::format("Did not find a match for identifier: {} ", identifier) - ); + throw ghoul::lua::LuaError(std::format( + "Did not find a match for identifier: {} ", identifier + )); } glm::dvec3 pos = node->worldPosition(); @@ -957,9 +977,9 @@ namespace { using namespace openspace; SceneGraphNode* node = sceneGraphNode(identifier); if (!node) { - throw ghoul::lua::LuaError( - std::format("Did not find a match for identifier: {} ", identifier) - ); + throw ghoul::lua::LuaError(std::format( + "Did not find a match for identifier: {} ", identifier + )); } glm::dmat3 rot = node->worldRotationMatrix(); @@ -1023,46 +1043,6 @@ namespace { return is; } -template -void createCustomProperty(openspace::properties::Property::PropertyInfo info, - std::optional onChange) -{ - T* p = new T(info); - if (onChange.has_value() && !onChange->empty()) { - p->onChange( - [p, script = *onChange]() { - using namespace ghoul::lua; - LuaState s; - openspace::global::scriptEngine->initializeLuaState(s); - ghoul::lua::push(s, p->value()); - lua_setglobal(s, "value"); - ghoul::lua::runScript(s, script); - } - ); - } - openspace::global::userPropertyOwner->addProperty(p); -} - -template <> -void createCustomProperty( - openspace::properties::Property::PropertyInfo info, - std::optional onChange) -{ - using namespace openspace::properties; - TriggerProperty* p = new TriggerProperty(info); - if (onChange.has_value() && !onChange->empty()) { - p->onChange( - [script = *onChange]() { - using namespace ghoul::lua; - LuaState s; - openspace::global::scriptEngine->initializeLuaState(s); - ghoul::lua::runScript(s, script); - } - ); - } - openspace::global::userPropertyOwner->addProperty(p); -} - enum class [[codegen::enum]] CustomPropertyType { BoolProperty, DoubleProperty, @@ -1247,15 +1227,14 @@ enum class [[codegen::enum]] CustomPropertyType { [[codegen::luawrap]] void removeCustomProperty(std::string identifier) { using namespace openspace; properties::Property* p = global::userPropertyOwner->property(identifier); - if (p) { - global::userPropertyOwner->removeProperty(p); - delete p; - } - else { + if (!p) { throw ghoul::lua::LuaError(std::format( "Could not find user-defined property '{}'", identifier )); } + + global::userPropertyOwner->removeProperty(p); + delete p; } /** diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7bcbeac0e7..4346f077e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,9 @@ add_executable( test_latlonpatch.cpp test_lrucache.cpp test_lua_createsinglecolorimage.cpp + test_lua_property.cpp + test_lua_propertyvalue.cpp + test_lua_setpropertyvalue.cpp test_profile.cpp test_rawvolumeio.cpp test_scriptscheduler.cpp diff --git a/tests/test_lua_property.cpp b/tests/test_lua_property.cpp new file mode 100644 index 0000000000..e0ca679452 --- /dev/null +++ b/tests/test_lua_property.cpp @@ -0,0 +1,296 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace openspace; +using namespace properties; + +namespace { + // Offloading this into a separate function as it would otherwise be a lot of + // non-intuitive copy-and-paste that we might want to change later anyway + void triggerScriptRun() { + global::scriptEngine->preSync(true); + global::scriptEngine->postSync(true); + } +} // namespace + +TEST_CASE("Property: Basic", "[property]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('base.p1')", + .callback = [&p1](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + REQUIRE(e.size() == 1); + REQUIRE(e.hasKey("1")); + REQUIRE(e.hasValue("1")); + const std::string v = e.value("1"); + CHECK(v == p1.uri()); + } + }); + + triggerScriptRun(); + } +} + +TEST_CASE("Property: Empty", "[property]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('other-name')", + .callback = [](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + CHECK(e.size() == 0); + } + }); + + triggerScriptRun(); + } + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('base.other-name')", + .callback = [](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + CHECK(e.size() == 0); + } + }); + + triggerScriptRun(); + } +} + +TEST_CASE("Property: Multiple", "[property]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner.addProperty(p2); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('base.*')", + .callback = [&p1, &p2](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + REQUIRE(e.size() == 2); + + // Sorting the results as the property return order is undefined + std::array r; + { + REQUIRE(e.hasKey("1")); + REQUIRE(e.hasValue("1")); + const std::string v = e.value("1"); + r[0] = v; + } + { + REQUIRE(e.hasKey("2")); + REQUIRE(e.hasValue("2")); + const std::string v = e.value("2"); + r[1] = v; + } + + std::sort(r.begin(), r.end()); + CHECK(r[0] == p1.uri()); + CHECK(r[1] == p2.uri()); + } + }); + + triggerScriptRun(); + } +} + +TEST_CASE("Property: Multiple/2", "[property]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + FloatProperty p22 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner2.addProperty(p22); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('base1.*')", + .callback = [&p1, &p2](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + REQUIRE(e.size() == 2); + + // Sorting the results as the property return order is undefined + std::array r; + { + REQUIRE(e.hasKey("1")); + REQUIRE(e.hasValue("1")); + const std::string v = e.value("1"); + r[0] = v; + } + { + REQUIRE(e.hasKey("2")); + REQUIRE(e.hasValue("2")); + const std::string v = e.value("2"); + r[1] = v; + } + std::sort(r.begin(), r.end()); + CHECK(r[0] == p1.uri()); + CHECK(r[1] == p2.uri()); + } + }); + + triggerScriptRun(); + } + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('*.p1')", + .callback = [&p1, &p21](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + REQUIRE(e.size() == 2); + + // Sorting the results as the property return order is undefined + std::array r; + { + REQUIRE(e.hasKey("1")); + REQUIRE(e.hasValue("1")); + const std::string v = e.value("1"); + r[0] = v; + } + { + REQUIRE(e.hasKey("2")); + REQUIRE(e.hasValue("2")); + const std::string v = e.value("2"); + r[1] = v; + } + std::sort(r.begin(), r.end()); + CHECK(r[0] == p1.uri()); + CHECK(r[1] == p21.uri()); + } + }); + + triggerScriptRun(); + } + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.property('base*.p1')", + .callback = [&p1, &p21](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + ghoul::Dictionary e = d.value("1"); + REQUIRE(e.size() == 2); + + // Sorting the results as the property return order is undefined + std::array r; + { + REQUIRE(e.hasKey("1")); + REQUIRE(e.hasValue("1")); + const std::string v = e.value("1"); + r[0] = v; + } + { + REQUIRE(e.hasKey("2")); + REQUIRE(e.hasValue("2")); + const std::string v = e.value("2"); + r[1] = v; + } + std::sort(r.begin(), r.end()); + CHECK(r[0] == p1.uri()); + CHECK(r[1] == p21.uri()); + } + }); + + triggerScriptRun(); + } +} diff --git a/tests/test_lua_propertyvalue.cpp b/tests/test_lua_propertyvalue.cpp new file mode 100644 index 0000000000..e674862305 --- /dev/null +++ b/tests/test_lua_propertyvalue.cpp @@ -0,0 +1,111 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace openspace; +using namespace properties; + +namespace { + // Offloading this into a separate function as it would otherwise be a lot of + // non-intuitive copy-and-paste that we might want to change later anyway + void triggerScriptRun() { + global::scriptEngine->preSync(true); + global::scriptEngine->postSync(true); + } +} // namespace + +TEST_CASE("PropertyValue: Basic", "[propertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.propertyValue('base.p1')", + .callback = [&p1](ghoul::Dictionary d) { + REQUIRE(d.size() == 1); + REQUIRE(d.hasKey("1")); + REQUIRE(d.hasValue("1")); + const float v = static_cast(d.value("1")); + CHECK(v == p1); + } + }); + + triggerScriptRun(); + } +} + +TEST_CASE("PropertyValue: Empty", "[propertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.propertyValue('other-name')", + .callback = [](ghoul::Dictionary d) { + CHECK(d.size() == 0); + } + }); + + triggerScriptRun(); + } + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript({ + .code = "return openspace.propertyValue('base.other-name')", + .callback = [](ghoul::Dictionary d) { + CHECK(d.size() == 0); + } + }); + + triggerScriptRun(); + } +} diff --git a/tests/test_lua_setpropertyvalue.cpp b/tests/test_lua_setpropertyvalue.cpp new file mode 100644 index 0000000000..65de212141 --- /dev/null +++ b/tests/test_lua_setpropertyvalue.cpp @@ -0,0 +1,2491 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace openspace; +using namespace properties; + +namespace { + // Offloading this into a separate function as it would otherwise be a lot of + // non-intuitive copy-and-paste that we might want to change later anyway + void triggerScriptRun() { + global::scriptEngine->preSync(true); + global::scriptEngine->postSync(true); + } + + // Updates any ongoing interpolations with an optional time delay + void updateInterpolations(std::optional ms = std::nullopt) + { + if (ms.has_value()) { + std::this_thread::sleep_for(*ms); + } + global::renderEngine->scene()->updateInterpolations(); + } +} // namespace + +TEST_CASE("SetPropertyValueSingle: Basic", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('base.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + } +} + +TEST_CASE("SetPropertyValueSingle: Wrong Type", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('base.p1', 'abc')" + ); + triggerScriptRun(); + int errorCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Error); + CHECK(errorCounter == 1); + } +} + +TEST_CASE("SetPropertyValueSingle: Non-existing", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('base.p2', 1.0)" + ); + triggerScriptRun(); + int errorCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Error); + CHECK(errorCounter == 1); + } +} + +TEST_CASE("SetPropertyValueSingle: Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('base.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + } +} + +TEST_CASE("SetPropertyValueSingle: Easing", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('base.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + CHECK(p1 > 1.f); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + } +} + +TEST_CASE("SetPropertyValueSingle: PostScript", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner.addProperty(p2); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValueSingle( + 'base.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValueSingle('base.p2', 0.75)]] + ) + )"); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 0.75f); + } +} + +TEST_CASE("SetPropertyValue: Basic", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Wrong Type", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base.p1', 'abc')" + ); + triggerScriptRun(); + int errorCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Error); + CHECK(errorCounter == 2); + } +} + +TEST_CASE("SetPropertyValue: Non-existing", "[setpropertyvalue]") { + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base.p2', 1.0)" + ); + triggerScriptRun(); + int errorCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Error); + CHECK(errorCounter == 1); + } +} + +TEST_CASE("SetPropertyValue: Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + } +} + +TEST_CASE("SetPropertyValue: Easing", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + CHECK(p1 > 1.f); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + } +} + +TEST_CASE("SetPropertyValue: PostScript", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner = PropertyOwner({ "base" }); + global::rootPropertyOwner->addPropertySubOwner(owner); + defer { global::rootPropertyOwner->removePropertySubOwner(owner); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner.addProperty(p2); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + 'base.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base.p2', 0.75)]] + ) + )"); + defer { scene->removePropertyInterpolation(&p1); }; + + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 0.75f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Basic", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('base1.*', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p21 == 1.f); + } + + p1 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('*.p1', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p21 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Basic Multiple", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('base1.*', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 2.f); + CHECK(p21 == 1.f); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('*.p1', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(p21 == 2.f); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('base*.p1', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(p21 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Basic Multiple/2", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.p*', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 2.f); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.*', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Interpolation Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.*', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 > 1.f); + CHECK_THAT(p2, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 == 1.f); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('*.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base*.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Interpolation Multiple/2", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.p*', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 > 1.f); + CHECK_THAT(p2, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Easing Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.*', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 > 1.f); + CHECK_THAT(p2, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p21 == 1.f); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('*.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.075)); + } + + p1 = 1.f; + p2 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base*.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.075)); + } +} + +TEST_CASE("SetPropertyValue: Wildcard Easing Multiple/2", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('base1.p*', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 > 1.f); + CHECK_THAT(p2, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard PostScript Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + 'base1.*', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 2.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 1.f); + } + + p1 = 1.f; + p2 = 1.f; + q1 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + '*.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 2.f); + } + + p1 = 1.f; + p2 = 1.f; + q1 = 1.f; + p21 = 1.f; + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + 'base*.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Wildcard PostScript Multiple/2", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + 'base1.p*', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p2); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 2.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Basic", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('{tag1}.p1', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Basic Multiple", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript("openspace.setPropertyValue('{tag1}.p1', 2.0)"); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1}.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Interpolation Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1}.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.05)); + } +} + +TEST_CASE("SetPropertyValue: Tags Easing Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1}.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.075)); + } +} + +TEST_CASE("SetPropertyValue: Tags PostScript Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + '{tag1}.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 1.f); + CHECK(p31 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Intersection Basic", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1&tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + CHECK(p1 == 1.f); + CHECK(p21 == 2.f); + CHECK(p31 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Intersection Basic Multiple", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1&tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 2.f); + CHECK(p31 == 2.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Intersection Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1&tag2}.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p31 == 1.f); + } +} + +TEST_CASE( + "SetPropertyValue: Tags Intersection Interpolation Multiple", + "[setpropertyvalue]" +) +{ + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1&tag2}.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Intersection Easing Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1&tag2}.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Intersection PostScript Multiple", "[setpropertyvalue]") +{ + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag1"); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + '{tag1&tag2}.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 2.f); + CHECK(p31 == 2.f); + CHECK(p41 == 2.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union Basic", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1|tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p21 == 2.f); + CHECK(p31 == 2.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union Basic Multiple", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + PropertyOwner owner5 = PropertyOwner({ "base5" }); + owner5.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner5); + defer { global::rootPropertyOwner->removePropertySubOwner(owner5); }; + FloatProperty p51 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner5.addProperty(p51); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1|tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + CHECK(p51 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(p21 == 2.f); + CHECK(p31 == 2.f); + CHECK(p41 == 2.f); + CHECK(p51 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1|tag2}.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union Interpolation Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + PropertyOwner owner5 = PropertyOwner({ "base5" }); + owner5.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner5); + defer { global::rootPropertyOwner->removePropertySubOwner(owner5); }; + FloatProperty p51 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner5.addProperty(p51); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1|tag2}.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + CHECK(p51 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p41 > 1.f); + CHECK_THAT(p41, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p51 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union Easing Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + PropertyOwner owner5 = PropertyOwner({ "base5" }); + owner5.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner5); + defer { global::rootPropertyOwner->removePropertySubOwner(owner5); }; + FloatProperty p51 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner5.addProperty(p51); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1|tag2}.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + CHECK(p51 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p31 > 1.f); + CHECK_THAT(p31, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p41 > 1.f); + CHECK_THAT(p41, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p51 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Union PostScript Multiple", "[setpropertyvalue]") +{ + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag3"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + '{tag1|tag2}.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 2.f); + CHECK(p31 == 2.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation Basic", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1~tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation Basic Multiple", "[setpropertyvalue]") { + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1~tag2}.p1', 2.0)" + ); + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(p21 == 2.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation Interpolation", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + owner2.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1~tag2}.p1', 2.0, 1.0)" + ); + defer { scene->removePropertyInterpolation(&p1); }; + + CHECK(p1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation Interpolation Multiple", "[setpropertyvalue]") +{ + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1~tag2}.p1', 2.0, 1.0)" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double v = glm::mix(1.0, 2.0, 0.1); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.05)); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation Easing Multiple", "[setpropertyvalue]") { + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + PropertyOwner owner4 = PropertyOwner({ "base4" }); + owner4.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner4); + defer { global::rootPropertyOwner->removePropertySubOwner(owner4); }; + FloatProperty p41 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner4.addProperty(p41); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript( + "openspace.setPropertyValue('{tag1~tag2}.p1', 2.0, 1.0, 'ExponentialEaseOut')" + ); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(100)); + const double t = ghoul::exponentialEaseOut(0.1); + const double v = glm::mix(1.0, 2.0, t); + CHECK(p1 > 1.f); + CHECK_THAT(p1, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p2 == 1.f); + CHECK(p21 > 1.f); + CHECK_THAT(p21, Catch::Matchers::WithinAbs(v, 0.075)); + CHECK(p31 == 1.f); + CHECK(p41 == 1.f); + } +} + +TEST_CASE("SetPropertyValue: Tags Negation PostScript Multiple", "[setpropertyvalue]") +{ + std::unique_ptr scene = std::make_unique( + std::make_unique() + ); + global::renderEngine->setScene(scene.get()); + defer { global::renderEngine->setScene(nullptr); }; + + PropertyOwner owner1 = PropertyOwner({ "base1" }); + owner1.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner1); + defer { global::rootPropertyOwner->removePropertySubOwner(owner1); }; + FloatProperty p1 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner1.addProperty(p1); + FloatProperty p2 = FloatProperty(Property::PropertyInfo("p2", "a", "b"), 1.f); + owner1.addProperty(p2); + FloatProperty q1 = FloatProperty(Property::PropertyInfo("q1", "a", "b"), 1.f); + owner1.addProperty(q1); + + PropertyOwner owner2 = PropertyOwner({ "base2" }); + owner2.addTag("tag1"); + global::rootPropertyOwner->addPropertySubOwner(owner2); + defer { global::rootPropertyOwner->removePropertySubOwner(owner2); }; + FloatProperty p21 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner2.addProperty(p21); + + PropertyOwner owner3 = PropertyOwner({ "base3" }); + owner3.addTag("tag1"); + owner3.addTag("tag2"); + global::rootPropertyOwner->addPropertySubOwner(owner3); + defer { global::rootPropertyOwner->removePropertySubOwner(owner3); }; + FloatProperty p31 = FloatProperty(Property::PropertyInfo("p1", "a", "b"), 1.f); + owner3.addProperty(p31); + + { + LogMgr.resetMessageCounters(); + defer { LogMgr.resetMessageCounters(); }; + + global::scriptEngine->queueScript(R"( + openspace.setPropertyValue( + '{tag1~tag2}.p1', + 2.0, + 0.1, + 'ExponentialEaseOut', + [[openspace.setPropertyValue('base1.q1', 0.75)]] + ) + )"); + defer { + scene->removePropertyInterpolation(&p1); + scene->removePropertyInterpolation(&p31); + }; + + CHECK(p1 == 1.f); + CHECK(p2 == 1.f); + CHECK(q1 == 1.f); + CHECK(p21 == 1.f); + CHECK(p31 == 1.f); + triggerScriptRun(); + updateInterpolations(std::chrono::milliseconds(150)); + triggerScriptRun(); + CHECK(p1 == 2.f); + CHECK(p2 == 1.f); + CHECK(q1 == 0.75f); + CHECK(p21 == 2.f); + CHECK(p31 == 1.f); + } +}