Next (OAuth) Previous (Custom Commands)

Custom Exporter

Introduction

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.

Plugin and Command Definitions

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');
    }
  });

Beginning Assembly

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';

Parsing Vertices, Normals, and UVs

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';
      });
    }

Faces and Indices

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;
  }
});

All Together

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;
  }
});

Next (OAuth) Previous (Custom Commands)