diff --git a/resources/Materials/TestSuite/locale/numericformat.mtlx b/resources/Materials/TestSuite/locale/numericformat.mtlx new file mode 100644 index 0000000000..6e9738b8f6 --- /dev/null +++ b/resources/Materials/TestSuite/locale/numericformat.mtlx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/Materials/TestSuite/locale/utf8.mtlx b/resources/Materials/TestSuite/locale/utf8.mtlx new file mode 100644 index 0000000000..9dca21491c --- /dev/null +++ b/resources/Materials/TestSuite/locale/utf8.mtlx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/source/MaterialXContrib/Utilities/CMakeLists.txt b/source/MaterialXContrib/Utilities/CMakeLists.txt new file mode 100644 index 0000000000..f8a604dff5 --- /dev/null +++ b/source/MaterialXContrib/Utilities/CMakeLists.txt @@ -0,0 +1 @@ +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Scripts/" DESTINATION "${CMAKE_INSTALL_PREFIX}/python" MESSAGE_NEVER) \ No newline at end of file diff --git a/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py b/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py new file mode 100644 index 0000000000..32b08da4b9 --- /dev/null +++ b/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +""" +Utility to generate json and hpp from MaterialX nodedef + +Given a node def e.g. ND_standard_surface_surfaceshader will +generate a standard_surface.json and standard_surface.hpp +The hpp/json can be used for simple reflection instead +of parsing mtlx libraries +""" + +import sys +import os +import argparse +import json +import hashlib +import MaterialX as mx + +INPUTFILEHASH = 0 +mx_stdTypes = { + 'color3': ['MaterialX::Color3', mx.Color3(1, 1, 1)], + 'color4': ['MaterialX::Color4', mx.Color4(1, 1, 1, 1)], + 'vector4': ['MaterialX::Vector4', mx.Vector4(1, 1, 1, 1)], + 'vector3': ['MaterialX::Vector3', mx.Vector3(1, 1, 1)], + 'vector2': ['MaterialX::Vector2', mx.Vector2(1, 1)], + 'matrix33': ['MaterialX::Matrix33', None], + 'matrix44': ['MaterialX::Matrix44', None], + 'integerarray': ['std::vector', None], + 'floatarray': ['std::vector', None], + 'color3array': ['std::vector', None], + 'color4array': ['std::vector', None], + 'vector2array': ['std::vector', None], + 'vector3array': ['std::vector', None], + 'vector4array': ['std::vector', None], + 'stringarray': ['std::vector', None], + 'boolean': ['bool', False], + 'integer': ['int', 0], + 'file': ['std::string', ""], + 'filename': ['std::string', ""], + 'string': ['std::string', ""], + 'float': ['float', 0], + + #TODO: create custom structs (fixme) + 'lightshader': ['lightshader', None], + 'volumeshader': ['volumeshader', None], + 'displacementshader': ['displacementshader', None], + 'surfaceshader': ['surfaceshader', None], + 'BSDF': ['BSDF', None], + 'EDF': ['EDF', None], + 'VDF': ['VDF', None], +} + +def _getType(mxType): + return mx_stdTypes[mxType][0] + +def _getDefault(mxType): + return mx_stdTypes[mxType][1] + +# Compute gitHash +def _computeGitHash(mtlxfile): + with open(mtlxfile, 'r') as afile: + buf = afile.read().encode() + hasher = hashlib.sha1() + hasher.update(b"blob %u\0" % len(buf)) + hasher.update(buf) + return hasher.hexdigest() + +def main(): + parser = argparse.ArgumentParser( + description="MaterialX nodedef to json/hpp converter.") + parser.add_argument(dest="inputFilename", + help="Filename of the input document.") + parser.add_argument("--node", dest="nodedef", type=str, + help="Node to export") + parser.add_argument("--stdlib", dest="stdlib", action="store_true", + help="Import standard MaterialX libraries into the document.") + opts = parser.parse_args() + + doc = mx.createDocument() + try: + mx.readFromXmlFile(doc, opts.inputFilename) + # Git hash for tracking source document + global INPUTFILEHASH + INPUTFILEHASH = _computeGitHash(opts.inputFilename) + + except mx.ExceptionFileMissing as err: + print(err) + sys.exit(0) + + if opts.stdlib: + stdlib = mx.createDocument() + filePath = os.path.dirname(os.path.abspath(__file__)) + searchPath = mx.FileSearchPath(os.path.join(filePath, '..', '..')) + searchPath.append(os.path.dirname(opts.inputFilename)) + libraryFolders = ["libraries"] + mx.loadLibraries(libraryFolders, searchPath, stdlib) + doc.importLibrary(stdlib) + + (valid, message) = doc.validate() + if valid: + print("%s is a valid MaterialX document in v%s" % + (opts.inputFilename, mx.getVersionString())) + else: + print("%s is not a valid MaterialX document in v%s" % + (opts.inputFilename, mx.getVersionString())) + print(message) + + nodedefs = doc.getNodeDefs() + nodedef = findNodeDef(nodedefs, opts.nodedef) + + print("Document Version: {}.{:02d}".format(*doc.getVersionIntegers())) + if nodedef is None: + print("Nodedef %s not found" % (opts.nodedef)) + else: + try: + exportNodeDef(nodedef) + print("%d NodeDef%s found.\nNode '%s' exported to %s(.json/.hpp)" + % (len(nodedefs), pl(nodedefs), opts.nodedef, nodedef.getNodeString())) + except Exception as e: + print(e) + sys.exit(0) + +def findNodeDef(elemlist, nodedefname): + if len(elemlist) == 0: + return None + for elem in elemlist: + if elem.isA(mx.NodeDef) and elem.getName() == nodedefname: + return elem + return None + +def exportNodeDef(elem): + if elem.isA(mx.NodeDef): + jsonfilename = elem.getNodeString()+'.json' + hppfilename = elem.getNodeString()+'.hpp' + export_json(elem, jsonfilename) + export_hpp(elem, hppfilename) + +def export_json(elem, filename): + nodefInterface = {} + nodefInterface["Nodedef"] = elem.getName() + nodefInterface["SHA1"] = INPUTFILEHASH + nodefInterface["MaterialX"] = mx.getVersionString() + nodefInterface["name"] = elem.getNodeString() + asJsonArray(nodefInterface, elem) + with open(filename, 'w', encoding='utf-8') as f: + json.dump(nodefInterface, f, indent=4) + +def asJsonArray(nodefInterface, nodedef): + inputs = [] + outputs = [] + for inp in nodedef.getActiveInputs(): + inputs.append((_getType(inp.getType()), + inp.getName(), + str(inp.getValue()))) + nodefInterface["inputs"] = inputs + for output in nodedef.getActiveOutputs(): + outputs.append((_getType(output.getType()), + output.getName(), + str(output.getValue()))) + nodefInterface["outputs"] = outputs + +def export_hpp(elem, filename): + # write to file + preamble = "/*\nGenerated using MaterialX nodedef \ + \n{nodename}\nSHA1:{filehash}\nVersion:{version}\n*/\n"\ + .format(nodename=elem, filehash=INPUTFILEHASH, version=mx.getVersionString()) + variable_defs = "" + for inp in elem.getActiveInputs(): + #create decl + decl = getVarDeclaration(inp) + #emit variable decl + if decl is None: + variable_def = ' {typename} {name};\n' \ + .format(typename=_getType(inp.getType()), + name=inp.getName()) + else: + variable_def = ' {typename} {name} = {declaration};\n' \ + .format(typename=_getType(inp.getType()), + name=inp.getName(), + declaration=decl) + variable_defs += variable_def + for output in elem.getActiveOutputs(): + #create decl + decl = getVarDeclaration(output) + #emit output + if decl is None: + variable_def = ' {typename}* {name};\n' \ + .format(typename=_getType(output.getType()), + name=output.getName()) + else: + variable_def = ' {typename} {name} = {declaration};\n' \ + .format(typename=_getType(output.getType()), + name=output.getName(), + declaration=decl) + variable_defs += variable_def + nodename_definition = ' std::string _nodename_ = "{nodename}";\n'.format( + nodename=elem.getNodeString()) + # create struct definition + struct_definition = """struct {structname} {{\n{variabledefs}{nodeiddef}}};""" \ + .format(structname=elem.getName(), + variabledefs=variable_defs, + nodeiddef=nodename_definition) + + with open(filename, 'w', encoding='utf-8') as f: + f.write(preamble) + f.write(struct_definition) + f.close() + + +def getVarDeclaration(inputVar): + + inputValue = inputVar.getValue() + typeName = _getType(inputVar.getType()) + if isinstance(inputValue, (mx.Color3, mx.Vector3)): + val = '{typename}({v0}f, {v1}f, {v2}f)'.format(typename=typeName, + v0=round( + inputValue[0], 5), + v1=round( + inputValue[1], 5), + v2=round(inputValue[2], 5)) + return val + if isinstance(inputValue, (mx.Color4, mx.Vector4)): + val = '{typename}({v0}f, {v1}f, {v2}f, {v3}f)'.format(typename=typeName, + v0=round( + inputValue[0], 5), + v1=round( + inputValue[1], 5), + v2=round( + inputValue[2], 5), + v3=round(inputValue[3], 5)) + return val + if isinstance(inputValue, float): + val = '{0}f'.format(round(inputValue, 5)) + return val + if isinstance(inputValue, bool): + val = '{0}'.format('true' if inputValue is True else 'false') + return val + if isinstance(inputValue, int): + val = '{0}'.format(inputValue) + return val + + # use input type if value is not defined and set default + defaultValue = _getDefault(inputVar.getType()) + if inputValue is None: + if inputVar.getType() in ['vector2']: + val = '{typename}({v0}f, {v1}f)'.format(typename=typeName, + v0=defaultValue[0], + v1=defaultValue[1]) + return val + if inputVar.getType() in ['vector3', 'color3']: + val = '{typename}({v0}f, {v1}f, {v2}f)'.format(typename=typeName, + v0=defaultValue[0], + v1=defaultValue[1], + v2=defaultValue[2]) + return val + if inputVar.getType() in ['vector4', 'color4']: + val = '{typename}({v0}f, {v1}f, {v2}f, {v3}f)'.format(typename=typeName, + v0=defaultValue[0], + v1=defaultValue[1], + v2=defaultValue[2], + v3=defaultValue[3]) + return val + else: + print("unhandled: " + typeName) + return None + + +def pl(elem): + if len(elem) == 1: + return "" + else: + return "s" + + +if __name__ == '__main__': + main() diff --git a/source/MaterialXCore/Value.cpp b/source/MaterialXCore/Value.cpp index f6adfa89b0..fe199bc958 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,7 +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(); ss.setf(std::ios_base::fmtflags( diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp index 8410bf0fb9..e34f42b3e6 100644 --- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp @@ -243,3 +243,80 @@ TEST_CASE("Export Document", "[xmlio]") REQUIRE(exportedDoc->getLookGroups().size() == 0); REQUIRE(exportedDoc->getLooks().size() == 1); } + +TEST_CASE("Load locale content", "[xmlio_locale]") +{ + /// Test locale region + /// The character used as the thousands separator. + /// The character used as the decimal separator. + + /// In the United States, this character is a comma(, ). + /// In Germany, it is a period(.). + /// Thus one thousandand twenty - five is displayed as 1, 025 in the United States and 1.025 in Germany.In Sweden, the thousands separator is a space. + /// mx:Vector3(1,1.5,2.0) should be interpreted as float[3] = [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); + } +}