This commit is contained in:
2026-01-06 13:25:49 +00:00
parent 5d495d731b
commit 4e15e08b7f
1395 changed files with 295666 additions and 323 deletions
@@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/preset-env",
{
"exclude": ["transform-regenerator"]
}
]
]
}
@@ -0,0 +1,47 @@
module.exports = function (config)
{
config.set({
basePath: '../', // base is the javascript folder
files: [
{ pattern: '_build/JsMaterialXGenShader.js', watched: false, included: true, served: true },
{ pattern: '_build/JsMaterialXGenShader.wasm', watched: false, included: false, served: true },
{ pattern: '_build/JsMaterialXGenShader.data', watched: false, included: false, served: true, nocache: true },
{ pattern: 'browser/*.spec.js', watched: true, included: true, served: true },
],
mime: {
'application/wasm': ['wasm'],
'application/octet-stream; charset=UTF-8': ['data'],
},
proxies: {
'/JsMaterialXGenShader.data': '/base/_build/JsMaterialXGenShader.data',
},
reporters: ['mocha'],
client: {
mocha: {
reporter: 'html'
}
},
browsers: ['Chrome'],
port: 8080,
autoWatch: true,
concurrency: Infinity,
// logLevel: config.LOG_DEBUG,
frameworks: ['mocha', 'chai'],
plugins: [
'karma-chai',
'karma-chrome-launcher',
'karma-mocha',
'karma-mocha-reporter',
],
customLaunchers: {
ChromeHeadlessGL: {
base: 'Chrome',
flags: [
'--headless=new',
'--enable-gpu',
'--enable-webgl',
],
},
},
});
};
@@ -0,0 +1,131 @@
// MaterialX is served through a script tag in the test setup.
function createStandardSurfaceMaterial(mx)
{
const doc = mx.createDocument();
const ssName = 'SR_default';
const ssNode = doc.addChildOfCategory('standard_surface', ssName);
ssNode.setType('surfaceshader');
const smNode = doc.addChildOfCategory('surfacematerial', 'Default');
smNode.setType('material');
const shaderElement = smNode.addInput('surfaceshader');
shaderElement.setType('surfaceshader');
shaderElement.setNodeName(ssName);
expect(doc.validate()).to.be.true;
// Release local wrappers
shaderElement.delete();
smNode.delete();
ssNode.delete();
return doc;
}
describe('Generate Shaders', function ()
{
let mx;
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2');
this.timeout(60000);
before(async function ()
{
mx = await MaterialX();
});
it('Compile Shaders', () =>
{
const doc = createStandardSurfaceMaterial(mx);
const generators = []
if (typeof mx.EsslShaderGenerator != 'undefined')
generators.push(mx.EsslShaderGenerator.create());
if (typeof mx.GlslShaderGenerator != 'undefined')
generators.push(mx.GlslShaderGenerator.create());
if (typeof mx.MslShaderGenerator != 'undefined')
generators.push(mx.MslShaderGenerator.create());
if (typeof mx.OslShaderGenerator != 'undefined')
generators.push(mx.OslShaderGenerator.create());
if (typeof mx.VkShaderGenerator != 'undefined')
generators.push(mx.VkShaderGenerator.create());
if (typeof mx.WgslShaderGenerator != 'undefined')
generators.push(mx.WgslShaderGenerator.create());
if (typeof mx.MdlShaderGenerator != 'undefined')
generators.push(mx.MdlShaderGenerator.create());
if (typeof mx.SlangShaderGenerator != 'undefined')
generators.push(mx.SlangShaderGenerator.create());
const elem = mx.findRenderableElement(doc);
for (let gen of generators)
{
console.log("Generating shader for " + gen.getTarget() + "...");
const genContext = new mx.GenContext(gen);
const stdlib = mx.loadStandardLibraries(genContext);
doc.importLibrary(stdlib);
try
{
const mxShader = gen.generate(elem.getNamePath(), elem, genContext);
const fShader = mxShader.getSourceCode("pixel");
if (gen.getTarget() == 'essl')
{
const vShader = mxShader.getSourceCode("vertex");
const glVertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(glVertexShader, vShader);
gl.compileShader(glVertexShader);
if (!gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS))
{
console.error("-------- VERTEX SHADER FAILED TO COMPILE: ----------------");
console.error("--- VERTEX SHADER LOG ---");
console.error(gl.getShaderInfoLog(glVertexShader));
console.error("--- VERTEX SHADER START ---");
console.error(fShader);
console.error("--- VERTEX SHADER END ---");
}
expect(gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS)).to.equal(true);
const glPixelShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(glPixelShader, fShader);
gl.compileShader(glPixelShader);
if (!gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS))
{
console.error("-------- PIXEL SHADER FAILED TO COMPILE: ----------------");
console.error("--- PIXEL SHADER LOG ---");
console.error(gl.getShaderInfoLog(glPixelShader));
console.error("--- PIXEL SHADER START ---");
console.error(fShader);
console.error("--- PIXEL SHADER END ---");
}
expect(gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS)).to.equal(true);
// Cleanup GL shaders
gl.deleteShader(glVertexShader);
gl.deleteShader(glPixelShader);
}
// Cleanup shader wrapper
mxShader.delete();
}
catch (errPtr)
{
console.error("-------- Failed code generation: ----------------");
if (typeof mx.getExceptionMessage === 'function')
{
console.error(mx.getExceptionMessage(errPtr));
}
else
{
console.error(errPtr);
}
}
// Cleanup per-generator wrappers
stdlib.delete();
genContext.delete();
gen.delete();
}
// Cleanup element and document
elem.delete();
doc.delete();
});
});
@@ -0,0 +1,145 @@
import { expect } from 'chai';
import Module from './_build/JsMaterialXCore.js';
import { getMtlxStrings } from './testHelpers';
describe('Code Examples', () =>
{
it('Building a MaterialX Document', async () =>
{
const mx = await Module();
// Create a document.
const doc = mx.createDocument();
// Create a node graph with a single image node and output.
const nodeGraph = doc.addNodeGraph();
expect(doc.getNodeGraphs().length).to.equal(1);
const image = nodeGraph.addNode('image');
const nodes = nodeGraph.getNodes();
expect(nodes.length).to.equal(1);
expect(nodes[0]).to.eql(image);
image.setInputValueString('file', 'image1.tif', 'filename');
const input = image.getInput('file');
expect(input).to.not.be.null;
expect(input.getValue().getData()).to.equal('image1.tif');
const output = nodeGraph.addOutput();
const outputs = nodeGraph.getOutputs();
expect(outputs.length).to.equal(1);
expect(outputs[0]).to.eql(output);
output.setConnectedNode(image);
const connectedNode = output.getConnectedNode();
expect(connectedNode).to.not.be.null;
expect(connectedNode instanceof mx.Node).to.be.true;
// Create a simple shader interface.
const simpleSrf = doc.addNodeDef('ND_simpleSrf', 'surfaceshader', 'simpleSrf');
const nodeDefs = doc.getNodeDefs();
expect(nodeDefs.length).to.equal(1);
expect(nodeDefs[0]).to.eql(simpleSrf);
simpleSrf.setInputValueColor3('diffColor', new mx.Color3(1.0, 1.0, 1.0));
let inputValue = simpleSrf.getInputValue('diffColor');
expect(inputValue).to.not.be.null;
expect(inputValue.getData()).to.eql(new mx.Color3(1.0, 1.0, 1.0));
simpleSrf.setInputValueColor3('specColor', new mx.Color3(0.0, 0.0, 0.0));
inputValue = simpleSrf.getInputValue('specColor');
expect(inputValue).to.not.be.null;
expect(inputValue.getData()).to.eql(new mx.Color3(0.0, 0.0, 0.0));
const roughness = simpleSrf.setInputValueFloat('roughness', 0.25);
inputValue = simpleSrf.getInputValue('roughness');
expect(inputValue).to.not.be.null;
expect(inputValue.getData()).to.equal(0.25);
// // Create a material that instantiates the shader.
// const material = doc.addMaterial();
// const materials = doc.getMaterials();
// expect(materials.length).to.equal(1);
// expect(materials[0]).to.eql(material);
// const refSimpleSrf = material.addShaderRef('SR_simpleSrf', 'simpleSrf');
// const shaderRefs = material.getShaderRefs();
// expect(shaderRefs.length).to.equal(1);
// expect(shaderRefs[0]).to.eql(refSimpleSrf);
// expect(shaderRefs[0].getName()).to.equal('SR_simpleSrf');
// // Bind roughness to a new value within this material.
// const bindInput = refSimpleSrf.addBindInput('roughness');
// const bindInputs = refSimpleSrf.getBindInputs();
// expect(bindInputs.length).to.equal(1);
// expect(bindInputs[0]).to.eql(bindInput);
// bindInput.setValuefloat(0.5);
// expect(bindInput.getValue()).to.not.be.null;
// expect(bindInput.getValue().getData()).to.equal(0.5);
// // Validate the value of roughness in the context of this material.
// expect(roughness.getBoundValue(material).getValueString()).to.equal('0.5');
// Cleanup wrappers
nodeDefs.forEach(nd => nd.delete());
output.delete();
image.delete();
nodeGraph.delete();
doc.delete();
});
it('Traversing a Document Tree', async () =>
{
const xmlStr = getMtlxStrings(
['standard_surface_greysphere_calibration.mtlx'],
'../../resources/Materials/Examples/StandardSurface'
)[0];
const mx = await Module();
// Read a document from disk.
const doc = mx.createDocument();
await mx.readFromXmlString(doc, xmlStr);
// Traverse the document tree in depth-first order.
const elements = doc.traverseTree();
let imageCount = 0;
for (let elem of elements)
{
if (elem.isANode('image'))
{
imageCount++;
}
}
expect(imageCount).to.greaterThan(0);
doc.delete();
});
it('Building a MaterialX Document', async () =>
{
const xmlStr = getMtlxStrings(['standard_surface_marble_solid.mtlx'], '../../resources/Materials/Examples/StandardSurface')[0];
const mx = await Module();
// Read a document from disk.
const doc = mx.createDocument();
await mx.readFromXmlString(doc, xmlStr);
// let materialCount = 0;
// let shaderInputCount = 0;
// // Iterate through 1.37 materials for which there should be none
// const materials = doc.getMaterials();
// materials.forEach((material) => {
// materialCount++;
// // For each shader input, find all upstream images in the dataflow graph.
// const primaryShaderInputs = material.getPrimaryShaderInputs();
// primaryShaderInputs.forEach((input) => {
// const graphIter = input.traverseGraph(material);
// let edge = graphIter.next();
// while (edge) {
// shaderInputCount++;
// edge = graphIter.next();
// }
// });
// });
// expect(materialCount).to.equal(0);
// expect(shaderInputCount).to.equal(0);
doc.delete();
});
});
@@ -0,0 +1,250 @@
import { expect } from 'chai';
import Module from './_build/JsMaterialXCore.js';
import { getMtlxStrings } from './testHelpers';
describe('Custom Bindings', () =>
{
const examplesPath = '../../resources/Materials/Examples/StandardSurface';
let mx;
before(async () =>
{
mx = await Module();
});
it('Optional parameters work as expected', () =>
{
const doc = mx.createDocument();
// Call a method without optional argument
const nodeGraph = doc.addNodeGraph();
expect(nodeGraph).to.be.instanceof(mx.NodeGraph);
expect(nodeGraph.getName()).to.equal('nodegraph1'); // Auto-constructed default value
// Call a method with optional argument
const nodeGraph2 = doc.addNodeGraph('myGraph');
expect(nodeGraph2).to.be.instanceof(mx.NodeGraph);
expect(nodeGraph2.getName()).to.equal('myGraph');
// Call a method that requires at least one parameter
const node = nodeGraph.addNode('node');
expect(node).to.be.instanceof(mx.Node);
// Omitting non-optional parameter should throw
expect(() => { nodeGraph.addNode(); }).to.throw;
// Cleanup
node.delete();
nodeGraph2.delete();
nodeGraph.delete();
doc.delete();
});
it('Vector <-> Array conversion', () =>
{
// Functions that return vectors in C++ should return an array in JS
const doc = mx.createDocument();
const nodeGraph = doc.addNodeGraph();
const nodeGraphB = doc.addNodeGraph();
const nodeGraphs = doc.getNodeGraphs();
expect(nodeGraphs).to.be.an.instanceof(Array);
expect(nodeGraphs.length).to.equal(2);
// Elements fetched through the vector -> array conversion should be editable and changes should be reflected
// in the original objects.
// Note: We cannot simply compare these objects for equality, since they're separately constructed pointers
// to the same object.
const backdrop = nodeGraph.addBackdrop();
const backDrops = nodeGraphs[0].getBackdrops();
expect(backDrops.length).to.equal(1);
nodeGraphs[0].addBackdrop();
expect(nodeGraph.getBackdrops().length).to.equal(2);
// Functions that expect vectors as parameters in C++ should accept arrays in JS
// Built-in types (strings)
const pathSegments = ['path', 'to', 'something'];
const namePath = mx.createNamePath(pathSegments);
expect(namePath).to.equal(pathSegments.join(mx.NAME_PATH_SEPARATOR));
// Complex (smart pointer) types
const node1 = nodeGraph.addNode('node');
const node2 = nodeGraph.addNode('node');
const node3 = nodeGraph.addNode('node', 'anotherNode');
backdrop.setContainsElements([node1, node2, node3]);
const nodes = backdrop.getContainsElements();
expect(nodes.length).to.equal(3);
expect(nodes[0].getName()).to.equal('node1'); // Name auto-constructed from category
expect(nodes[1].getName()).to.equal('node2'); // Name auto-constructed from category
expect(nodes[2].getName()).to.equal('anotherNode'); // Name set explicitly at creation time
// Cleanup created wrappers
nodes.forEach(n => n.delete());
backdrop.delete();
node3.delete();
node2.delete();
node1.delete();
nodeGraphB.delete();
nodeGraph.delete();
doc.delete();
});
it('C++ exception handling', () =>
{
// Exceptions that are thrown and caught in C++ shouldn't bubble up to JS
const doc = mx.createDocument();
const nodeGraph1 = doc.addNodeGraph();
const nodeGraph2 = doc.addNodeGraph();
nodeGraph1.setInheritsFrom(nodeGraph2);
nodeGraph2.setInheritsFrom(nodeGraph1);
expect(nodeGraph1.hasInheritanceCycle()).to.not.throw;
expect(nodeGraph1.hasInheritanceCycle()).to.be.true;
// Exceptions that are not caught in C++ should throw
nodeGraph1.addNode('node', 'node1');
expect(() => { nodeGraph1.addNode('node', 'node1'); }).to.throw;
try
{
nodeGraph1.addNode('node', 'node1');
} catch (err)
{
expect(mx.getExceptionMessage(err)).to.be.a('string');
}
// Cleanup
nodeGraph2.delete();
nodeGraph1.delete();
doc.delete();
});
it('getReferencedSourceUris', async () =>
{
const doc = mx.createDocument();
const filename = 'standard_surface_look_brass_tiled.mtlx';
await mx.readFromXmlFile(doc, filename, examplesPath);
const sourceUris = doc.getReferencedSourceUris();
expect(sourceUris).to.be.instanceof(Array);
expect(sourceUris.length).to.equal(3);
expect(sourceUris[0]).to.be.a('string');
expect(sourceUris.includes('standard_surface_brass_tiled.mtlx')).to.be.true;
doc.delete();
});
it('Should invoke correct instance of \'validate\'', () =>
{
// We check whether the correct function is called by provoking an error message that is specific to the
// function that we expect to be called.
const message = {};
// Should invoke Document::validate.
const doc = mx.createDocument();
expect(doc.validate()).to.be.true;
doc.removeAttribute(mx.InterfaceElement.VERSION_ATTRIBUTE);
expect(doc.validate()).to.be.true;
// Should invoke Node::validate
const node = doc.addNode('node');
expect(node.validate()).to.be.true;
node.setCategory('');
expect(node.validate()).to.be.false;
expect(node.validate(message)).to.be.false;
expect(message.message).to.include('Node element is missing a category');
// Should invoke inherited ValueElement::validate
const token = new mx.Token(node, 'token');
expect(token.validate()).to.be.true;
token.setUnitType('bogus');
expect(token.validate()).to.be.false;
expect(token.validate(message)).to.be.false;
expect(message.message).to.include('Unit type definition does not exist in document')
// Cleanup
token.delete();
node.delete();
doc.delete();
});
it('StringResolver name substitution getters', () =>
{
const fnTestData = {
fnKey: 'fnValue',
fnKey1: 'fnValue1'
};
const fnTestKeys = Object.keys(fnTestData);
const gnTestData = {
gnKey: 'gnValue',
gnKey1: 'gnValue1'
};
const gnTestKeys = Object.keys(gnTestData);
const resolver = mx.StringResolver.create();
resolver.setFilenameSubstitution(fnTestKeys[0], fnTestData[fnTestKeys[0]]);
resolver.setFilenameSubstitution(fnTestKeys[1], fnTestData[fnTestKeys[1]]);
const fnSubs = resolver.getFilenameSubstitutions();
expect(fnSubs).to.be.instanceof(Object);
expect(Object.keys(fnSubs).length).to.equal(2);
expect(fnSubs).to.deep.equal(fnTestData);
resolver.setGeomNameSubstitution(gnTestKeys[0], gnTestData[gnTestKeys[0]]);
resolver.setGeomNameSubstitution(gnTestKeys[1], gnTestData[gnTestKeys[1]]);
const gnSubs = resolver.getGeomNameSubstitutions();
expect(gnSubs).to.be.instanceof(Object);
expect(Object.keys(gnSubs).length).to.equal(2);
expect(gnSubs).to.deep.equal(gnTestData);
resolver.delete();
});
it('getShaderNodes', async () =>
{
const doc = mx.createDocument();
const fileNames = ['standard_surface_marble_solid.mtlx'];
const mtlxStrs = getMtlxStrings(fileNames, examplesPath);
await mx.readFromXmlString(doc, mtlxStrs[0]);
let matNodes = doc.getMaterialNodes();
expect(matNodes.length).to.equal(1);
const matNode = matNodes[0];
// Should return a surface shader node but no displacement shader node
let shaderNodes = mx.getShaderNodes(matNode);
expect(shaderNodes).to.be.instanceof(Array);
expect(shaderNodes.length).to.equal(1);
expect(shaderNodes[0].getType()).to.equal(mx.SURFACE_SHADER_TYPE_STRING);
shaderNodes = mx.getShaderNodes(matNode, mx.DISPLACEMENT_SHADER_TYPE_STRING);
expect(shaderNodes).to.be.instanceof(Array);
expect(shaderNodes.length).to.equal(0);
// Cleanup wrappers
shaderNodes.forEach(s => s.delete());
matNodes.forEach(n => n.delete());
doc.delete();
});
it('createValidName', () =>
{
const testString = '_Note_:Please,turn.this+-into*1#valid\nname for_me';
const replaceRegex = /[^a-zA-Z0-9_:]/g
expect(mx.createValidName(testString)).to.equal(testString.replace(replaceRegex, '_'));
expect(mx.createValidName(testString, '-')).to.equal(testString.replace(replaceRegex, '-'));
});
it('getVersionIntegers', () =>
{
const versionStringArr = mx.getVersionString().split('.').map((value) => parseInt(value, 10));
// Global getVersionIntegers
const globalVersion = mx.getVersionIntegers();
expect(globalVersion).to.be.instanceof(Array);
expect(globalVersion.length).to.equal(3);
expect(globalVersion).to.deep.equal(versionStringArr);
// Document.getVersionIntegers
versionStringArr.pop();
const doc = mx.createDocument();
const docVersion = doc.getVersionIntegers();
expect(docVersion).to.be.instanceof(Array);
expect(docVersion.length).to.equal(2);
expect(docVersion).to.deep.equal(versionStringArr);
doc.delete();
// InterfaceElement.getVersionIntegers (via NodeDef)
// TODO: This function can currently not be called, since we have a linker issue that messes up this function.
});
});
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="cycle.mtlx" />
</materialx>
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="../folder2/include3.mtlx" />
</materialx>
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<simple_srf name="sr_ps" type="surfaceshader">
<input name="specColor" type="color3" value="0.05, 0.05, 0.05" />
<input name="specRoughness" type="float" value="0.14" />
</simple_srf>
<surfacematerial name="paint_semigloss" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="sr_ps" />
</surfacematerial>
</materialx>
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="folder2/include3.mtlx" />
</materialx>
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="include2.mtlx" />
</materialx>
@@ -0,0 +1,5 @@
<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="include1.mtlx" />
<xi:include href="folder/include2.mtlx" />
</materialx>
@@ -0,0 +1,188 @@
import { expect } from 'chai';
import Module from './_build/JsMaterialXCore.js';
describe('Document', () =>
{
let mx, doc;
before(async () =>
{
mx = await Module();
// Create a document.
doc = mx.createDocument();
});
function expectError(type, cb)
{
try
{
cb();
throw new Error('Expected function to throw!');
} catch (exceptionPtr)
{
const message = mx.getExceptionMessage(exceptionPtr);
expect(message.indexOf(type) !== -1).to.be.true;
}
}
let nodeGraph;
it('Build document', () =>
{
// Create a node graph with constant and image sources.
nodeGraph = doc.addNodeGraph();
expect(nodeGraph).to.exist;
expectError('Child name is not unique: nodegraph1', () =>
{
doc.addNodeGraph(nodeGraph.getName());
});
const constant = nodeGraph.addNode('constant');
const image = nodeGraph.addNode('image');
// Connect sources to outputs
const output1 = nodeGraph.addOutput();
const output2 = nodeGraph.addOutput();
output1.setConnectedNode(constant);
output2.setConnectedNode(image);
expect(output1.getConnectedNode()).to.eql(constant);
expect(output2.getConnectedNode()).to.eql(image);
expect(output1.getUpstreamElement()).to.eql(constant);
expect(output2.getUpstreamElement()).to.eql(image);
// Set constant node color
const color = new mx.Color3(0.1, 0.2, 0.3);
constant.setInputValueColor3('value', color);
expect(constant.getInputValue('value').getData()).to.eql(color);
// Set image node file
const file = 'image1.tif';
image.setInputValueString('file', file, 'filename');
expect(image.getInputValue('file').getData()).to.eql(file);
// Create a custom nodedef
const nodeDef = doc.addNodeDef('nodeDef1', 'float', 'turbulence3d');
nodeDef.setInputValueInteger('octaves', 3);
nodeDef.setInputValueFloat('lacunarity', 2.0);
nodeDef.setInputValueFloat('gain', 0.5);
// Reference the custom nodedef
const custom = nodeGraph.addNode('turbulence3d', 'turbulence1', 'float');
expect(custom.getInputValue('octaves').getData()).to.equal(3);
custom.setInputValueInteger('octaves', 5);
expect(custom.getInputValue('octaves').getData()).to.equal(5);
// Test scoped attributes
nodeGraph.setFilePrefix('folder/');
nodeGraph.setColorSpace('lin_rec709');
expect(image.getInput('file').getResolvedValueString()).to.equal('folder/image1.tif');
expect(constant.getActiveColorSpace()).to.equal('lin_rec709');
// Create a simple shader interface
const simpleSrf = doc.addNodeDef('', 'surfaceshader', 'simpleSrf');
simpleSrf.setInputValueColor3('diffColor', new mx.Color3(1.0, 1.0, 1.0));
simpleSrf.setInputValueColor3('specColor', new mx.Color3(0.0, 0.0, 0.0));
const roughness = simpleSrf.setInputValueFloat('roughness', 0.25);
expect(roughness.getIsUniform()).to.be.false;
roughness.setIsUniform(true);
expect(roughness.getIsUniform()).to.be.true;
// Instantiate shader and material nodes
const shaderNode = doc.addNodeInstance(simpleSrf);
const materialNode = doc.addMaterialNode('', shaderNode);
expect(materialNode.getUpstreamElement().equals(shaderNode)).to.be.true;
// Bind the diffuse color input to the constant color output
shaderNode.setConnectedOutput('diffColor', output1);
expect(shaderNode.getUpstreamElement().equals(constant)).to.be.true;
// Bind the roughness input to a value
const instanceRoughness = shaderNode.setInputValueFloat('roughness', 0.5);
expect(instanceRoughness.getValue().getData()).to.equal(0.5);
expect(instanceRoughness.getDefaultValue().getData()).to.equal(0.25);
// Create a look for the material
const look = doc.addLook();
expect(doc.getLooks().length).to.equal(1);
// Bind the material to a geometry string
let matAssign1 = look.addMaterialAssign('matAssign1', materialNode.getName());
matAssign1 = look.getMaterialAssign('matAssign1');
expect(matAssign1);
matAssign1.setGeom('/robot1');
expect(matAssign1.getReferencedMaterial().equals(materialNode)).to.be.true;
expect(mx.getGeometryBindings(materialNode, '/robot1').length).to.equal(1);
expect(mx.getGeometryBindings(materialNode, '/robot2').length).to.equal(0);
// Bind the material to a collection
let matAssign2 = look.addMaterialAssign('matAssign2', materialNode.getName());
matAssign2 = look.getMaterialAssign('matAssign1');
expect(matAssign2);
const collection = doc.addCollection();
collection.setIncludeGeom('/robot2');
collection.setExcludeGeom('/robot2/left_arm');
matAssign2.setCollection(collection);
expect(matAssign2.getReferencedMaterial().equals(materialNode)).to.be.true;
expect(mx.getGeometryBindings(materialNode, '/robot2').length).to.equal(1);
expect(mx.getGeometryBindings(materialNode, '/robot2/right_arm').length).to.equal(1);
expect(mx.getGeometryBindings(materialNode, '/robot2/left_arm').length).to.equal(0);
const materialAssigns = look.getMaterialAssigns();
expect(materialAssigns.length).to.equal(2);
// Create a property assignment
const propertyAssign = look.addPropertyAssign();
propertyAssign.setProperty('twosided');
propertyAssign.setGeom('/robot1');
propertyAssign.setValueBoolean(true);
expect(propertyAssign.getProperty()).to.equal('twosided');
expect(propertyAssign.getGeom()).to.equal('/robot1');
expect(propertyAssign.getValue().getData()).to.equal(true);
let propertyAssigns = look.getPropertyAssigns();
expect(propertyAssigns.length).to.equal(1);
// Create a property set assignment
const propertySet = doc.addPropertySet();
propertySet.setPropertyValueBoolean('matte', false);
expect(propertySet.getPropertyValue('matte').getData()).to.equal(false);
const propertySetAssign = look.addPropertySetAssign();
propertySetAssign.setPropertySet(propertySet);
propertySetAssign.setGeom('/robot1');
expect(propertySetAssign.getPropertySet().equals(propertySet)).to.be.true;
expect(propertySetAssign.getGeom()).to.equal('/robot1');
// Create a variant set
const variantSet = doc.addVariantSet();
variantSet.addVariant('original');
variantSet.addVariant('damaged');
expect(variantSet.getVariants().length).to.equal(2);
// Validate the document
expect(doc.validate()).to.be.true;
// Disconnect output from sources
output1.setConnectedNode(null);
output2.setConnectedNode(null);
expect(output1.getConnectedNode()).to.equal(null);
expect(output2.getConnectedNode()).to.equal(null);
// Cleanup created wrappers
propertySetAssign.delete();
propertySet.delete();
propertyAssign.delete();
variantSet.delete();
collection.delete();
matAssign2.delete();
matAssign1.delete();
look.delete();
instanceRoughness.delete();
shaderNode.delete();
materialNode.delete();
simpleSrf.delete();
output2.delete();
output1.delete();
custom.delete();
color.delete();
image.delete();
constant.delete();
nodeDef.delete();
nodeGraph.delete();
doc.delete();
});
});
@@ -0,0 +1,201 @@
import { expect } from 'chai';
import Module from './_build/JsMaterialXCore.js';
describe('Element', () =>
{
let mx, doc, valueTypes;
const primitiveValueTypes = {
Integer: 10,
Boolean: true,
String: 'test',
Float: 15,
IntegerArray: [1, 2, 3, 4, 5],
FloatArray: [12, 14], // Not using actual floats to avoid precision problems
StringArray: ['first', 'second'],
BooleanArray: [true, true, false],
}
before(async () =>
{
mx = await Module();
doc = mx.createDocument();
valueTypes = {
Color3: new mx.Color3(1, 0, 0.5),
Color4: new mx.Color4(0, 1, 0.5, 1),
Vector2: new mx.Vector2(0, 1),
Vector3: new mx.Vector3(0, 1, 2),
Vector4: new mx.Vector4(0, 1, 2, 1),
Matrix33: new mx.Matrix33(0, 1, 2, 3, 4, 5, 6, 7, 8),
Matrix44: new mx.Matrix44(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
};
});
after(() =>
{
// Cleanup typed helper objects and document
Object.values(valueTypes).forEach(v => v.delete());
doc.delete();
});
describe('value setters', () =>
{
const checkValue = (types, assertionCallback) =>
{
const elem = doc.addChildOfCategory('geomprop');
Object.keys(types).forEach((typeName) =>
{
const setFn = `setValue${typeName}`;
elem[setFn](types[typeName]);
assertionCallback(elem.getValue().getData(), typeName);
});
elem.delete();
};
it('should work with expected type', () =>
{
checkValue(valueTypes, (returnedValue, typeName) =>
{
expect(returnedValue).to.be.an.instanceof(mx[`${typeName}`]);
expect(returnedValue.equals(valueTypes[typeName])).to.equal(true);
});
});
it('should work with expected primitive type', () =>
{
checkValue(primitiveValueTypes, (returnedValue, typeName) =>
{
expect(returnedValue).to.eql(primitiveValueTypes[typeName]);
});
});
it('should fail for incorrect type', () =>
{
const elem = doc.addChildOfCategory('geomprop');
expect(() => elem.Matrix33(true)).to.throw();
});
});
describe('typed value setters', () =>
{
const checkTypes = (types, assertionCallback) =>
{
const elem = doc.addChildOfCategory('geomprop');
Object.keys(types).forEach((typeName) =>
{
const setFn = `setTypedAttribute${typeName}`;
const getFn = `getTypedAttribute${typeName}`;
elem[setFn](typeName, types[typeName]);
assertionCallback(elem[getFn](typeName), types[typeName]);
});
elem.delete();
};
it('should work with expected custom type', () =>
{
checkTypes(valueTypes, (returnedValue, originalValue) =>
{
expect(returnedValue.equals(originalValue)).to.equal(true);
});
});
it('should work with expected primitive type', () =>
{
checkTypes(primitiveValueTypes, (returnedValue, originalValue) =>
{
expect(returnedValue).to.eql(originalValue);
});
});
it('should fail for incorrect type', () =>
{
const elem = doc.addChildOfCategory('geomprop');
expect(() => elem.setTypedAttributeColor3('wrongType', true)).to.throw();
});
});
it('factory invocation should match specialized functions', () =>
{
// List based in source/MaterialXCore/Element.cpp
const elemtypeArr = [
'Backdrop',
'Collection',
'GeomInfo',
'MaterialAssign',
'PropertySetAssign',
'Visibility',
'GeomPropDef',
'Look',
'LookGroup',
'PropertySet',
'TypeDef',
'AttributeDef',
'NodeGraph',
'Implementation',
'Node',
'NodeDef',
'Variant',
'Member',
'TargetDef',
'GeomProp',
'Input',
'Output',
'Property',
'PropertyAssign',
'Unit',
'UnitDef',
'UnitTypeDef',
'VariantAssign',
'VariantSet',
];
elemtypeArr.forEach((typeName) =>
{
const specializedFn = `addChild${typeName}`;
const factoryName = typeName.toLowerCase();
const type = mx[typeName];
expect(doc[specializedFn]()).to.be.an.instanceof(type);
expect(doc.addChildOfCategory(factoryName)).to.be.an.instanceof(type);
});
const specialElemType = {
'MaterialX': mx.Document,
'Comment': mx.CommentElement,
'Generic': mx.GenericElement,
};
Object.keys(specialElemType).forEach((typeName) =>
{
const specializedFn = `addChild${typeName}`;
const factoryName = typeName.toLowerCase();
expect(doc[specializedFn]()).to.be.an.instanceof(specialElemType[typeName]);
expect(doc.addChildOfCategory(factoryName)).to.be.an.instanceof(specialElemType[typeName]);
});
// No doc.delete() here; cleaned up in after()
});
});
describe('Equivalence', () =>
{
let mx, doc, doc2
before(async () => {
mx = await Module();
doc = mx.createDocument();
doc.addNodeGraph("graph");
doc2 = mx.createDocument();
doc2.addNodeGraph("graph1");
});
it('Compare document equivalency', () =>
{
let options = new mx.ElementEquivalenceOptions();
let differences = {};
options.performValueComparisons = false;
let result = doc.isEquivalent(doc2, options, differences);
expect(result).to.be.false;
expect(differences.message).to.not.be.empty;
result = doc.isEquivalent(doc2, options, undefined);
expect(result).to.be.false;
});
});
@@ -0,0 +1,20 @@
import { expect } from 'chai';;
import Module from './_build/JsMaterialXCore.js';
describe('Environ', () =>
{
let mx;
before(async () =>
{
mx = await Module();
});
it('Environment variables', () =>
{
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('');
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, 'test');
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('test');
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('');
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
{
"name": "MaterialXTest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"clean": "rimraf ./_build/",
"copyBuild": "copyfiles -f ../build/bin/JsMaterialX* _build",
"pretest": "npm run clean && npm run copyBuild",
"test": "npm run mocha",
"test:browser": "npm run karma -- --browsers ChromeHeadlessGL --singleRun true",
"mocha": "mocha *.spec.js --require @babel/register --timeout 5000",
"karma": "karma start browser/karma.conf.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@babel/register": "^7.28.3",
"chai": "^4.5.0",
"copyfiles": "^2.4.1",
"karma": "^6.4.4",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.2.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"mocha": "^11.7.5",
"rimraf": "^3.0.2"
}
}
@@ -0,0 +1,14 @@
var fs = require('fs');
var path = require('path');
export function getMtlxStrings(fileNames, subPath)
{
const mtlxStrs = [];
for (let i = 0; i < fileNames.length; i++)
{
const p = path.resolve(subPath, fileNames[parseInt(i, 10)]);
const t = fs.readFileSync(p, 'utf8');
mtlxStrs.push(t);
}
return mtlxStrs;
}
@@ -0,0 +1,236 @@
import { expect } from 'chai';
import Module from './_build/JsMaterialXCore.js';
describe('Traversal', () =>
{
let mx;
before(async () =>
{
mx = await Module();
});
it('Traverse Graph', () =>
{
// Create a document.
const doc = mx.createDocument();
// Create a node graph with the following structure:
//
// [image1] [constant] [image2]
// \ / |
// [multiply] [contrast] [noise3d]
// \____________ | ____________/
// [mix]
// |
// [output]
//
const nodeGraph = doc.addNodeGraph();
const image1 = nodeGraph.addNode('image');
const image2 = nodeGraph.addNode('image');
const constant = nodeGraph.addNode('constant');
const multiply = nodeGraph.addNode('multiply');
const contrast = nodeGraph.addNode('contrast');
const noise3d = nodeGraph.addNode('noise3d');
const mix = nodeGraph.addNode('mix');
const output = nodeGraph.addOutput();
multiply.setConnectedNode('in1', image1);
multiply.setConnectedNode('in2', constant);
contrast.setConnectedNode('in', image2);
mix.setConnectedNode('fg', multiply);
mix.setConnectedNode('bg', contrast);
mix.setConnectedNode('mask', noise3d);
output.setConnectedNode(mix);
expect(doc.validate()).to.be.true;
// Traverse the document tree (implicit iterator).
let nodeCount = 0;
for (let elem of doc.traverseTree())
{
if (elem instanceof mx.Node)
{
nodeCount++;
}
elem.delete();
}
expect(nodeCount).to.equal(7);
// Traverse the document tree (explicit iterator)
let treeIter = doc.traverseTree();
nodeCount = 0;
let maxElementDepth = 0;
for (let elem of treeIter)
{
if (elem instanceof mx.Node)
{
nodeCount++;
}
maxElementDepth = Math.max(maxElementDepth, treeIter.getElementDepth());
elem.delete();
}
expect(nodeCount).to.equal(7);
expect(maxElementDepth).to.equal(3);
// Traverse the document tree (prune subtree).
nodeCount = 0;
treeIter = doc.traverseTree();
for (let elem of treeIter)
{
if (elem instanceof mx.Node)
{
nodeCount++;
}
if (elem instanceof mx.NodeGraph)
{
treeIter.setPruneSubtree(true);
}
elem.delete();
}
expect(nodeCount).to.equal(0);
// Traverse upstream from the graph output (implicit iterator)
nodeCount = 0;
for (let edge of output.traverseGraph())
{
const upstreamElem = edge.getUpstreamElement();
const connectingElem = edge.getConnectingElement();
const downstreamElem = edge.getDownstreamElement();
if (upstreamElem instanceof mx.Node)
{
nodeCount++;
if (downstreamElem instanceof mx.Node)
{
expect(connectingElem instanceof mx.Input).to.be.true;
}
}
if (upstreamElem) upstreamElem.delete();
if (connectingElem) connectingElem.delete();
if (downstreamElem) downstreamElem.delete();
if (edge) edge.delete();
}
expect(nodeCount).to.equal(7);
// Traverse upstream from the graph output (explicit iterator)
nodeCount = 0;
maxElementDepth = 0;
let maxNodeDepth = 0;
let graphIter = output.traverseGraph();
for (let edge of graphIter)
{
const upstreamElem = edge.getUpstreamElement();
if (upstreamElem instanceof mx.Node)
{
nodeCount++;
}
maxElementDepth = Math.max(maxElementDepth, graphIter.getElementDepth());
maxNodeDepth = Math.max(maxNodeDepth, graphIter.getNodeDepth());
if (upstreamElem) upstreamElem.delete();
if (edge) edge.delete();
}
expect(nodeCount).to.equal(7);
expect(maxElementDepth).to.equal(3);
expect(maxNodeDepth).to.equal(3);
// Traverse upstream from the graph output (prune subgraph)
nodeCount = 0;
graphIter = output.traverseGraph();
for (let edge of graphIter)
{
const upstreamElem = edge.getUpstreamElement();
expect(upstreamElem.getSelf()).to.be.an.instanceof(mx.Element);
if (upstreamElem instanceof mx.Node)
{
nodeCount++;
}
if (upstreamElem.getCategory() === 'multiply')
{
graphIter.setPruneSubgraph(true);
}
if (upstreamElem) upstreamElem.delete();
if (edge) edge.delete();
}
expect(nodeCount).to.equal(5);
// Create and detect a cycle
multiply.setConnectedNode('in2', mix);
expect(output.hasUpstreamCycle()).to.be.true;
expect(doc.validate()).to.be.false;
multiply.setConnectedNode('in2', constant);
expect(output.hasUpstreamCycle()).to.be.false;
expect(doc.validate()).to.be.true;
// Create and detect a loop
contrast.setConnectedNode('in', contrast);
expect(output.hasUpstreamCycle()).to.be.true;
expect(doc.validate()).to.be.false;
contrast.setConnectedNode('in', image2);
expect(output.hasUpstreamCycle()).to.be.false;
expect(doc.validate()).to.be.true;
// Cleanup wrappers
output.delete();
mix.delete();
noise3d.delete();
contrast.delete();
multiply.delete();
constant.delete();
image2.delete();
image1.delete();
nodeGraph.delete();
doc.delete();
});
describe("Traverse inheritance", () =>
{
let nodeDefInheritanceLevel2, nodeDefInheritanceLevel1, nodeDefParent;
let doc;
beforeEach(() =>
{
doc = mx.createDocument();
nodeDefParent = doc.addNodeDef();
nodeDefParent.setName('BaseClass');
nodeDefInheritanceLevel1 = doc.addNodeDef();
nodeDefInheritanceLevel1.setName('InheritanceLevel1');
nodeDefInheritanceLevel2 = doc.addNodeDef();
nodeDefInheritanceLevel2.setName('InheritanceLevel2');
nodeDefInheritanceLevel2.setInheritsFrom(nodeDefInheritanceLevel1);
nodeDefInheritanceLevel1.setInheritsFrom(nodeDefParent);
});
afterEach(() =>
{
nodeDefInheritanceLevel2.delete();
nodeDefInheritanceLevel1.delete();
nodeDefParent.delete();
doc.delete();
});
it('for of loop', () =>
{
const inheritanceIterator = nodeDefInheritanceLevel2.traverseInheritance();
let inheritanceChainLength = 0;
for (const elem of inheritanceIterator)
{
if (elem instanceof mx.NodeDef)
{
inheritanceChainLength++;
}
}
expect(inheritanceChainLength).to.equal(2);;
});
it('while loop', () =>
{
const inheritanceIterator = nodeDefInheritanceLevel2.traverseInheritance();
let inheritanceChainLength = 0;
let elem = inheritanceIterator.next();
while (!elem.done)
{
if (elem.value instanceof mx.NodeDef)
{
inheritanceChainLength++;
}
elem = inheritanceIterator.next();
}
expect(inheritanceChainLength).to.equal(2);;
});
});
});
@@ -0,0 +1,178 @@
import { expect } from 'chai';;
import Module from './_build/JsMaterialXCore.js';
describe('Types', () =>
{
let mx;
before(async () =>
{
mx = await Module();
});
it('Vectors', () =>
{
const v1 = new mx.Vector3(1, 2, 3);
let v2 = new mx.Vector3(2, 4, 6);
// Indexing operators
expect(v1.getItem(2)).to.equal(3);
v1.setItem(2, 4);
expect(v1.getItem(2)).to.equal(4);
v1.setItem(2, 3);
// Component-wise operators
let res = v2.add(v1);
expect(res.equals(new mx.Vector3(3, 6, 9))).to.be.true;
res = v2.sub(v1);
expect(res.equals(new mx.Vector3(1, 2, 3))).to.be.true;
res = v2.multiply(v1);
expect(res.equals(new mx.Vector3(2, 8, 18))).to.be.true;
res = v2.divide(v1);
expect(res.equals(new mx.Vector3(2, 2, 2))).to.be.true;
v2 = v2.add(v1);
expect(v2.equals(new mx.Vector3(3, 6, 9))).to.be.true;
v2 = v2.sub(v1);
expect(v2.equals(new mx.Vector3(2, 4, 6))).to.be.true;
v2 = v2.multiply(v1);
expect(v2.equals(new mx.Vector3(2, 8, 18))).to.be.true;
v2 = v2.divide(v1);
expect(v2.equals(new mx.Vector3(2, 4, 6))).to.be.true;
expect(v1.multiply(new mx.Vector3(2, 2, 2)).equals(v2)).to.be.true;
expect(v2.divide(new mx.Vector3(2, 2, 2)).equals(v1)).to.be.true;
// Geometric methods
let v3 = new mx.Vector4(4, 4, 4, 4);
expect(v3.getMagnitude()).to.equal(8);
expect(v3.getNormalized().getMagnitude()).to.equal(1);
expect(v1.dot(v2)).to.equal(28);
expect(v1.cross(v2).equals(new mx.Vector3())).to.be.true;
// Vector copy
const v4 = v2.copy();
expect(v4.equals(v2)).to.be.true;
v4.setItem(0, v4.getItem(0) + 1);
expect(v4.notEquals(v2)).to.be.true;
});
function multiplyMatrix(matrix, val)
{
const clonedMatrix = matrix.copy();
for (let i = 0; i < clonedMatrix.numRows(); ++i)
{
for (let k = 0; k < clonedMatrix.numColumns(); ++k)
{
const v = clonedMatrix.getItem(i, k);
clonedMatrix.setItem(i, k, v * val);
}
}
return clonedMatrix;
}
function divideMatrix(matrix, val)
{
const clonedMatrix = matrix.copy();
for (let i = 0; i < clonedMatrix.numRows(); ++i)
{
for (let k = 0; k < clonedMatrix.numColumns(); ++k)
{
const v = clonedMatrix.getItem(i, k);
clonedMatrix.setItem(i, k, v / val);
}
}
return clonedMatrix;
}
it('Matrices', () =>
{
// Translation and scale
const trans = mx.Matrix44.createTranslation(new mx.Vector3(1, 2, 3));
const scale = mx.Matrix44.createScale(new mx.Vector3(2, 2, 2));
expect(trans.equals(new mx.Matrix44(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1, 2, 3, 1)));
expect(scale.equals(new mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
0, 0, 0, 1)));
// Indexing operators
expect(trans.getItem(3, 2)).to.equal(3);
trans.setItem(3, 2, 4);
expect(trans.getItem(3, 2)).to.equal(4);
trans.setItem(3, 2, 3);
// Matrix methods
expect(trans.getTranspose().equals(
new mx.Matrix44(1, 0, 0, 1,
0, 1, 0, 2,
0, 0, 1, 3,
0, 0, 0, 1)
)).to.be.true;
expect(scale.getTranspose().equals(scale)).to.be.true;
expect(trans.getDeterminant()).to.equal(1);
expect(scale.getDeterminant()).to.equal(8);
expect(trans.getInverse().equals(
mx.Matrix44.createTranslation(new mx.Vector3(-1, -2, -3)))).to.be.true;
// Matrix product
const prod1 = trans.multiply(scale);
const prod2 = scale.multiply(trans);
const prod3 = multiplyMatrix(trans, 2);
let prod4 = trans;
prod4 = prod4.multiply(scale);
expect(prod1.equals(new mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
2, 4, 6, 1)));
expect(prod2.equals(new mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
1, 2, 3, 1)));
expect(prod3.equals(new mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
2, 4, 6, 2)));
expect(prod4.equals(prod1));
// Matrix division
const quot1 = prod1.divide(scale);
const quot2 = prod2.divide(trans);
const quot3 = divideMatrix(prod3, 2);
let quot4 = quot1;
quot4 = quot4.divide(trans);
expect(quot1.equals(trans)).to.be.true;
expect(quot2.equals(scale)).to.be.true;
expect(quot3.equals(trans)).to.be.true;
// 2D rotation
const _epsilon = 1e-4;
const rot1 = mx.Matrix33.createRotation(Math.PI / 2);
const rot2 = mx.Matrix33.createRotation(Math.PI);
expect(rot1.multiply(rot1).isEquivalent(rot2, _epsilon));
expect(rot2.isEquivalent(mx.Matrix33.createScale(new mx.Vector2(-1, -1)), _epsilon));
expect(rot2.multiply(rot2).isEquivalent(mx.Matrix33.IDENTITY, _epsilon));
// 3D rotation
const rotX = mx.Matrix44.createRotationX(Math.PI);
const rotY = mx.Matrix44.createRotationY(Math.PI);
const rotZ = mx.Matrix44.createRotationZ(Math.PI);
expect(rotX.multiply(rotY).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(-1, -1, 1)), _epsilon));
expect(rotX.multiply(rotZ).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(-1, 1, -1)), _epsilon));
expect(rotY.multiply(rotZ).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(1, -1, -1)), _epsilon));
// Matrix copy
const trans2 = trans.copy();
expect(trans2.equals(trans)).to.be.true;
trans2.setItem(0, 0, trans2.getItem(0, 0) + 1);
expect(trans2.notEquals(trans)).to.be.true;
});
});
@@ -0,0 +1,42 @@
import { expect } from 'chai';;
import Module from './_build/JsMaterialXCore.js';
describe('Value', () =>
{
let mx;
before(async () =>
{
mx = await Module();
});
it('Create values of different types', () =>
{
const testValues = {
integer: '1',
boolean: 'true',
float: '1.1',
color3: '0.1, 0.2, 0.3',
color4: '0.1, 0.2, 0.3, 0.4',
vector2: '1.1, 2.1',
vector3: '1.1, 2.1, 3.1',
vector4: '1.1, 2.1, 3.1, 4.1',
matrix33: '0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1',
matrix44: '1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1',
string: 'value',
integerarray: '1, 2, 3',
booleanarray: 'false, true, false',
floatarray: '1.1, 2.1, 3.1',
stringarray: "'one', 'two', 'three'",
};
for (let type in testValues)
{
const value = testValues[String(type)];
const newValue = mx.Value.createValueFromStrings(value, type);
const typeString = newValue.getTypeString();
const valueString = newValue.getValueString();
expect(typeString).to.equal(type);
expect(valueString).to.equal(value);
}
});
});
@@ -0,0 +1,304 @@
import Module from './_build/JsMaterialXCore.js';
import { expect } from 'chai';
import { getMtlxStrings } from './testHelpers';
const TIMEOUT = 60000;
describe('XmlIo', () =>
{
let mx;
// These should be relative to cwd
const includeTestPath = 'data/includes';
const libraryPath = '../../libraries/stdlib';
const examplesPath = '../../resources/Materials/Examples';
// TODO: Is there a better way to get these filenames than hardcoding them here?
// The C++ tests load all files in the given directories. This would work in Node, but not in the browser.
// Should we use a pre-test script that fetches the files and makes them available somehow?
const libraryFilenames = ['stdlib_defs.mtlx', 'stdlib_ng.mtlx'];
const exampleFilenames = [
'StandardSurface/standard_surface_brass_tiled.mtlx',
'StandardSurface/standard_surface_brick_procedural.mtlx',
'StandardSurface/standard_surface_carpaint.mtlx',
'StandardSurface/standard_surface_marble_solid.mtlx',
'UsdPreviewSurface/usd_preview_surface_gold.mtlx',
'UsdPreviewSurface/usd_preview_surface_plastic.mtlx',
];
async function readStdLibrary(asString = false)
{
const libs = [];
let iterable = libraryFilenames;
if (asString)
{
const libraryMtlxStrings = getMtlxStrings(libraryFilenames, libraryPath);
iterable = libraryMtlxStrings;
}
for (let file of iterable)
{
const lib = mx.createDocument();
if (asString)
{
await mx.readFromXmlString(lib, file, libraryPath);
} else
{
await mx.readFromXmlFile(lib, file, libraryPath);
}
libs.push(lib);
};
return libs;
}
async function readAndValidateExamples(examples, libraries, readFunc, searchPath = undefined)
{
for (let file of examples)
{
const doc = mx.createDocument();
await readFunc(doc, file, searchPath);
// Import stdlib into the current document and validate it.
for (let lib of libraries)
{
doc.importLibrary(lib);
}
expect(doc.validate()).to.be.true;
// Make sure the document does actually contain something.
let valueElementCount = 0;
const treeIter = doc.traverseTree();
for (const elem of treeIter)
{
if (elem instanceof mx.ValueElement)
{
valueElementCount++;
}
// Release wrapper created by iterator
elem.delete();
}
expect(valueElementCount).to.be.greaterThan(0);
doc.delete();
};
}
before(async () =>
{
mx = await Module();
});
it('Read XML from file', async () =>
{
// Read the standard library
const libs = await readStdLibrary(false);
// Read and validate the example documents.
await readAndValidateExamples(exampleFilenames, libs,
async (document, file, sp) =>
{
await mx.readFromXmlFile(document, file, sp);
}, examplesPath);
// Read the same document twice, and verify that duplicate elements
// are skipped.
const doc = mx.createDocument();
const filename = 'StandardSurface/standard_surface_carpaint.mtlx';
await mx.readFromXmlFile(doc, filename, examplesPath);
const copy = doc.copy();
await mx.readFromXmlFile(doc, filename, examplesPath);
expect(doc.validate()).to.be.true;
expect(copy.equals(doc)).to.be.true;
copy.delete();
doc.delete();
libs.forEach(l => l.delete());
}).timeout(TIMEOUT);
it('Read XML from string', async () =>
{
// Read the standard library
const libs = await readStdLibrary(true);
// Read and validate each example document.
const examplesStrings = getMtlxStrings(exampleFilenames, examplesPath);
await readAndValidateExamples(examplesStrings, libs,
async (document, file) =>
{
await mx.readFromXmlString(document, file);
});
// Read the same document twice, and verify that duplicate elements
// are skipped.
const doc = mx.createDocument();
const file = examplesStrings[exampleFilenames.indexOf('StandardSurface/standard_surface_carpaint.mtlx')];
await mx.readFromXmlString(doc, file);
const copy = doc.copy();
await mx.readFromXmlString(doc, file);
expect(doc.validate()).to.be.true;
expect(copy.equals(doc)).to.be.true;
copy.delete();
doc.delete();
libs.forEach(l => l.delete());
}).timeout(TIMEOUT);
it('Read XML with recursive includes', async () =>
{
const doc = mx.createDocument();
await mx.readFromXmlFile(doc, includeTestPath + '/root.mtlx');
expect(doc.getChild('paint_semigloss')).to.exist;
expect(doc.validate()).to.be.true;
doc.delete();
});
it('Locate XML includes via search path', async () =>
{
const searchPath = includeTestPath + ';' + includeTestPath + '/folder';
const filename = 'non_relative_includes.mtlx';
const doc = mx.createDocument();
expect(async () => await mx.readFromXmlFile(doc, filename, includeTestPath)).to.throw;
await mx.readFromXmlFile(doc, filename, searchPath);
expect(doc.getChild('paint_semigloss')).to.exist;
expect(doc.validate()).to.be.true;
const doc2 = mx.createDocument();
const mtlxString = getMtlxStrings([filename], includeTestPath);
expect(async () => await mx.readFromXmlString(doc2, mtlxString[0])).to.throw;
await mx.readFromXmlString(doc2, mtlxString[0], searchPath);
expect(doc2.getChild('paint_semigloss')).to.exist;
expect(doc2.validate()).to.be.true;
expect(doc2.equals(doc)).to.be.true;
doc2.delete();
doc.delete();
});
it('Locate XML includes via environment variable', async () =>
{
const searchPath = includeTestPath + ';' + includeTestPath + '/folder';
const filename = 'non_relative_includes.mtlx';
const doc = mx.createDocument();
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/' + filename)).to.throw;
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, searchPath);
await mx.readFromXmlFile(doc, filename);
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
expect(doc.getChild('paint_semigloss')).to.exist;
expect(doc.validate()).to.be.true;
const doc2 = mx.createDocument();
const mtlxString = getMtlxStrings([filename], includeTestPath);
expect(async () => await mx.readFromXmlString(doc2, mtlxString[0])).to.throw;
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, searchPath);
await mx.readFromXmlString(doc2, mtlxString[0]);
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
expect(doc2.getChild('paint_semigloss')).to.exist;
expect(doc2.validate()).to.be.true;
expect(doc2.equals(doc)).to.be.true;
doc2.delete();
doc.delete();
});
it('Locate XML includes via absolute search paths', async () =>
{
let absolutePath;
if (typeof window === 'object')
{
// We're in the browser
const cwd = window.location.origin + window.location.pathname;
absolutePath = cwd + '/' + includeTestPath;
} else if (typeof process === 'object')
{
// We're in Node
const nodePath = require('path');
absolutePath = nodePath.resolve(includeTestPath);
}
const doc = mx.createDocument();
await mx.readFromXmlFile(doc, 'root.mtlx', absolutePath);
doc.delete();
});
it('Detect XML include cycles', async () =>
{
const doc = mx.createDocument();
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/cycle.mtlx')).to.throw;
doc.delete();
});
it('Disabling XML includes', async () =>
{
const doc = mx.createDocument();
const readOptions = new mx.XmlReadOptions();
readOptions.readXIncludes = false;
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/cycle.mtlx', readOptions)).to.not.throw;
doc.delete();
});
it('Write to XML string', async () =>
{
// Read all example documents and write them to an XML string
const searchPath = libraryPath + ';' + examplesPath;
for (let filename of exampleFilenames)
{
const doc = mx.createDocument();
await mx.readFromXmlFile(doc, filename, searchPath);
// Serialize to XML.
const writeOptions = new mx.XmlWriteOptions();
writeOptions.writeXIncludeEnable = false;
const xmlString = mx.writeToXmlString(doc, writeOptions);
writeOptions.delete();
// Verify that the serialized document is identical.
const writtenDoc = mx.createDocument();
await mx.readFromXmlString(writtenDoc, xmlString);
expect(writtenDoc).to.eql(doc);
writtenDoc.delete();
doc.delete();
};
});
it('Prepend include tag', () =>
{
const doc = mx.createDocument();
const includePath = "SomePath";
const writeOptions = new mx.XmlWriteOptions();
mx.prependXInclude(doc, includePath);
const xmlString = mx.writeToXmlString(doc, writeOptions);
expect(xmlString).to.include(includePath);
writeOptions.delete();
doc.delete();
});
// Node only, because we cannot read from a downloaded file in the browser
it('Write XML to file', async () =>
{
const filename = '_build/testFile.mtlx';
const includeRegex = /<xi:include href="(.*)"\s*\/>/g;
const doc = mx.createDocument();
await mx.readFromXmlFile(doc, 'root.mtlx', includeTestPath);
// Write using includes
mx.writeToXmlFile(doc, filename);
// Read written document and compare with the original
const doc2 = mx.createDocument();
await mx.readFromXmlFile(doc2, filename, includeTestPath);
expect(doc2.equals(doc));
// Read written file content and verify that includes are preserved
let fileString = getMtlxStrings([filename], '')[0];
let matches = Array.from(fileString.matchAll(includeRegex));
expect(matches.length).to.be.greaterThan(0);
// Write inlining included content
const writeOptions = new mx.XmlWriteOptions();
writeOptions.writeXIncludeEnable = false;
mx.writeToXmlFile(doc, filename, writeOptions);
// Read written document and compare with the original
const doc3 = mx.createDocument();
await mx.readFromXmlFile(doc3, filename);
expect(doc3.equals(doc));
expect(doc.getChild('paint_semigloss')).to.exist;
// Read written file content and verify that includes are inlined
fileString = getMtlxStrings([filename], '')[0];
matches = Array.from(fileString.matchAll(includeRegex));
expect(matches.length).to.equal(0);
doc3.delete();
doc2.delete();
doc.delete();
writeOptions.delete();
});
});