Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Element Equivalence Interfaces #2003

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions source/MaterialXCore/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down Expand Up @@ -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<ElementPtr>& c1 = getChildren();
jstone-lucasfilm marked this conversation as resolved.
Show resolved Hide resolved
const vector<ElementPtr>& 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<GraphElement>();
if (graph)
{
NodeGraphPtr nodeGraph = graph->asA<NodeGraph>();
DocumentPtr document = graph->asA<Document>();
if (document || (nodeGraph && !nodeGraph->getNodeDef()))
{
const string& childName = c1[i]->getName();
c2Element = rhs->getChild(childName);
if (!c2Element)
{
if (result)
result->addDifference(c1[i]->getNamePath(), "<NONE>", 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();
Expand Down Expand Up @@ -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<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 (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()))
Expand Down
126 changes: 126 additions & 0 deletions source/MaterialXCore/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,92 @@ using ElementMap = std::unordered_map<string, ElementPtr>;
/// A standard function taking an ElementPtr and returning a boolean.
using ElementPredicate = std::function<bool(ConstElementPtr)>;

/// @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<StringVec> 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.
///
Expand Down Expand Up @@ -99,6 +185,9 @@ class MX_CORE_API Element : public std::enable_shared_from_this<Element>
template <class T> 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;
Expand All @@ -107,6 +196,28 @@ class MX_CORE_API Element : public std::enable_shared_from_this<Element>
/// 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
/// @{

Expand Down Expand Up @@ -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
/// @{

Expand Down
Loading