diff --git a/resources/Materials/TestSuite/locale/numericformat.mtlx b/resources/Materials/TestSuite/locale/numericformat.mtlx
new file mode 100644
index 0000000000..b9400e270c
--- /dev/null
+++ b/resources/Materials/TestSuite/locale/numericformat.mtlx
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/Materials/TestSuite/locale/utf8.mtlx b/resources/Materials/TestSuite/locale/utf8.mtlx
new file mode 100644
index 0000000000..a6a30f02d2
--- /dev/null
+++ b/resources/Materials/TestSuite/locale/utf8.mtlx
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/MaterialXCore/Unit.cpp b/source/MaterialXCore/Unit.cpp
index f55cdc108a..959f697b24 100644
--- a/source/MaterialXCore/Unit.cpp
+++ b/source/MaterialXCore/Unit.cpp
@@ -241,4 +241,70 @@ void UnitConverterRegistry::write(DocumentPtr doc) const
}
}
+bool UnitConverterRegistry::convertToUnit(DocumentPtr doc, const string& unitType, const string& targetUnit)
+{
+ UnitTypeDefPtr unitTypeDef = doc->getUnitTypeDef(unitType);
+ UnitConverterPtr unitConverter = getUnitConverter(unitTypeDef);
+
+ if (!unitTypeDef || !unitConverter)
+ {
+ return false;
+ }
+
+ bool convertedUnits = false;
+ for (ElementPtr elem : doc->traverseTree())
+ {
+ NodePtr pNode = elem->asA();
+ if (!pNode || !pNode->getInputCount())
+ {
+ continue;
+ }
+ for (InputPtr input : pNode->getInputs())
+ {
+ const std::string type = input->getType();
+ const ValuePtr value = input->getValue();
+ if (value && input->hasUnit() && (input->getUnitType() == unitType) && value)
+ {
+ if (type == getTypeString())
+ {
+ float originalval = value->asA();
+ float convertedValue = unitConverter->convert(originalval, input->getUnit(), targetUnit);
+ input->setValue(convertedValue);
+ input->removeAttribute(ValueElement::UNIT_ATTRIBUTE);
+ input->removeAttribute(ValueElement::UNITTYPE_ATTRIBUTE);
+ convertedUnits = true;
+ }
+ else if (type == getTypeString())
+ {
+ Vector2 originalval = value->asA();
+ Vector2 convertedValue = unitConverter->convert(originalval, input->getUnit(), targetUnit);
+ input->setValue(convertedValue);
+ input->removeAttribute(ValueElement::UNIT_ATTRIBUTE);
+ input->removeAttribute(ValueElement::UNITTYPE_ATTRIBUTE);
+ convertedUnits = true;
+ }
+ else if (type == getTypeString())
+ {
+ Vector3 originalval = value->asA();
+ Vector3 convertedValue = unitConverter->convert(originalval, input->getUnit(), targetUnit);
+ input->setValue(convertedValue);
+ input->removeAttribute(ValueElement::UNIT_ATTRIBUTE);
+ input->removeAttribute(ValueElement::UNITTYPE_ATTRIBUTE);
+ convertedUnits = true;
+ }
+ else if (type == getTypeString())
+ {
+ Vector4 originalval = value->asA();
+ Vector4 convertedValue = unitConverter->convert(originalval, input->getUnit(), targetUnit);
+ input->setValue(convertedValue);
+ input->removeAttribute(ValueElement::UNIT_ATTRIBUTE);
+ input->removeAttribute(ValueElement::UNITTYPE_ATTRIBUTE);
+ convertedUnits = true;
+ }
+ }
+ }
+ }
+ return convertedUnits;
+}
+
MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXCore/Unit.h b/source/MaterialXCore/Unit.h
index f50101a1bf..26b2b0df58 100644
--- a/source/MaterialXCore/Unit.h
+++ b/source/MaterialXCore/Unit.h
@@ -195,6 +195,10 @@ class MX_CORE_API UnitConverterRegistry
/// Create unit definitions in a document based on registered converters
void write(DocumentPtr doc) const;
+ /// Convert input values which have a source unit to a given target unit.
+ /// Returns if any unit conversion occured.
+ bool convertToUnit(DocumentPtr doc, const string& unitType, const string& targetUnit);
+
private:
UnitConverterRegistry(const UnitConverterRegistry&) = delete;
UnitConverterRegistry() { }
diff --git a/source/MaterialXCore/Util.cpp b/source/MaterialXCore/Util.cpp
index a0fb1ff1f2..9805f4a7c9 100644
--- a/source/MaterialXCore/Util.cpp
+++ b/source/MaterialXCore/Util.cpp
@@ -91,6 +91,16 @@ StringVec splitString(const string& str, const string& sep)
return split;
}
+string joinStrings(const StringVec& stringVec, const string& sep)
+{
+ string res;
+ for (const string& name : stringVec)
+ {
+ res = res.empty() ? name : res + sep + name;
+ }
+ return res;
+}
+
string replaceSubstrings(string str, const StringMap& stringMap)
{
for (const auto& pair : stringMap)
@@ -145,11 +155,7 @@ StringVec splitNamePath(const string& namePath)
string createNamePath(const StringVec& nameVec)
{
- string res;
- for (const string& name : nameVec)
- {
- res = res.empty() ? name : res + NAME_PATH_SEPARATOR + name;
- }
+ string res = joinStrings(nameVec, NAME_PATH_SEPARATOR);
return res;
}
diff --git a/source/MaterialXCore/Util.h b/source/MaterialXCore/Util.h
index 8e201b37a3..a3f2e8a64c 100644
--- a/source/MaterialXCore/Util.h
+++ b/source/MaterialXCore/Util.h
@@ -35,6 +35,10 @@ MX_CORE_API string incrementName(const string& name);
/// separator characters.
MX_CORE_API StringVec splitString(const string& str, const string& sep);
+/// Join a vector of substrings into a single string, placing the given
+/// separator between each substring.
+MX_CORE_API string joinStrings(const StringVec& strVec, const string& sep);
+
/// Apply the given substring substitutions to the input string.
MX_CORE_API string replaceSubstrings(string str, const StringMap& stringMap);
diff --git a/source/MaterialXCore/Value.cpp b/source/MaterialXCore/Value.cpp
index f83fb81e63..ebf1346f8d 100644
--- a/source/MaterialXCore/Value.cpp
+++ b/source/MaterialXCore/Value.cpp
@@ -31,6 +31,7 @@ template using enable_if_std_vector_t =
template void stringToData(const string& str, T& data)
{
std::stringstream ss(str);
+ ss.imbue(std::locale::classic());
if (!(ss >> data))
{
throw ExceptionTypeError("Type mismatch in generic stringToData: " + str);
@@ -94,6 +95,7 @@ template void stringToData(const string& str, enable_if_std_vector_t void dataToString(const T& data, string& str)
{
std::stringstream ss;
+ ss.imbue(std::locale::classic());
// Set float format and precision for the stream
const Value::FloatFormat fmt = Value::getFloatFormat();
diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
index d59e8d930d..fb2df02acf 100644
--- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
+++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
@@ -227,3 +227,75 @@ TEST_CASE("Load content", "[xmlio]")
mx::DocumentPtr nonExistentDoc = mx::createDocument();
REQUIRE_THROWS_AS(mx::readFromXmlFile(nonExistentDoc, "NonExistent.mtlx", mx::FileSearchPath(), &readOptions), mx::ExceptionFileMissing&);
}
+
+TEST_CASE("Locale region testing", "[xmlio]")
+{
+ /// In the United States, the thousands separator is a comma, while in Germany it is a period.
+ /// Thus, one thousand twenty five is displayed as 1,025 in the United States and 1.025 in Germany.
+ ///
+ /// In a MaterialX document, a vector3 value of "1,1.5,2.0" should be interpreted as (1.0f, 1.5f, 2.0f).
+
+ try
+ {
+ // Set locale to de
+ std::locale deLocale("de_DE");
+ std::locale::global(deLocale);
+ }
+ catch (const std::runtime_error& e)
+ {
+ WARN("Unable to change locale " << e.what());
+ return;
+ }
+
+ mx::FilePath libraryPath("libraries/stdlib");
+ mx::FilePath testPath("resources/Materials/TestSuite/locale");
+ mx::FileSearchPath searchPath = libraryPath.asString() +
+ mx::PATH_LIST_SEPARATOR +
+ testPath.asString();
+
+ // Read the standard library.
+ std::vector libs;
+ for (const mx::FilePath& filename : libraryPath.getFilesInDirectory(mx::MTLX_EXTENSION))
+ {
+ mx::DocumentPtr lib = mx::createDocument();
+ mx::readFromXmlFile(lib, filename, searchPath);
+ libs.push_back(lib);
+ }
+
+ // Read and validate each example document.
+ for (const mx::FilePath& filename : testPath.getFilesInDirectory(mx::MTLX_EXTENSION))
+ {
+ mx::DocumentPtr doc = mx::createDocument();
+ mx::readFromXmlFile(doc, filename, searchPath);
+ for (mx::DocumentPtr lib : libs)
+ {
+ doc->importLibrary(lib);
+ }
+ std::string message;
+
+ bool docValid = doc->validate(&message);
+ if (!docValid)
+ {
+ WARN("[" + filename.asString() + "] " + message);
+ }
+ REQUIRE(docValid);
+
+ // Traverse the document tree
+ int valueElementCount = 0;
+ int uiAttributeCount = 0;
+ for (mx::ElementPtr elem : doc->traverseTree())
+ {
+ if (elem->isA())
+ {
+ valueElementCount++;
+ if (elem->hasAttribute("uiname"))
+ {
+ REQUIRE(!elem->getAttribute("uiname").empty());
+ uiAttributeCount++;
+ }
+ }
+ }
+ REQUIRE(valueElementCount > 0);
+ REQUIRE(uiAttributeCount > 0);
+ }
+}
diff --git a/source/PyMaterialX/PyMaterialXCore/PyUtil.cpp b/source/PyMaterialX/PyMaterialXCore/PyUtil.cpp
index a646a5ecf1..190668a608 100644
--- a/source/PyMaterialX/PyMaterialXCore/PyUtil.cpp
+++ b/source/PyMaterialX/PyMaterialXCore/PyUtil.cpp
@@ -21,6 +21,7 @@ void bindPyUtil(py::module& mod)
mod.def("isValidName", &mx::isValidName);
mod.def("incrementName", &mx::incrementName);
mod.def("splitString", &mx::splitString);
+ mod.def("joinStrings", &mx::joinStrings);
mod.def("replaceSubstrings", &mx::replaceSubstrings);
mod.def("stringEndsWith", &mx::stringEndsWith);
mod.def("splitNamePath", &mx::splitNamePath);
diff --git a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp
index 683a487931..1ac0c5ca7c 100644
--- a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp
+++ b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp
@@ -35,6 +35,12 @@ void bindPyXmlIo(py::module& mod)
py::arg("doc"), py::arg("writeOptions") = nullptr);
mod.def("prependXInclude", mx::prependXInclude);
+ mod.def("getEnvironmentPath", &mx::getEnvironmentPath,
+ py::arg("sep") = mx::PATH_LIST_SEPARATOR);
+
+ mod.attr("PATH_LIST_SEPARATOR") = mx::PATH_LIST_SEPARATOR;
+ mod.attr("MATERIALX_SEARCH_PATH_ENV_VAR") = mx::MATERIALX_SEARCH_PATH_ENV_VAR;
+
py::register_exception(mod, "ExceptionParseError");
py::register_exception(mod, "ExceptionFileMissing");
}