Skip to content

Commit

Permalink
Add Element Equivalence Interfaces (AcademySoftwareFoundation#2003)
Browse files Browse the repository at this point in the history
- Adds a new `isEquivalent` method to `Element`. 
- Adds a new `isAttributeEquivalent` method for `Element` and override for `ValueElement` to allow for value vs string comparisons on the latter.
- Adds equivalence options class: `ElementEquivalanceOptions` which can be passed in as an argument to `isEquivalent`
- Adds an optional results / feedback class: `ElementEquivalenceResult` which can be passed in as an argument to `isEquivalent` and `isAttributeEquivalent`.
  • Loading branch information
kwokcb authored Oct 7, 2024
1 parent 19d6928 commit a5073ac
Show file tree
Hide file tree
Showing 4 changed files with 521 additions and 0 deletions.
174 changes: 174 additions & 0 deletions source/MaterialXCore/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ const string ValueElement::UI_ADVANCED_ATTRIBUTE = "uiadvanced";
const string ValueElement::UNIT_ATTRIBUTE = "unit";
const string ValueElement::UNITTYPE_ATTRIBUTE = "unittype";
const string ValueElement::UNIFORM_ATTRIBUTE = "uniform";
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::CreatorMap Element::_creatorMap;

Expand Down Expand Up @@ -334,6 +340,108 @@ bool Element::hasInheritanceCycle() const
return false;
}

bool Element::isEquivalent(ConstElementPtr rhs, const ElementEquivalenceOptions& options,
ElementEquivalenceResultVec* results) const
{
if (getName() != rhs->getName())
{
if (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::NAME));
return false;
}
if (getCategory() != rhs->getCategory())
{
if (results)
results->push_back(ElementEquivalenceResult(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 (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE_NAMES));
return false;
}

for (const string& attr : rhsAttributeNames)
{
if (!isAttributeEquivalent(rhs, attr, options, results))
{
return false;
}
}

// Compare children.
const vector<ElementPtr>& children = getChildren();
const vector<ElementPtr>& rhsChildren = rhs->getChildren();
if (children.size() != rhsChildren.size())
{
if (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::CHILD_COUNT));
return false;
}
for (size_t i = 0; i < children.size(); i++)
{
ElementPtr rhsElement = rhsChildren[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.)
ConstGraphElementPtr graph = this->getSelf()->asA<GraphElement>();
if (graph)
{
ConstNodeGraphPtr nodeGraph = graph->asA<NodeGraph>();
ConstDocumentPtr document = graph->asA<Document>();
if (document || (nodeGraph && !nodeGraph->getNodeDef()))
{
const string& childName = children[i]->getName();
rhsElement = rhs->getChild(childName);
if (!rhsElement)
{
if (results)
results->push_back(ElementEquivalenceResult(children[i]->getNamePath(), "<NONE>",
ElementEquivalenceResult::CHILD_NAME, childName));
return false;
}
}
}
if (!children[i]->isEquivalent(rhsElement, options, results))
return false;
}
return true;
}

bool Element::isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName,
const ElementEquivalenceOptions& /*options*/, ElementEquivalenceResultVec* results) const
{
if (getAttribute(attributeName) != rhs->getAttribute(attributeName))
{
if (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName));
return false;
}
return true;
}

TreeIterator Element::traverseTree() const
{
return TreeIterator(getSelfNonConst());
Expand Down Expand Up @@ -534,6 +642,72 @@ const string& ValueElement::getActiveUnit() const
return EMPTY_STRING;
}

bool ValueElement::isAttributeEquivalent(ConstElementPtr rhs, const string& attributeName,
const ElementEquivalenceOptions& options, ElementEquivalenceResultVec* results) const
{
// Perform value comparisons
bool performedValueComparison = false;
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<ValueElement>();

// Check value equality
if (attributeName == ValueElement::VALUE_ATTRIBUTE)
{
ValuePtr thisValue = getValue();
ValuePtr rhsValue = rhsValueElement->getValue();
if (thisValue && rhsValue)
{
if (thisValue->getValueString() != rhsValue->getValueString())
{
if (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName));
return false;
}
}
performedValueComparison = true;
}

// 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 (results)
results->push_back(ElementEquivalenceResult(getNamePath(), rhs->getNamePath(), ElementEquivalenceResult::ATTRIBUTE, attributeName));
return false;
}
}

performedValueComparison = true;
}
}

// If did not peform a value comparison, perform the default comparison
if (!performedValueComparison)
{
return Element::isAttributeEquivalent(rhs, attributeName, options, results);
}

return true;
}

bool ValueElement::validate(string* message) const
{
bool res = true;
Expand Down
110 changes: 110 additions & 0 deletions source/MaterialXCore/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ using ElementMap = std::unordered_map<string, ElementPtr>;
/// A standard function taking an ElementPtr and returning a boolean.
using ElementPredicate = std::function<bool(ConstElementPtr)>;

class ElementEquivalenceOptions;
class ElementEquivalenceResult;
using ElementEquivalenceResultVec = vector<ElementEquivalenceResult>;

/// @class Element
/// The base class for MaterialX elements.
///
Expand Down Expand Up @@ -612,6 +616,31 @@ class MX_CORE_API Element : public std::enable_shared_from_this<Element>
return nullptr;
}

/// @}
/// @name Functional Equivalence
/// @{

/// Return true if the given element tree, 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 results Results of comparison if argument is specified.
/// @return True if the elements are equivalent. False otherwise.
bool isEquivalent(ConstElementPtr rhs, const ElementEquivalenceOptions& options,
ElementEquivalenceResultVec* results = 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 results 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,
const ElementEquivalenceOptions& options,
ElementEquivalenceResultVec* results = nullptr) const;

/// @}
/// @name Traversal
/// @{
Expand Down Expand Up @@ -1114,6 +1143,21 @@ class MX_CORE_API ValueElement : public TypedElement
return getTypedAttribute<bool>(UNIFORM_ATTRIBUTE);
}

/// @}
/// @name Functional Equivalence
/// @{

/// 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 results 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,
const ElementEquivalenceOptions& options,
ElementEquivalenceResultVec* results = nullptr) const override;

/// @}
/// @name Validation
/// @{
Expand Down Expand Up @@ -1336,6 +1380,72 @@ class MX_CORE_API StringResolver
StringMap _geomNameMap;
};

/// @class ElementEquivalenceResult
/// A comparison result for the functional equivalence of two elements.
class MX_CORE_API ElementEquivalenceResult
{
public:
ElementEquivalenceResult(const string& p1, const string& p2, const string& type,
const string& attrName = EMPTY_STRING)
{
path1 = p1;
path2 = p2;
differenceType = type;
attributeName = attrName;
}
ElementEquivalenceResult() = delete;
~ElementEquivalenceResult() = default;

string path1;
string path2;
string differenceType;
string attributeName;

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;
};

/// @class ElementEquivalenceOptions
/// A set of options for comparing the functional equivalence of elements.
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 ExceptionOrphanedElement
/// An exception that is thrown when an ElementPtr is used after its owning
/// Document has gone out of scope.
Expand Down
Loading

0 comments on commit a5073ac

Please sign in to comment.