diff --git a/source/MaterialXCore/Element.cpp b/source/MaterialXCore/Element.cpp index 94836245a7..cc0b7da2ff 100644 --- a/source/MaterialXCore/Element.cpp +++ b/source/MaterialXCore/Element.cpp @@ -42,6 +42,14 @@ const string ValueElement::UNIFORM_ATTRIBUTE = "uniform"; Element::CreatorMap Element::_creatorMap; +const string ElementEquivalenceResult::ATTRIBUTE = "attribute"; +const string ElementEquivalenceResult::ATTRIBUTE_NAMES = "attribute names"; +const string ElementEquivalenceResult::CHILD_COUNT = "child count"; +const string ElementEquivalenceResult::CHILD_NAME = "child name"; +const string ElementEquivalenceResult::NAME = "name"; +const string ElementEquivalenceResult::CATEGORY = "category"; + + // // Element methods // @@ -81,6 +89,108 @@ bool Element::operator!=(const Element& rhs) const return !(*this == rhs); } +bool Element::isEquivalent(ConstElementPtr rhs, ElementEquivalenceOptions& options, + ElementEquivalenceResult* result) const +{ + if (getName() != rhs->getName()) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::NAME); + return false; + } + if (getCategory() != rhs->getCategory()) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::CATEGORY); + return false; + } + + // Compare attribute names. + StringVec attributeNames = getAttributeNames(); + StringVec rhsAttributeNames = rhs->getAttributeNames(); + + // Filter out any attributes specified in the options. + const StringSet& skipAttributes = options.skipAttributes; + if (!skipAttributes.empty()) + { + attributeNames.erase(std::remove_if(attributeNames.begin(), attributeNames.end(), + [&skipAttributes](const string& attr) { return skipAttributes.find(attr) != skipAttributes.end(); }), + attributeNames.end()); + rhsAttributeNames.erase(std::remove_if(rhsAttributeNames.begin(), rhsAttributeNames.end(), + [&skipAttributes](const string& attr) { return skipAttributes.find(attr) != skipAttributes.end(); }), + rhsAttributeNames.end()); + } + + // Ignore attribute ordering by sorting names + std::sort(attributeNames.begin(), attributeNames.end()); + std::sort(rhsAttributeNames.begin(), rhsAttributeNames.end()); + + if (attributeNames != rhsAttributeNames) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE_NAMES); + return false; + } + + for (const string& attr : rhsAttributeNames) + { + if (!isAttributeEquivalent(rhs, attr, options, result)) + { + return false; + } + } + + // Compare children. + const vector& c1 = getChildren(); + const vector& c2 = rhs->getChildren(); + if (c1.size() != c2.size()) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::CHILD_COUNT); + return false; + } + for (size_t i = 0; i < c1.size(); i++) + { + ElementPtr c2Element = c2[i]; + // Handle unordered children if parent is a compound graph (NodeGraph, Document). + // (Functional graphs have a "nodedef" reference and define node interfaces + // so require strict interface ordering.) + GraphElementPtr graph = this->getSelfNonConst()->asA(); + if (graph) + { + NodeGraphPtr nodeGraph = graph->asA(); + DocumentPtr document = graph->asA(); + if (document || (nodeGraph && !nodeGraph->getNodeDef())) + { + const string& childName = c1[i]->getName(); + c2Element = rhs->getChild(childName); + if (!c2Element) + { + if (result) + result->addDifference(c1[i]->getNamePath(), "", ElementEquivalenceResult::CHILD_NAME, + childName); + return false; + } + } + } + if (!c1[i]->isEquivalent(c2Element, options, result)) + return false; + } + return true; +} + +bool Element::isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName, + ElementEquivalenceOptions& /*options*/, ElementEquivalenceResult* result) const +{ + if (getAttribute(attributeName) != rhs->getAttribute(attributeName)) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName); + return false; + } + return true; +} + void Element::setName(const string& name) { ElementPtr parent = getParent(); @@ -482,6 +592,70 @@ TypeDefPtr TypedElement::getTypeDef() const // ValueElement methods // +bool ValueElement::isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName, + ElementEquivalenceOptions& options, ElementEquivalenceResult* result) const +{ + bool perforDefaultCompare = true; + + if (!options.skipValueComparisons) + { + const StringSet uiAttributes = + { + ValueElement::UI_MIN_ATTRIBUTE, ValueElement::UI_MAX_ATTRIBUTE, + ValueElement::UI_SOFT_MIN_ATTRIBUTE, ValueElement::UI_SOFT_MAX_ATTRIBUTE, + ValueElement::UI_STEP_ATTRIBUTE + }; + + // Get precision and format options + ScopedFloatFormatting fmt(options.format, options.precision); + + ConstValueElementPtr rhsValueElement = rhs->asA(); + + // Check value equality + if (attributeName == ValueElement::VALUE_ATTRIBUTE) + { + ValuePtr thisValue = getValue(); + ValuePtr rhsValue = rhsValueElement->getValue(); + if (thisValue && rhsValue) + { + if (thisValue->getValueString() != rhsValue->getValueString()) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName); + return false; + } + } + perforDefaultCompare = false; + } + + // Check ui attribute value equality + else if (uiAttributes.find(attributeName) != uiAttributes.end()) + { + const string& uiAttribute = getAttribute(attributeName); + const string& rhsUiAttribute = getAttribute(attributeName); + ValuePtr uiValue = !rhsUiAttribute.empty() ? Value::createValueFromStrings(uiAttribute, getType()) : nullptr; + ValuePtr rhsUiValue = !rhsUiAttribute.empty() ? Value::createValueFromStrings(rhsUiAttribute, getType()) : nullptr; + if (uiValue && rhsUiValue) + { + if (uiValue->getValueString() != rhsUiValue->getValueString()) + { + if (result) + result->addDifference(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName); + return false; + } + } + perforDefaultCompare = false; + } + } + + if (perforDefaultCompare) + { + return Element::isAttributeEquivalent(rhs, attributeName, options, result); + } + + return true; +} + string ValueElement::getResolvedValueString(StringResolverPtr resolver) const { if (!StringResolver::isResolvedType(getType())) diff --git a/source/MaterialXCore/Element.h b/source/MaterialXCore/Element.h index d1abcdfdca..047911f281 100644 --- a/source/MaterialXCore/Element.h +++ b/source/MaterialXCore/Element.h @@ -71,6 +71,92 @@ using ElementMap = std::unordered_map; /// A standard function taking an ElementPtr and returning a boolean. using ElementPredicate = std::function; +/// @class ElemenEquivalenceResult +/// The results of comparing for equivalence. +class MX_CORE_API ElementEquivalenceResult +{ + public: + ElementEquivalenceResult() = default; + ~ElementEquivalenceResult() = default; + + /// Append to list of equivalence differences + void addDifference(const string& path1, const string& path2, const string& differenceType, + const string& name=EMPTY_STRING) + { + StringVec difference = { path1, path2, differenceType, name}; + differences.push_back(difference); + } + + /// Clear result information + void clear() + { + differences.clear(); + } + + /// Get a list of equivalence differences + /// Difference is of the form: + /// { path to 1st element, path to 2nd element, difference type, [attribute if is attribute difference] } + StringVec getDifference(size_t index) const + { + if (index < differenceCount()) + return differences[index]; + return StringVec(); + } + + const size_t differenceCount() const + { + return differences.size(); + } + + static const string ATTRIBUTE; + static const string ATTRIBUTE_NAMES; + static const string CHILD_COUNT; + static const string CHILD_NAME; + static const string NAME; + static const string CATEGORY; + + private: + /// A list of differences + vector differences; +}; + +/// @class ElemenEquivalenceOptions +/// A set of options for controlling for equivalence comparison. +class MX_CORE_API ElementEquivalenceOptions +{ + public: + ElementEquivalenceOptions() + { + format = Value::getFloatFormat(); + precision = Value::getFloatPrecision(); + skipAttributes = {}; + skipValueComparisons = false; + }; + ~ElementEquivalenceOptions() { } + + /// Floating point format option for floating point value comparisons + Value::FloatFormat format; + + /// Floating point precision option for floating point value comparisons + int precision; + + /// Attribute filtering options. By default all attributes are considered. + /// Name, category attributes cannot be skipped. + /// + /// For example UI attribute comparision be skipped by setting: + /// skipAttributes = { + /// ValueElement::UI_MIN_ATTRIBUTE, ValueElement::UI_MAX_ATTRIBUTE, + /// ValueElement::UI_SOFT_MIN_ATTRIBUTE, ValueElement::UI_SOFT_MAX_ATTRIBUTE, + /// ValueElement::UI_STEP_ATTRIBUTE, Element::XPOS_ATTRIBUTE, + /// Element::YPOS_ATTRIBUTE }; + StringSet skipAttributes; + + /// Do not perform any value comparisions. Instead perform exact string comparisons for attributes + /// Default is false. The operator==() method can be used instead as it always performs + /// a strict comparison. Default is false. + bool skipValueComparisons; +}; + /// @class Element /// The base class for MaterialX elements. /// @@ -99,6 +185,9 @@ class MX_CORE_API Element : public std::enable_shared_from_this template friend class ElementRegistry; public: + /// @name Comparison interfaces + /// @{ + /// Return true if the given element tree, including all descendants, /// is identical to this one. bool operator==(const Element& rhs) const; @@ -107,6 +196,28 @@ class MX_CORE_API Element : public std::enable_shared_from_this /// differs from this one. bool operator!=(const Element& rhs) const; + /// Return true if the given element treee, including all descendents, + /// is considered to be equivalent to this one based on the equivalence + /// criteria provided. + /// @param rhs Element to compare against + /// @param options Equivalence criteria + /// @param result Results of comparison if argument is specified. + /// @return True if the elements are equivalent. False otherwise. + bool isEquivalent(ConstElementPtr rhs, ElementEquivalenceOptions& options, + ElementEquivalenceResult* result = nullptr) const; + + /// Return true if the attribute on a given element is equivalent + /// based on the equivalence criteria provided. + /// @param rhs Element to compare against + /// @param attributeName Name of attribute to compare + /// @param options Equivalence criteria + /// @param result Results of comparison if argument is specified. + /// @return True if the attribute on the elements are equivalent. False otherwise. + virtual bool isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName, + ElementEquivalenceOptions& options, + ElementEquivalenceResult* result = nullptr) const; + + /// @} /// @name Category /// @{ @@ -925,6 +1036,21 @@ class MX_CORE_API ValueElement : public TypedElement public: virtual ~ValueElement() { } + /// @name Comparison interfaces + /// @{ + + /// Return true if the attribute on a given element is equivalent + /// based on the equivalence criteria provided. + /// @param rhs Element to compare against + /// @param attributeName Name of attribute to compare + /// @param options Equivalence criteria + /// @param result Results of comparison if argument is specified. + /// @return True if the attribute on the elements are equivalent. False otherwise. + bool isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName, + ElementEquivalenceOptions& options, + ElementEquivalenceResult* result = nullptr) const override; + + /// @} /// @name Value String /// @{ diff --git a/source/MaterialXTest/MaterialXCore/Document.cpp b/source/MaterialXTest/MaterialXCore/Document.cpp index fb35e58ccb..be0119dcde 100644 --- a/source/MaterialXTest/MaterialXCore/Document.cpp +++ b/source/MaterialXTest/MaterialXCore/Document.cpp @@ -10,6 +10,9 @@ #include #include +#include +#include + namespace mx = MaterialX; TEST_CASE("Document", "[document]") @@ -116,3 +119,215 @@ TEST_CASE("Document", "[document]") // Validate the combined document. REQUIRE(doc->validate()); } + +void printDifferences(mx::ElementEquivalenceResult& result, const std::string& label) +{ + size_t differenceCount = result.differenceCount(); + for (size_t i=0; i inputMap; + + inputMap.insert({ "color3", " 1.0, +2.0, 3.0 " }); + inputMap.insert({ "color4", "1.0, 2.00, 0.3000, -4" }); + inputMap.insert({ "integer", " 12 " }); + inputMap.insert({ "matrix33", + "01.0, 2.0, 0000.2310, " + " 01.0, 2.0, 0000.2310, " + "01.0, 2.0, 0000.2310 " }); + inputMap.insert({ "matrix44", + "01.0, 2.0, 0000.2310, 0.100, " + "01.0, 2.0, 0000.2310, 0.100, " + "01.0, 2.0, 0000.2310, 0.100, " + "01.0, 2.0, 0000.2310, 0.100" }); + inputMap.insert({ "vector2", "1.0, 0.012345608" }); // For precision check + inputMap.insert({ "vector3", " 1.0, +2.0, 3.0 " }); + inputMap.insert({ "vector4", "1.0, 2.00, 0.3000, -4" }); + inputMap.insert({ "string", "mystring" }); + inputMap.insert({ "boolean", "false" }); + inputMap.insert({ "filename", "filename1" }); + inputMap.insert({ "float", " 1.2e-10 " }); + inputMap.insert({ "float", " 00.1000 " }); + + unsigned int index = 0; + mx::ElementPtr child = doc->addNodeGraph("mygraph"); + mx::NodeGraphPtr graph = child->asA(); + for (auto it = inputMap.begin(); it != inputMap.end(); ++it) + { + const std::string inputType = (*it).first; + mx::InputPtr input = graph->addInput("input_" + std::to_string(index), inputType); + if (inputType == "float") + { + input->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, " 0.0100 "); + input->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, " 01.0100 "); + index++; + } + else + { + input->setName("input_" + inputType); // Set by name for difference in order test + } + input->setValueString((*it).second); + } + + mx::DocumentPtr doc2 = mx::createDocument(); + std::unordered_multimap inputMap2; + inputMap2.insert({ "color4", "1, 2, 0.3, -4" }); + inputMap2.insert({ "integer", "12" }); + inputMap2.insert({ "matrix33", "1, 2, 0.231, 1, 2, 0.231, 1, 2, 0.231, 1, 2, 0.231" }); + inputMap2.insert({ "matrix44", "1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1" }); + inputMap2.insert({ "vector2", "1, 0.012345611" }); // For precision check + inputMap2.insert({ "string", "mystring" }); + inputMap2.insert({ "boolean", "false" }); + inputMap2.insert({ "color3", "1, 2, 3" }); + inputMap2.insert({ "vector3", "1, 2, 3" }); + inputMap2.insert({ "vector4", "1, 2, 0.3, -4" }); + inputMap2.insert({ "filename", "filename1" }); + inputMap2.insert({ "float", "1.2e-10" }); + inputMap2.insert({ "float", "0.1" }); + + index = 0; + child = doc2->addNodeGraph("mygraph"); + graph = child->asA(); + std::vector floatInputs; + for (auto it = inputMap2.begin(); it != inputMap2.end(); ++it) + { + const std::string inputType = (*it).first; + mx::InputPtr input = graph->addInput("input_" + std::to_string(index), inputType); + // Note: order of value and ui attributes is different for ordering comparison + input->setValueString((*it).second); + if (inputType == "float") + { + input->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, " 0.01"); + input->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, " 1.01"); + floatInputs.push_back(input); + index++; + } + else + { + input->setName("input_" + inputType); + } + } + + mx::ElementEquivalenceOptions options; + mx::ElementEquivalenceResult result; + + // Check skipping all value compares + options.skipValueComparisons = true; + bool equivalent = doc->isEquivalent(doc2, options, &result); + if (equivalent) + { + std::cout << "Unexpected skip value equivalence:" << std::endl; + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + else + { + printDifferences(result, "Expected value differences"); + } + REQUIRE(!equivalent); + + // Check attibute values + options.skipValueComparisons = false; + result.clear(); + equivalent = doc->isEquivalent(doc2, options, &result); + if (!equivalent) + { + printDifferences(result, "Unexpected value difference"); + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + REQUIRE(equivalent); + + unsigned int currentPrecision = mx::Value::getFloatPrecision(); + // This will compare 0.012345608 versus: 1, 0.012345611 for input10 + options.precision = 8; + equivalent = doc->isEquivalent(doc2, options); + if (equivalent) + { + std::cout << "Unexpected precision equivalence:" << std::endl; + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + else + { + printDifferences(result, "Expected precision difference"); + } + REQUIRE(!equivalent); + options.precision = currentPrecision; + + // Check attribute filtering of inputs + result.clear(); + options.skipAttributes = { mx::ValueElement::UI_MIN_ATTRIBUTE, mx::ValueElement::UI_MAX_ATTRIBUTE }; + for (mx::InputPtr floatInput : floatInputs) + { + floatInput->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, "0.9"); + floatInput->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, "100.0"); + } + equivalent = doc->isEquivalent(doc2, options, &result); + if (!equivalent) + { + printDifferences(result, "Unexpected filtering differences"); + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + REQUIRE(equivalent); + for (mx::InputPtr floatInput : floatInputs) + { + floatInput->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, " 0.01"); + floatInput->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, " 1.01"); + } + + // Check for child name mismatch + mx::ElementPtr mismatchElement = doc->getDescendant("mygraph/input_color4"); + std::string previousName = mismatchElement->getName(); + mismatchElement->setName("mismatch_color4"); + result.clear(); + equivalent = doc->isEquivalent(doc2, options, &result); + if (!equivalent) + { + printDifferences(result, "Expected name mismatch differences"); + } + else + { + std::cout << "Unexpected name match equivalence:" << std::endl; + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + REQUIRE(!equivalent); + mismatchElement->setName(previousName); + result.clear(); + equivalent = doc->isEquivalent(doc2, options, &result); + REQUIRE(equivalent); + + // Check for functional nodegraphs + mx::NodeGraphPtr nodeGraph = doc->getNodeGraph("mygraph"); + REQUIRE(nodeGraph); + doc->addNodeDef("ND_mygraph"); + nodeGraph->setNodeDefString("ND_mygraph"); + mx::NodeGraphPtr nodeGraph2 = doc2->getNodeGraph("mygraph"); + REQUIRE(nodeGraph2); + doc2->addNodeDef("ND_mygraph"); + nodeGraph2->setNodeDefString("ND_mygraph"); + result.clear(); + equivalent = doc->isEquivalent(doc2, options, &result); + if (!equivalent) + { + printDifferences(result, "Expected functional graph differences"); + } + else + { + std::cout << "Unexpected functional graph equivalence:" << std::endl; + std::cout << "Document 1: " << mx::prettyPrint(doc) << std::endl; + std::cout << "Document 2: " << mx::prettyPrint(doc2) << std::endl; + } + REQUIRE(!equivalent); +}