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