In this last tutorial of this series, we will be creating an .obj exporter to pair with our importer. Since many of the concepts in this tutorial have already been covered by the Importer and Commands tutorials, this tutorial will leave most review to the comments, only stopping to comment on new concepts.
We start with a plugin definition and command definition very reminiscent of our last tutorial. This time, our options will expect a member called nodeName
. We will create a function called assembleOBJ
that will take a reference to ctx and our node’s name. It will return a string representing the contents of our .obj file. We must also error check to see if the function threw an error.
Finally, we need to save the file in the client. This is done creating a data blob and saving it using the standard HTML API. This will start a download with the given file contents and file name. In our case, we will pass it the contents generated by our helper function and the name of our node with the .obj extension.
exo.api.definePlugin('OBJExporter', { version: '0.0.1' }, function(app) {
app.defineCommand({
name: 'ExportOBJ',
execute: function(ctx, options) {
// Retrieve the node to export from the options
var nodeName = options.get('nodeName');
// Call our helper function, passing in our reference to ctx and the node' name
var content = assembleOBJ(ctx, nodeName);
// Exit execution if no OBJ was assembled
if (!content) {
return;
}
// Save the data to the client computer
var blob = new Blob([content], {type: 'application/octet-stream;charset=' + document.characterSet});
saveAs(blob, nodeName + '.obj');
}
});
Next we define our helper function. Its first job is to try and find the PolyMesh operator on the node that we specified. We do this using a ctx selection. Since the ctx selection does not return the object found directly to allow chaining commands, we get at the object by using at(0)
, which will grab the first object found in the event that multiple nodes share the same name.
Next we error check and throw an error, returning undefined, if we did not find any nodes that match our requirements. If this passed, we can go ahead and grab the polyMesh member which will contain our vertex and face information.
We will also check to see if the polyMesh contains maps for UVs and normals. We can null check these later in the code as well.
Lastly, we begin our string object, objContent
, to which we will append all of our formatted information. We initialize it with a header using the .obj comment character (#
).
var assembleOBJ = function(ctx, nodeName) {
// Find the mesh operator of our node using a ctx filter
var content = ctx(nodeName + '#PolyMesh[name=Mesh]').at(0);
// Throw an error if we couldn't find anything
if (!content) {
exo.reportError('No match found, or object "' + nodeName + '" is not a polymesh.');
return undefined;
}
// Grab the polyMesh member
content = content.polyMesh;
// Try and grab the existing maps
var uvFaces = content.uvMaps.default ? content.uvMaps.default.faces : null;
var normalFaces = content.normalMap.faces ? content.normalMap.faces : null;
// Header text
var objContent = '# Clara.io Custom .obj Exporter\n# Object ' + nodeName + '\n\n';
Next, we simply iterate through each vertex, UV, and normal, and append them to our objContent
string. In the case of UVs and normals, we null check to see if it is necessary.
// Vertices
objContent += '# Vertices: ' + content.positions.length + '\n';
_.each(content.positions, function(position) {
objContent += 'v ' + position.x + ' ' + position.y + ' ' + position.z + '\n';
});
// UVs, if they exist
if (uvFaces) {
objContent += '\n# UVs: ' + content.uvMaps.default.values.length + '\n';
_.each(content.uvMaps.default.values, function(uv) {
objContent += 'vt ' + uv.x + ' ' + uv.y + '\n';
});
}
// Normals, if they exist
if (normalFaces) {
objContent += '\n# Normals: ' + content.normalMap.values.length + '\n';
_.each(content.normalMap.values, function(normal) {
objContent += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';
});
}
The last part of our function defines the faces using index references. Since these are stored as arrays within arrays, we use a double Underscore.js each
loop, saving the current indices as i
and j
, and adding 1 since .obj indices begin at 1. Once again, UVs and Normals are null checked we need to bother including them. Once this is complete, we return the content string to be exported.
// Faces / UV indices / Normal indices
objContent += '\n# Faces: ' + content.faces.length + '\n';
_.each(content.faces, function(face, i) {
objContent += 'f ';
_.each(face, function(index, j) {
// If UVs or normals exist
if (uvFaces || normalFaces) {
objContent += index + 1;
if (uvFaces) {
objContent += '/' + (uvFaces[i][j] + 1);
} else {
objContent += '/';
}
if (normalFaces) {
objContent += '/' + (normalFaces[i][j] + 1) + ' ';
} else {
objContent += '/';
}
// Otherwise, print just the face index
} else {
objContent += (index + 1) + ' ';
}
});
objContent += '\n';
});
// Return the assembled string
return objContent;
}
});
We hope that this series of tutorials has served as a good introduction to working with Clara.io, ctx, our PolyMesh structure, our command system, and using Underscore.js to keep things simple. Using these tutorials as a template, we hope that you can create your own set of importers and exporters for Clara.io, making it as accessible as possible to all forms of data. If there is any confusion or if you have any feedback, come drop by our discussion forums and let us know. Thanks!
Below is the script in its entirety.
exo.api.definePlugin('OBJExporter', { version: '0.0.1' }, function(app) {
app.defineCommand({
name: 'ExportOBJ',
execute: function(ctx, options) {
// Retrieve the node to export from the options
var nodeName = options.get('nodeName');
// Call our helper function, passing in our reference to ctx and the node' name
var content = assembleOBJ(ctx, nodeName);
// Exit execution if no OBJ was assembled
if (!content) {
return;
}
// Save the data to the client computer
var blob = new Blob([content], {type: 'application/octet-stream;charset=' + document.characterSet});
saveAs(blob, nodeName + '.obj');
}
});
var assembleOBJ = function(ctx, nodeName) {
// Find the mesh operator of our node using a ctx filter
var content = ctx(nodeName + '#PolyMesh[name=Mesh]').at(0);
// Throw an error if we couldn't find anything
if (!content) {
exo.reportError('No match found, or object "' + nodeName + '" is not a polymesh.');
return undefined;
}
// Grab the polyMesh member
content = content.polyMesh;
// Try and grab the existing maps
var uvFaces = content.uvMaps.default ? content.uvMaps.default.faces : null;
var normalFaces = content.normalMap.faces ? content.normalMap.faces : null;
// Header text
var objContent = '# Clara.io Custom .obj Exporter\n# Object ' + nodeName + '\n\n';
// Vertices
objContent += '# Vertices: ' + content.positions.length + '\n';
_.each(content.positions, function(position) {
objContent += 'v ' + position.x + ' ' + position.y + ' ' + position.z + '\n';
});
// UVs, if they exist
if (uvFaces) {
objContent += '\n# UVs: ' + content.uvMaps.default.values.length + '\n';
_.each(content.uvMaps.default.values, function(uv) {
objContent += 'vt ' + uv.x + ' ' + uv.y + '\n';
});
}
// Normals, if they exist
if (normalFaces) {
objContent += '\n# Normals: ' + content.normalMap.values.length + '\n';
_.each(content.normalMap.values, function(normal) {
objContent += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';
});
}
// Faces / UV indices / Normal indices
objContent += '\n# Faces: ' + content.faces.length + '\n';
_.each(content.faces, function(face, i) {
objContent += 'f ';
_.each(face, function(index, j) {
// If UVs or normals exist
if (uvFaces || normalFaces) {
objContent += index + 1;
if (uvFaces) {
objContent += '/' + (uvFaces[i][j] + 1);
} else {
objContent += '/';
}
if (normalFaces) {
objContent += '/' + (normalFaces[i][j] + 1) + ' ';
} else {
objContent += '/';
}
// Otherwise, print just the face index
} else {
objContent += (index + 1) + ' ';
}
});
objContent += '\n';
});
// Return the assembled string
return objContent;
}
});