Next (Custom Exporter) Previous (Custom Importer)

Custom Commands

Introduction

In this example, we will be creating a command based on the .obj importer created previously. A command has the following advantages over a script:

  • It is wrapped in a plugin and can be distributed to other users
  • It can be attached to the interface menu
  • It is be versioned to ensure safe updating

Creating a Plugin

To create a plugin, we must enter the Plugin area of Clara.io by clicking Plugins on the top navigation bar. This will bring you to an explorer similar to the Scene explorer, but this will list all created plugins. We will create a new plugin using the Create New Plugin button.

Here, you can enter a name, version, and description. In this example, we will be using the name “MyOBJ Importer”, with the description “Import uploaded .myobj assets.”. At any time we can press the Save button to commit this change. Be sure to press this button before leaving the page.

Plugin Template

On the right, we have our script editor. Here is where we define our plugin's content. All plugins begin with the same template, seen below.

exo.api.definePlugin('PluginName', { version: '0.0.1' }, function(app) {
    // Code goes here.
});

We use the definePlugin function of the API, providing it a name, options, and a function definition. The plugin name is how others will call on this plugin, so we must be careful in naming it. The options argument is a Javascript object that specifies information about the plugin. Currently, the only member included should be a version number mirroring the one used on the left. The function is passed a reference to the app, which is used to register commands or operators. All supporting code should also be contained within this function.

In our case, our plugin definition will look like this.

exo.api.definePlugin('MyOBJImporter', { version: '0.0.1' }, function(app) {
    // Our importer will be moved into here.
});

Command Template

Next we must create a command so that users can call our functions. Below is a command template.

  app.defineCommand({
    name: 'CommandName',

    execute: function(ctx, options) {
      // Execute command here.
    }
  });

We use the app argument passed in from the plugin to call the defineCommand function, which takes one Javascript object. The first member should be name. Similar to the plugin name, this is how users will call this command. Next we define an execute function member, which is the entry point for our command. It takes two arguments - a reference to ctx, which many scripts require, and options as a Javascript object. This is how users will pass arguments to the command. There are also members that can be defined to attach the command to the UI, but we will discuss those in a later example.

In our case, our command definition will look like this.

  app.defineCommand({
    name: 'ImportMyOBJ',

    execute: function(ctx, options) {
      importMyOBJ(ctx, options.get('assetName');
    }
  });

Here we call our command 'ImportMyOBJ'. In execute we make a call to a function that we will define called importMyOBJ, where we insert our existing code. We also use get to fetch the assetName member from our options.

Adding the Existing Code

We are now ready to add in the existing code from our other example. We will wrap it in the importMyOBJ function so that our functions will still have access to ctx. Our function is defined as below.

  var importMyOBJ = function(ctx, assetName) {

    // Pasted from our existing code
    var getContentFromMyOBJ = function(fileName, callback) {
      var myObjs = _.filter(ctx().scene.files.assets(), function(file) {
    .
    .
    .
        ctx('%Objects').addNode('MyMesh', 'PolyMesh', { PolyMesh: { Mesh: { geometry: myMesh } } });
      });
    }
  }

  // Finally, we call our function
  getContentFromMyOBJ(assetName, continueImport);
}

Calling the Command

Now that the plugin has been defined, we can test it by calling it in a scene. Save the script and enter a scene with a .myobj file uploaded. In the scripting area at the bottom, in a new script, call the command with the following.

ctx.exec('MyOBJImporter', 'ImportMyOBJ', { assetName: 'Cube' });

Of course, Cube will be replaced by the name of the asset. This is also how users of the plugin will call the command. And that's it! We've now created a fully functioning importer.

A Couple Improvements

Now that we are using a command, there are a couple improvements that we can make for our end users. The first is error reporting. We should provide the user with a meaningful log error if no nodes can be found. We'll add the following code into the getContentFromMyOBJ function.

        return file.getName() === fileName && file.getFileType() === '.myobj';
      });

      // Throw an error and abort if no assets are found
      if (myObjs.length < 1) {
        exo.reportError('No asset named "' + assetName + '" found.');
        return;
      }

      myObjs[0].fetchContent(callback);

This uses the exo.reportError message, which will display the error in the log and the browser console.

Next, since we now have the name of the asset in the closure (in the last example we lost it between the callback), we should name the new node with the asset name, rather than ‘MyMesh’.

        internal: false
      }).then(function(myMesh) {
        ctx('%Objects').addNode(assetName, 'PolyMesh', { PolyMesh: { Mesh: { geometry: myMesh } } });
      });
    }

In general, as a plugin developer you should keep an eye out for small improvements such as these that can make a difference to the end user. Responding to user feedback or making the plugin open source are two viable ways of making your plugin as effective as possible.

All Together

Below is the plugin in its entierety.

exo.api.definePlugin('MyOBJImporter', { version: '0.0.1' }, function(app) {

  app.defineCommand({
    name: 'ImportMyOBJ',

    execute: function(ctx, options) {
      importMyOBJ(ctx, options.get('assetName'));
    }
  });

  var importMyOBJ = function(ctx, assetName) {
    var getContentFromMyOBJ = function(fileName, callback) {
      var myObjs = _.filter(ctx().scene.files.assets(), function(file) {
        return file.getName() === fileName && file.getFileType() === '.myobj';
      });

      // Throw an error and abort if no assets are found
      if (myObjs.length < 1) {
        exo.reportError('No asset named "' + assetName + '" found.');
        return;
      }

      myObjs[0].fetchContent(callback);
    }

    var continueImport = function(err, content) {
      // Split lines on newline or backslash
      var lines = content.split(/[\n\\]/);

      // Create empty containers
      var verts = [];
      var faces = [];
      var uvs = [];
      var uvIndices = [];
      var normals = [];
      var normalIndices = [];

      // Parse each line
      _.each(lines, function(line) {
        line = line.trim();
        // Split on whitespace
        var lineArr = line.split(/\s+/g);

        // Parse vertex positions
        if (lineArr[0] === 'v') {
          verts.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
        }

        // Parse UVs
        else if (lineArr[0] === 'vt') {
          uvs.push(new THREE.Vector2(parseFloat(lineArr[1]), parseFloat(lineArr[2])));
        }

        // Parse normals
        else if (lineArr[0] === 'vn') {
          normals.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
        }

        // Parse faces
        else if (lineArr[0] === 'f') {
          var face = [];
          var uvIndex = [];
          var normalIndex = [];
          // Parse each attribute set
          _.each(_.rest(lineArr), function(point) {
            var pointAttrs = point.split('/');

            // Attributes (off-by-one adjustment)
            // Position indices
            face.push(parseInt(pointAttrs[0]) - 1);

            // UV indices
            if (pointAttrs.length >= 2 && pointAttrs[1] !== "") {
              uvIndex.push(parseInt(pointAttrs[1]) - 1);
            }

            // Normal indices
            if (pointAttrs.length >= 3 && pointAttrs[2] !== "") {
              normalIndex.push(parseInt(pointAttrs[2]) - 1);
            }
          });
          // Add face to collection
          faces.push(face);

          // Add UVs and normals to collection, if they exist
          if (uvIndex.length > 0) {
            uvIndices.push(uvIndex);
          }
          if (normalIndex.length > 0) {
            normalIndices.push(normalIndex);
          }
        }
      });

      // Create polyMesh object
      var polyMesh = new exo.geometry.PolyMesh( faces, verts );

      // If there are UVs or normals, create a map to store them and apply to the polyMesh object
      if (uvs.length > 0 && uvIndices.length > 0) {
        polyMesh.setUVMap(new exo.geometry.PolyMap(uvIndices, uvs));
      }
      if (normals.length > 0 && normalIndices.length > 0) {
        polyMesh.setNormalMap(new exo.geometry.PolyMap(normalIndices, normals));
      }

      ctx().addFile({
        name: 'MyMesh.json',
        content: polyMesh,
        type: 'application/json',
        sourceType: 'import',
        internal: false
      }).then(function(myMesh) {
        ctx('%Objects').addNode(assetName, 'PolyMesh', { PolyMesh: { Mesh: { geometry: myMesh } } });
      });
    }

    getContentFromMyOBJ(assetName, continueImport);
  }
});

Next (Custom Exporter) Previous (Custom Importer)