Difference between revisions of "Example cmd sy partifymesh"

From The Foundry MODO SDK wiki
Jump to: navigation, search
(Initial version.)
 
m (Config: Removed note about kit relative help url's not working, they have since been fixed.)
 
(23 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
== Introduction ==
 
== Introduction ==
  
 +
This article details in laymans terms adding a 'new' command to modo.
 +
It has been written by an SDK end user, so may or may not be representative of 'good' or even accurate information.
  
This article is a an example of a adding a 'new' command to modo.
+
== Planning ==
  
...TODO: more info req'd... eg. style, coding do's & dont's etc., why one should have used a PreExecute() method to prompt the user with continue or not if there were a ginormous amount of polygons in the given mesh, etc. etc.
+
It's good practice to note down a few details describing the scope of what your command does, when it does it and how it may be activated so as to give yourself some guidelines to work too. So...
  
 +
<ul>
 +
  <ol>
 +
    <li>''What should this command do?''<br/><blockquote>This command should 'apply a unique part tag to each polygon in the currently selected mesh'.</blockquote></li>
 +
    <li>''What should this command be called?''<br/><blockquote>Based on what the command is supposed to do and whom has made it a good name for this command would be ''''sy.partifymesh''''.</blockquote></li>
 +
    <li>''When should this command be allowed to run?''<br/><blockquote>This command should only be 'enabled' when the user has a 'mesh' item selected AND that mesh item has some polygons.</blockquote></li>
 +
    <li>''Is the command going to possibly run from a button?''<br/><blockquote>For this particular example we'll make the command suitable for running from a button. So, we'll detail help information and images that will automatically apply when the command is mapped to a button.</blockquote></li>
 +
  </ol>
 +
</ul>
  
== Code ==
+
== Implementation ==
  
 +
=== Code ===
  
... TODO: Discuss the code ...
+
A basic 'code flow' summary follows, it's not in any particular order of operation :
  
 +
* The module's initialize method is called which in turn calls the namespace's initialize method.
 +
* The namespace initialize registers the 'server' with appropriate interfaces for this plugin, which is how modo knows that these servers exist.
 +
* When the command is activated within modo the constructor sets a CLxUser_Log.  The fact that the log subsystem even exists is set up via server tags (LXtTagInfoDesc ).  Note that this log is purely for debugging purposes, and should not be present in shipping code.
 +
* The runtime acquires the flags from this plugin via the basic_CmdFlags method, which we use to report that this command performs undoable actions, in this case by calling functions that modify the mesh.
 +
* The runtime calls the basic_Enable method to see if this command can be run or not, and which is used to mark the command as disabled or enabled in the user interface.  If the command is disabled, cmd_Execute will not be called.
 +
* When added to as a button in Form View, the runtime calls into basic_AddNotifiers, which in turn essentially registers what events this plugin will receive notification for and how to invalidate the user interface when a relevant event occurs.  In this case, we're listening for changes to the item selection, and that we want it to notify the user interface to refresh our button's enable state when that occurs.
 +
* Finally, the runtime will call the plugin's cmd_Execute method if the command is enabled.  Errors are reported through the command's CLxUser_Message, which is obtained through a call to the basic_Message method on our object.
  
 +
 +
==== sy_cmds.cpp ====
 
<source lang="cpp">
 
<source lang="cpp">
//Copyright Synide(Sy) 2011
+
/* synide@rocketmail.com */
//synide@rocketmail.com
+
 
 +
#include <lx_plugin.hpp>
 +
 
 +
namespace sy_partifymesh_cmd { extern void initialize();
 +
                                                      extern void cleanup(); };
 +
 
 +
 
 +
void initialize() {
 +
sy_partifymesh_cmd ::initialize();
 +
};
 +
 
 +
void cleanup() {
 +
sy_partifymesh_cmd ::cleanup();
 +
};
 +
</source>
 +
 
 +
 
 +
==== sy_partifymesh_cmd.cpp ====
 +
<source lang="cpp">
 +
/* synide@rocketmail.com */
 +
 
 
//=====================================
 
//=====================================
 
// Notes:
 
// Notes:
//           April, 2012 - Revised for Liki example.
+
// April, 2012 - Revised for Liki example.
 +
// May, 2012 - A More comprehensive implementation.
 +
//                                              Added preprocessor directives to disabled EventLog output when not in Debug.
 +
//                                              Added 'meshes.event' to facilitate disabling if the mesh is emptied of polygons.
 +
//                                              Added a 'completion' message.
  
 
#include <time.h>
 
#include <time.h>
Line 30: Line 74:
 
#include <lx_log.hpp>
 
#include <lx_log.hpp>
 
#include <lxu_command.hpp>
 
#include <lxu_command.hpp>
 +
#include <lxu_select.hpp>
  
std::string fnI2S(unsigned value, bool notnullterminated = true, unsigned padding = 0, bool asHexadecimal = false, bool withHexPrefix = false) {
 
    //'fnI2S'
 
    //    Integer to string, a variant of David Vasquez's Integer2String
 
    std::ostringstream os;
 
    std::string result;
 
    if (asHexadecimal) {os << std::hex;}
 
    if (value < 10 && padding) {os << 0;}
 
    if (value < 100 && padding > 1) {os << 0;}
 
    if (value < 1000 && padding > 2) {os << 0;}
 
    if (notnullterminated)
 
        os << value;
 
    else
 
        os << value << std::ends;
 
    if (asHexadecimal && withHexPrefix)
 
        result = std::string("0x");
 
    result += os.str();
 
    return result;
 
};
 
  
namespace sy_partifymesh
+
namespace sy_partifymesh_cmd
 
{
 
{
 +
 +
#ifndef __NAMESPACE_STATICS
 +
  
 
static char *SERVER_NAME                = "sy.partifymesh";
 
static char *SERVER_NAME                = "sy.partifymesh";
static char *LOG_NAME                   = "sy/cmds/partifymesh";
+
static char *SERVER_USERNAME        = "Sy PartifyMesh";
 +
static char *LOG_NAME                     = "sy/cmds/partifymesh";
 +
 
 +
static CLxItemType                          iType_Mesh                (LXsITYPE_MESH);
 +
 
 +
 
 +
#endif __NAMESPACE_STATICS
 +
 
 +
 
 +
std::string fnI2S(unsigned value, bool notnullterminated = true, unsigned padding = 0, bool asHexadecimal = false, bool withHexPrefix = false)
 +
{
 +
std::ostringstream os;
 +
std::string result;
 +
if (asHexadecimal) {os << std::hex;}
 +
if (value < 10 && padding) {os << 0;}
 +
if (value < 100 && padding > 1) {os << 0;}
 +
if (value < 1000 && padding > 2) {os << 0;}
 +
if (notnullterminated)
 +
os << value;
 +
else
 +
os << value << std::ends;
 +
if (asHexadecimal && withHexPrefix) {result = std::string("0x");}
 +
result += os.str();
 +
return result;
 +
};
 +
 
 +
std::string fnF2Sfixed(float value, bool nullterminated = false, unsigned p = 1)
 +
{
 +
std::ostringstream os;
 +
os.setf(std::ios::fixed);
 +
os.precision(p);
 +
if (nullterminated)
 +
os << value << std::ends;
 +
else
 +
os << value;
 +
return os.str();
 +
};
 +
 
 +
 
  
 
/*
 
/*
 
  * 'sy.partifymesh' command is derived from 'CLxBasicCommand'.
 
  * 'sy.partifymesh' command is derived from 'CLxBasicCommand'.
 
  *
 
  *
* Was going to have a parameter passed to the command (name of the mesh to process) but decided against it in the end.
 
*
 
* We won't bother with utilizing any of the system called pre-work methods for doing
 
* sanity checks etc. We'll just set the 'flags' for this command and let the system flow
 
* straight through to 'cmd_Execute()' where we'll just go ahead and do the work regardless.
 
* Not recommended coding... :)
 
*
 
* As I'm not sure if commands can utlize a 'CLxUser_Monitor'. We might have to
 
* fire progress reports through to the log system. - TODO.
 
 
  */
 
  */
 
class CSyPartifyMesh : public CLxBasicCommand {
 
class CSyPartifyMesh : public CLxBasicCommand {
    public:
+
public:
        CSyPartifyMesh();
+
static LXtTagInfoDesc          descInfo[];
        int                            basic_CmdFlags      () LXx_OVERRIDE;
+
        void                            cmd_Execute        (unsigned int flags) LXx_OVERRIDE;
+
        CLxUser_LayerService            LayerService;
+
        CLxUser_LogService              LogService;
+
        CLxUser_Log                    PartifyMeshLog;
+
        static LXtTagInfoDesc          descInfo[];
+
   
+
};
+
  
LXtTagInfoDesc CSyPartifyMesh::descInfo[] = {{LXsSRV_LOGSUBSYSTEM,LOG_NAME}, {0}};
+
CLxUser_LayerService            LayerService;
  
CSyPartifyMesh::CSyPartifyMesh() {
+
#ifdef _DEBUG
    LogService.GetSubSystem(LOG_NAME, PartifyMeshLog);
+
CLxUser_Log                    PartifyMeshLog;
};
+
  
int CSyPartifyMesh::basic_CmdFlags() {
+
CSyPartifyMesh() {
    return LXfCMD_UNDO;
+
PartifyMeshLog.setByName(LOG_NAME);
};
+
};
 +
#endif _DEBUG
  
void CSyPartifyMesh::cmd_Execute(unsigned int flags) {
 
/*
 
*    We restrict the LayerScan's results to the 'Primary' selected mesh so that we
 
*    guard against the user having multiple 'selected' meshs which could result in overload.
 
*/
 
  
    CLxUser_LayerScan              LayerScan;
+
void basic_Notifiers() override {
    CLxUser_Item                    theItem;
+
basic_AddNotify(LXsNOTIFIER_SELECT, "item +d"); /* Will be notified when Item selection changes */
    CLxUser_Mesh                    theMesh;
+
basic_AddNotify(LXsNOTIFIER_MESHES, "+p +d");   /* Will be notified on polygon changes in the mesh */
    CLxUser_Polygon                thePolygon;
+
};
    CLxUser_StringTag              theTag;
+
    const char                      *ItemName;
+
  
    LayerService.BeginScan(LXf_LAYERSCAN_PRIMARY | LXf_LAYERSCAN_WRITEMESH, LayerScan);
 
  
    if (LayerScan.ItemByIndex(0, theItem))
+
bool basic_Enable(CLxUser_Message &msg) override {
        theItem.Name(&ItemName);
+
/*
   
+
Utilize the new (601) CLxItemSelectionType to find the first mesh item and then check if it
    // As there should only ever be 1 mesh found by the LayerScan it's index should be zero.
+
has any polygons.
    if (LayerScan.EditMeshByIndex(0, theMesh))
+
*/
    {
+
        int NoOfPolygons = theMesh.NPolygons();
+
  
        // Only do work if there are polygons.
+
bool result(false);
        if (NoOfPolygons > 0)
+
        {
+
CLxItemSelectionType TypedItemSelector(LXsITYPE_MESH);
            // Set 'thePolygon' object as the polygon accessor for the 'theMesh' object.
+
CLxUser_Item theItem;
            thePolygon.fromMeshObj(theMesh);
+
const char *ItemName;
  
            // Set 'theTag' object as the 'tags' accessor for the polygon object.
+
if (TypedItemSelector.GetFirst(theItem) && theItem.test())
            theTag.set(thePolygon);
+
{
 +
if (theItem.Type() == iType_Mesh.Type())
 +
{
 +
CLxUser_Mesh theMesh;
 +
CLxUser_ChannelRead aChanReader;
  
            std::string tag;
+
if (aChanReader.from(theItem))
            unsigned msg_stride = NoOfPolygons / 5;
+
{
            unsigned msg_counter = 1;
+
if (aChanReader.Object(theItem, LXsICHAN_MESH_MESH, theMesh))
            clock_t timerStart = clock();
+
{
 +
result = theMesh.NPolygons() > 0;
 +
if (!result)
 +
{
 +
theItem.UniqueName(&ItemName);
 +
if (ItemName) {
 +
msg.SetCode(LXe_FAILED);
 +
msg.SetMessage(SERVER_NAME, "ItemHasZeroPolygons", 0);
 +
msg.SetArgumentString(1, ItemName);
 +
}
 +
}
 +
}
 +
else
 +
{
 +
/* Unlikely to ever get here */
 +
msg.SetCode(LXe_CMD_DISABLED);
 +
msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
 +
}
 +
}
  
            for (int i1 = 0; i1 < NoOfPolygons; i1++)
+
aChanReader.clear();
            {
+
theMesh.clear();
                //Ask the polygon accessor to populate itself based on the poly index.
+
}
                thePolygon.SelectByIndex(i1);
+
else
 +
{
 +
msg.SetCode(LXe_CMD_DISABLED);
 +
msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
 +
}
 +
theItem.clear();
 +
}
 +
else
 +
{
 +
msg.SetCode(LXe_CMD_DISABLED);
 +
msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
 +
}
 +
return result;
 +
};
  
                // Construct a unique 'part' tag name.
 
                tag = "poly_" + fnI2S(i1);
 
  
                // Ask the 'tags' accessor to set the 'PART' tag.
+
int basic_CmdFlags() override {
                theTag.Set(LXi_PTAG_PART, tag.c_str());
+
return LXfCMD_UNDO;
 +
};
  
  
                //logging at 20%, 40%, 60%, 80%
+
void cmd_Execute(unsigned int flags) override {
                if (PartifyMeshLog && (i1 == (msg_stride * msg_counter)))
+
/*
                {
+
    We restrict the LayerScan's results to the 'Primary' selected mesh only so that we
                    clock_t timerDiff = clock() - timerStart;
+
    guard against the user having multiple 'selected' meshs which could result in overload.
                    double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
+
*/
                    switch(msg_counter)
+
                    {
+
                        case 1:
+
                            PartifyMeshLog.Message(LXe_INFO, "20\%%, %u polygons in %.6f seconds.", i1, duration);
+
                            break;
+
                        case 2:
+
                            PartifyMeshLog.Message(LXe_INFO, "40\%%, %u polygons in %.6f seconds.", i1, duration);
+
                            break;
+
                        case 3:
+
                            PartifyMeshLog.Message(LXe_INFO, "60\%%, %u polygons in %.6f seconds.", i1, duration);
+
                            break;
+
                        case 4:
+
                            PartifyMeshLog.Message(LXe_INFO, "80\%%, %u polygons in %.6f seconds.", i1, duration);
+
                            break;
+
                    }
+
                    msg_counter += 1;
+
                }
+
            }
+
  
            // Tell the LayerScan that we are only 'editing' polygon tags.
+
CLxUser_LayerScan              LayerScan;
            LayerScan.SetMeshChange(0, LXf_MESHEDIT_POL_TAGS);
+
CLxUser_Item                    theItem;
 +
CLxUser_Mesh                    theMesh;
 +
CLxUser_Polygon                thePolygon;
 +
CLxUser_StringTag              theTag;
 +
const char                      *ItemName;
  
            // Apply the mesh changes.
+
LayerService.BeginScan(LXf_LAYERSCAN_PRIMARY | LXf_LAYERSCAN_WRITEMESH, LayerScan);
            LayerScan.Apply();
+
  
            clock_t timerDiff = clock() - timerStart;
+
if (LayerScan.ItemByIndex(0, theItem))
            double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
+
theItem.UniqueName(&ItemName);
            PartifyMeshLog.Message(LXe_INFO, "100\%%, %u polygons in %.6f seconds.", NoOfPolygons, duration);
+
        }
+
        else
+
        {
+
            if (ItemName)
+
                PartifyMeshLog.Message(LXe_WARNING, "'%s' has zero polygons.", ItemName);
+
        }
+
    }
+
    else
+
    {
+
        PartifyMeshLog.Message(LXe_NOTFOUND, "No valid mesh found!");
+
    }
+
  
};
+
/* As there should only ever be 1 mesh found by the LayerScan it's index should be zero. */
 +
if (LayerScan.EditMeshByIndex(0, theMesh))
 +
{
 +
int NoOfPolygons = theMesh.NPolygons();
  
//NOTE: Read the code carefully, this is a sy_partifymesh::initialize() method, NOT the global initialize() method.
+
// Set 'thePolygon' as the polygon accessor for the 'theMesh'.
void initialize() {
+
thePolygon.fromMeshObj(theMesh);
        CLxGenericPolymorph    *srv;
+
        srv = new CLxPolymorph<CSyPartifyMesh>;
+
        srv->AddInterface(new CLxIfc_Command <CSyPartifyMesh>);
+
        srv->AddInterface(new CLxIfc_StaticDesc <CSyPartifyMesh>);
+
        thisModule.AddServer(sy_partifymesh::SERVER_NAME, srv); //Just to illustrate thisModule usage instead of lx::AddServer
+
};
+
  
/*
+
// Set 'theTag' as the 'tags' accessor for the polygon.
//NOTE: Read the code carefully, this is a sy_partifymesh::cleanup() method, NOT the global cleanup() method.
+
theTag.set(thePolygon);
void cleanup()
+
{
+
   
+
};
+
*/
+
  
};//namespace sy_partifymesh
+
std::string tag;
 +
clock_t timerStart = clock();
  
 +
#ifdef _DEBUG
 +
unsigned msg_stride = NoOfPolygons / 5;
 +
unsigned msg_counter = 1;
 +
#endif _DEBUG
 +
 +
for (int i1 = 0; i1 < NoOfPolygons; i1++)
 +
{
 +
/*Ask the polygon accessor to populate itself based on the poly index. */
 +
thePolygon.SelectByIndex(i1);
 +
 +
/* Construct a unique 'part' tag name. */
 +
tag = "poly_" + fnI2S(i1);
 +
 +
/* Ask the 'tags' accessor to set the 'PART' tag. */
 +
theTag.Set(LXi_PTAG_PART, tag.c_str());
 +
 +
#ifdef  _DEBUG
 +
//logging at 20%, 40%, 60%, 80%
 +
if (PartifyMeshLog && (i1 == (msg_stride * msg_counter)))
 +
{
 +
clock_t timerDiff = clock() - timerStart;
 +
double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
 +
switch(msg_counter)
 +
{
 +
case 1:
 +
PartifyMeshLog.Message(LXe_INFO, "20\%%, %u polygons in %.6f seconds.", i1, duration);
 +
break;
 +
case 2:
 +
PartifyMeshLog.Message(LXe_INFO, "40\%%, %u polygons in %.6f seconds.", i1, duration);
 +
break;
 +
case 3:
 +
PartifyMeshLog.Message(LXe_INFO, "60\%%, %u polygons in %.6f seconds.", i1, duration);
 +
break;
 +
case 4:
 +
PartifyMeshLog.Message(LXe_INFO, "80\%%, %u polygons in %.6f seconds.", i1, duration);
 +
break;
 +
}
 +
msg_counter += 1;
 +
}
 +
#endif  _DEBUG
 +
}
 +
 +
/* Tell the LayerScan that we are only 'editing' polygon tags. */
 +
LayerScan.SetMeshChange(0, LXf_MESHEDIT_POL_TAGS);
 +
 +
/* Apply the mesh changes. */
 +
LayerScan.Apply();
 +
 +
clock_t timerDiff = clock() - timerStart;
 +
double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
 +
 +
basic_Message().SetCode(LXe_INFO);
 +
basic_Message().SetMessage(SERVER_NAME, "Complete", 0);
 +
basic_Message().SetArgumentInt(1, NoOfPolygons);
 +
basic_Message().SetArgumentString(2, fnF2Sfixed((float)duration,false,6).c_str());
 +
 +
#ifdef  _DEBUG
 +
PartifyMeshLog.Message(LXe_INFO, "100\%%, %u polygons in %.6f seconds.", NoOfPolygons, duration);
 +
#endif  _DEBUG
 +
}
 +
else
 +
{
 +
/* Unlikely to ever get here */
 +
basic_Message().SetCode(LXe_CMD_DISABLED);
 +
basic_Message().SetMessage(SERVER_NAME, "NoValidMesh", 0);
 +
 +
#ifdef  _DEBUG
 +
PartifyMeshLog.Message(LXe_NOTFOUND, "No valid mesh found!");
 +
#endif _DEBUG
 +
}
 +
};
  
// I like the new layout of utilizing namespaces with an initialize() in each and then
 
// calling those from the global initialize() method. Keeps things tidy!
 
// FYI. Don't need to extern in this instance 'cause we're only 1 file...
 
  
void initialize()
 
{
 
    sy_partifymesh::initialize();
 
 
};
 
};
  
/* No need for a cleanup() but if we did they'd go here...
+
 
// NOTE: cleanup() should rarely need to be used if things a destructed well.
+
LXtTagInfoDesc CSyPartifyMesh::descInfo[] = {
void cleanup()
+
{LXsSRV_LOGSUBSYSTEM, LOG_NAME},
{
+
{LXsSRV_USERNAME, SERVER_USERNAME},
    sy_partifymesh::cleanup();
+
{0}
 
};
 
};
*/
 
</source>
 
  
  
== Config ==
+
void initialize() {
 +
CLxGenericPolymorph *srv = new CLxPolymorph<CSyPartifyMesh>;
  
 +
srv->AddInterface(new CLxIfc_Command    <CSyPartifyMesh>);
 +
srv->AddInterface(new CLxIfc_StaticDesc <CSyPartifyMesh>);
 +
 +
lx::AddServer(SERVER_NAME, srv);
 +
};
  
In the following xml file a new commands category is created to apportion this particular command under 'Sy/Cmds' in modo.
 
Also, we specify command help details regarding what the command does. It should be noted that one would also include help & examples for the command and/or command parameters within the 'CommandHelp' element.
 
Finally, we tell modo to enable the log sub-system that this command plugin is registering itself under because by default they are disabled within the logging system.
 
  
 +
void cleanup() {};
 +
 +
};//namespace sy_partifymesh_cmd
 +
</source>
 +
 +
=== Config ===
 +
 +
The following xml file is what's referred to as a [[:Category:Configs|config]] and would form part of your plugin's release files for your [[Kits|kit]].
 +
This particular config has the following features :-
 +
 +
* A new command category is created to apportion this particular command under 'Sy/Cmds' in the modo command tree listing.
 +
* We specify command help details regarding what the command does.
 +
* We define a message table for localized error messages.
 +
* We specify a help url for this command.
 +
* We detail some UI elements relating to icons. See [[Icon_Resources|icon resources]] for more details.
  
 
<source lang="xml">
 
<source lang="xml">
 
<?xml version="1.0" encoding="UTF-8"?>
 
<?xml version="1.0" encoding="UTF-8"?>
 
<configuration>
 
<configuration>
 +
 
   <atom type="Categories">
 
   <atom type="Categories">
 
     <hash type="Category" key="Commands">
 
     <hash type="Category" key="Commands">
Line 245: Line 368:
 
   <atom type="CommandHelp">
 
   <atom type="CommandHelp">
 
     <hash type="Command" key="sy.partifymesh@en_US">
 
     <hash type="Command" key="sy.partifymesh@en_US">
       <atom type="UserName">sy.partifymesh</atom>
+
       <atom type="UserName">PartifyMesh</atom>
       <atom type="Desc">Give each polygon in selected mesh a unique 'part' tag.</atom>
+
       <atom type="Desc">Apply unique 'part' tag to each polygon in selected mesh.</atom>
       <atom type="Tooltip"></atom>
+
      <atom type="Tooltip">Apply unique 'part' tag to each polygon in selected mesh.</atom>
 +
       <atom type="ButtonName">PartifyMesh</atom>
 +
      <atom type="Example">sy.partifymesh</atom>
 
     </hash>
 
     </hash>
 
   </atom>
 
   </atom>
   <atom type="StartupCommands">
+
 
     <list type="Command">log.subEnable "sy/cmds/partifymesh" true</list>
+
   <atom type="Messages">
 +
     <hash type="Table" key="sy.partifymesh.en_US">
 +
      <hash type="T" key="ItemHasZeroPolygons">&quot;%1&quot; has zero polygons.</hash>
 +
      <hash type="T" key="NoValidMesh">No valid mesh selected.</hash>
 +
      <hash type="T" key="Complete">&quot;%1&quot; polygons completed in &quot;%2&quot; seconds.</hash>
 +
    </hash>
 
   </atom>
 
   </atom>
 +
 +
  <atom type="HelpURLs">
 +
    <hash type="HelpURL" key="command:sy.partifymesh">kit_SyCmds:help\index.html#partifymesh</hash>
 +
  </atom>
 +
 +
  <atom type="UIElements">
 +
    <hash type="Image" key="sy_cmd_icons">sy_cmd_icons.png</hash>
 +
 +
    <!-- First row of image -->
 +
    <hash type="Icon" key="sy.partifymesh_20">        <atom type="Source">sy_cmd_icons</atom>    <atom type="Location">0 0 20 20</atom>  </hash>
 +
 +
    <!-- Second row of image -->
 +
    <hash type="Icon" key="sy.partifymesh_32">        <atom type="Source">sy_cmd_icons</atom>    <atom type="Location">0 20 32 32</atom>  </hash>
 +
  </atom>
 +
 
</configuration>
 
</configuration>
 
</source>
 
</source>
 +
 +
=== Icon Resources ===
 +
 +
The following .png file is provided as an example of the icon images that commands mapped to UI buttons may implement, the display of which is an automated process. When the command returns 'false'/LXe_CMD_DISABLED from basic_Enable, the system automatically generates a desaturated version of the icon image for the button.
 +
See the [[Icon_Resources|icon resources]] article for more details.
 +
 +
:[[File:sy_cmd_icons.png]] - 20px on first row, 32px on second row.
 +
 +
== More Information ==
 +
* [[SDK]]
 +
* [[Command System]]
 +
 +
[[Category: SDK Examples]]

Latest revision as of 21:22, 14 October 2013

Introduction

This article details in laymans terms adding a 'new' command to modo. It has been written by an SDK end user, so may or may not be representative of 'good' or even accurate information.

Planning

It's good practice to note down a few details describing the scope of what your command does, when it does it and how it may be activated so as to give yourself some guidelines to work too. So...

    1. What should this command do?
      This command should 'apply a unique part tag to each polygon in the currently selected mesh'.
    2. What should this command be called?
      Based on what the command is supposed to do and whom has made it a good name for this command would be 'sy.partifymesh'.
    3. When should this command be allowed to run?
      This command should only be 'enabled' when the user has a 'mesh' item selected AND that mesh item has some polygons.
    4. Is the command going to possibly run from a button?
      For this particular example we'll make the command suitable for running from a button. So, we'll detail help information and images that will automatically apply when the command is mapped to a button.

Implementation

Code

A basic 'code flow' summary follows, it's not in any particular order of operation :

  • The module's initialize method is called which in turn calls the namespace's initialize method.
  • The namespace initialize registers the 'server' with appropriate interfaces for this plugin, which is how modo knows that these servers exist.
  • When the command is activated within modo the constructor sets a CLxUser_Log. The fact that the log subsystem even exists is set up via server tags (LXtTagInfoDesc ). Note that this log is purely for debugging purposes, and should not be present in shipping code.
  • The runtime acquires the flags from this plugin via the basic_CmdFlags method, which we use to report that this command performs undoable actions, in this case by calling functions that modify the mesh.
  • The runtime calls the basic_Enable method to see if this command can be run or not, and which is used to mark the command as disabled or enabled in the user interface. If the command is disabled, cmd_Execute will not be called.
  • When added to as a button in Form View, the runtime calls into basic_AddNotifiers, which in turn essentially registers what events this plugin will receive notification for and how to invalidate the user interface when a relevant event occurs. In this case, we're listening for changes to the item selection, and that we want it to notify the user interface to refresh our button's enable state when that occurs.
  • Finally, the runtime will call the plugin's cmd_Execute method if the command is enabled. Errors are reported through the command's CLxUser_Message, which is obtained through a call to the basic_Message method on our object.


sy_cmds.cpp

/* synide@rocketmail.com */
 
#include <lx_plugin.hpp>
 
namespace sy_partifymesh_cmd			{	extern void initialize();	
                                                       extern void cleanup();	};
 
 
void initialize() {
	sy_partifymesh_cmd			::initialize();
};
 
void cleanup() {
	sy_partifymesh_cmd			::cleanup();
};


sy_partifymesh_cmd.cpp

/* synide@rocketmail.com */
 
//=====================================
// Notes:
//				April, 2012 - Revised for Liki example.
//				May, 2012 - A More comprehensive implementation.
//                                               Added preprocessor directives to disabled EventLog output when not in Debug.
//                                               Added 'meshes.event' to facilitate disabling if the mesh is emptied of polygons.
//                                               Added a 'completion' message.
 
#include <time.h>
#include <string>
#include <sstream>
 
#include <lxidef.h>
#include <lx_plugin.hpp>
#include <lx_layer.hpp>
#include <lx_mesh.hpp>
#include <lx_log.hpp>
#include <lxu_command.hpp>
#include <lxu_select.hpp>
 
 
namespace sy_partifymesh_cmd
{
 
#ifndef	__NAMESPACE_STATICS
 
 
static char *SERVER_NAME                = "sy.partifymesh";
static char *SERVER_USERNAME         = "Sy PartifyMesh";
static char *LOG_NAME                     = "sy/cmds/partifymesh";
 
static CLxItemType                           iType_Mesh                 (LXsITYPE_MESH);
 
 
#endif	__NAMESPACE_STATICS
 
 
std::string fnI2S(unsigned value, bool notnullterminated = true, unsigned padding = 0, bool asHexadecimal = false, bool withHexPrefix = false)
{
	std::ostringstream os;
	std::string result;
	if (asHexadecimal) {os << std::hex;}
	if (value < 10 && padding) {os << 0;}
	if (value < 100 && padding > 1) {os << 0;}
	if (value < 1000 && padding > 2) {os << 0;}
	if (notnullterminated)
		os << value;
	else
		os << value << std::ends;
	if (asHexadecimal && withHexPrefix) {result = std::string("0x");}
	result += os.str();
	return result;
};
 
std::string fnF2Sfixed(float value, bool nullterminated = false, unsigned p = 1)
{
	std::ostringstream os;
	os.setf(std::ios::fixed);
	os.precision(p);
	if (nullterminated)
		os << value << std::ends;
	else
		os << value;
	return os.str();
};
 
 
 
/*
 * 'sy.partifymesh' command is derived from 'CLxBasicCommand'.
 *
 */
class CSyPartifyMesh : public CLxBasicCommand {
public:
	static LXtTagInfoDesc           descInfo[];
 
	CLxUser_LayerService            LayerService;
 
	#ifdef	_DEBUG
	CLxUser_Log                     PartifyMeshLog;
 
	CSyPartifyMesh() {
		PartifyMeshLog.setByName(LOG_NAME);
	};
	#endif	_DEBUG
 
 
	void basic_Notifiers() override {
		basic_AddNotify(LXsNOTIFIER_SELECT, "item +d"); /* Will be notified when Item selection changes */
		basic_AddNotify(LXsNOTIFIER_MESHES, "+p +d");   /* Will be notified on polygon changes in the mesh */
	};
 
 
	bool basic_Enable(CLxUser_Message &msg) override {
		/*
			Utilize the new (601) CLxItemSelectionType to find the first mesh item and then check if it
			has any polygons.
		*/
 
		bool result(false);
 
		CLxItemSelectionType	TypedItemSelector(LXsITYPE_MESH);
		CLxUser_Item			theItem;
		const char				*ItemName;
 
		if (TypedItemSelector.GetFirst(theItem) && theItem.test())
		{
			if (theItem.Type() == iType_Mesh.Type())
			{
				CLxUser_Mesh			theMesh;
				CLxUser_ChannelRead		aChanReader;
 
				if (aChanReader.from(theItem))
				{
					if (aChanReader.Object(theItem, LXsICHAN_MESH_MESH, theMesh))
					{
						result = theMesh.NPolygons() > 0;
						if (!result)
						{
							theItem.UniqueName(&ItemName);
							if (ItemName) {
								msg.SetCode(LXe_FAILED);
								msg.SetMessage(SERVER_NAME, "ItemHasZeroPolygons", 0);
								msg.SetArgumentString(1, ItemName);
							}
						}
					}
					else
					{
						/* Unlikely to ever get here */
						msg.SetCode(LXe_CMD_DISABLED);
						msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
					}
				}
 
				aChanReader.clear();
				theMesh.clear();
			}
			else
			{
				msg.SetCode(LXe_CMD_DISABLED);
				msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
			}
			theItem.clear();
		}
		else
		{
			msg.SetCode(LXe_CMD_DISABLED);
			msg.SetMessage(SERVER_NAME, "NoValidMesh", 0);
		}
		return result;
	};
 
 
	int basic_CmdFlags() override {
		return LXfCMD_UNDO;
	};
 
 
	void cmd_Execute(unsigned int flags) override {
		/*
		    We restrict the LayerScan's results to the 'Primary' selected mesh only so that we
		    guard against the user having multiple 'selected' meshs which could result in overload.
		*/
 
		CLxUser_LayerScan               LayerScan;
		CLxUser_Item                    theItem;
		CLxUser_Mesh                    theMesh;
		CLxUser_Polygon                 thePolygon;
		CLxUser_StringTag               theTag;
		const char                      *ItemName;
 
		LayerService.BeginScan(LXf_LAYERSCAN_PRIMARY | LXf_LAYERSCAN_WRITEMESH, LayerScan);
 
		if (LayerScan.ItemByIndex(0, theItem))
			theItem.UniqueName(&ItemName);
 
		/* As there should only ever be 1 mesh found by the LayerScan it's index should be zero. */
		if (LayerScan.EditMeshByIndex(0, theMesh))
		{
			int NoOfPolygons = theMesh.NPolygons();
 
			// Set 'thePolygon' as the polygon accessor for the 'theMesh'.
			thePolygon.fromMeshObj(theMesh);
 
			// Set 'theTag' as the 'tags' accessor for the polygon.
			theTag.set(thePolygon);
 
			std::string tag;
			clock_t timerStart = clock();
 
			#ifdef _DEBUG
			unsigned msg_stride = NoOfPolygons / 5;
			unsigned msg_counter = 1;
			#endif	_DEBUG
 
			for (int i1 = 0; i1 < NoOfPolygons; i1++)
			{
				/*Ask the polygon accessor to populate itself based on the poly index. */
				thePolygon.SelectByIndex(i1);
 
				/* Construct a unique 'part' tag name. */
				tag = "poly_" + fnI2S(i1);
 
				/* Ask the 'tags' accessor to set the 'PART' tag. */
				theTag.Set(LXi_PTAG_PART, tag.c_str());
 
				#ifdef  _DEBUG
				//logging at 20%, 40%, 60%, 80%
				if (PartifyMeshLog && (i1 == (msg_stride * msg_counter)))
				{
					clock_t timerDiff = clock() - timerStart;
					double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
					switch(msg_counter)
					{
					case 1:
						PartifyMeshLog.Message(LXe_INFO, "20\%%, %u polygons in %.6f seconds.", i1, duration);
						break;
					case 2:
						PartifyMeshLog.Message(LXe_INFO, "40\%%, %u polygons in %.6f seconds.", i1, duration);
						break;
					case 3:
						PartifyMeshLog.Message(LXe_INFO, "60\%%, %u polygons in %.6f seconds.", i1, duration);
						break;
					case 4:
						PartifyMeshLog.Message(LXe_INFO, "80\%%, %u polygons in %.6f seconds.", i1, duration);
						break;
					}
					msg_counter += 1;
				}
				#endif  _DEBUG
			}
 
			/* Tell the LayerScan that we are only 'editing' polygon tags. */
			LayerScan.SetMeshChange(0, LXf_MESHEDIT_POL_TAGS);
 
			/* Apply the mesh changes. */
			LayerScan.Apply();
 
			clock_t timerDiff = clock() - timerStart;
			double duration = (double)timerDiff / (double)CLOCKS_PER_SEC;
 
			basic_Message().SetCode(LXe_INFO);
			basic_Message().SetMessage(SERVER_NAME, "Complete", 0);
			basic_Message().SetArgumentInt(1, NoOfPolygons);
			basic_Message().SetArgumentString(2, fnF2Sfixed((float)duration,false,6).c_str());
 
			#ifdef   _DEBUG
			PartifyMeshLog.Message(LXe_INFO, "100\%%, %u polygons in %.6f seconds.", NoOfPolygons, duration);
			#endif  _DEBUG
		}
		else
		{
			/* Unlikely to ever get here */
			basic_Message().SetCode(LXe_CMD_DISABLED);
			basic_Message().SetMessage(SERVER_NAME, "NoValidMesh", 0);
 
			#ifdef  _DEBUG
			PartifyMeshLog.Message(LXe_NOTFOUND, "No valid mesh found!");
			#endif _DEBUG
		}
	};
 
 
};
 
 
LXtTagInfoDesc CSyPartifyMesh::descInfo[] = {
	{LXsSRV_LOGSUBSYSTEM, LOG_NAME},
	{LXsSRV_USERNAME, SERVER_USERNAME},
	{0}
};
 
 
void initialize() {
		CLxGenericPolymorph *srv = new CLxPolymorph<CSyPartifyMesh>;
 
		srv->AddInterface(new CLxIfc_Command    <CSyPartifyMesh>);
		srv->AddInterface(new CLxIfc_StaticDesc <CSyPartifyMesh>);
 
		lx::AddServer(SERVER_NAME, srv);
};
 
 
void cleanup() {};
 
};//namespace sy_partifymesh_cmd

Config

The following xml file is what's referred to as a config and would form part of your plugin's release files for your kit. This particular config has the following features :-

  • A new command category is created to apportion this particular command under 'Sy/Cmds' in the modo command tree listing.
  • We specify command help details regarding what the command does.
  • We define a message table for localized error messages.
  • We specify a help url for this command.
  • We detail some UI elements relating to icons. See icon resources for more details.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 
  <atom type="Categories">
    <hash type="Category" key="Commands">
      <hash type="C" key="sy.partifymesh">Sy/Cmds</hash>
    </hash>
  </atom>
 
  <atom type="CommandHelp">
    <hash type="Command" key="sy.partifymesh@en_US">
      <atom type="UserName">PartifyMesh</atom>
      <atom type="Desc">Apply unique 'part' tag to each polygon in selected mesh.</atom>
      <atom type="Tooltip">Apply unique 'part' tag to each polygon in selected mesh.</atom>
      <atom type="ButtonName">PartifyMesh</atom>
      <atom type="Example">sy.partifymesh</atom>
    </hash>
  </atom>
 
  <atom type="Messages">
    <hash type="Table" key="sy.partifymesh.en_US">
      <hash type="T" key="ItemHasZeroPolygons">&quot;%1&quot; has zero polygons.</hash>
      <hash type="T" key="NoValidMesh">No valid mesh selected.</hash>
      <hash type="T" key="Complete">&quot;%1&quot; polygons completed in &quot;%2&quot; seconds.</hash>
    </hash>
  </atom>
 
  <atom type="HelpURLs">
    <hash type="HelpURL" key="command:sy.partifymesh">kit_SyCmds:help\index.html#partifymesh</hash>
  </atom>
 
  <atom type="UIElements">
    <hash type="Image" key="sy_cmd_icons">sy_cmd_icons.png</hash>
 
    <!-- First row of image -->
    <hash type="Icon" key="sy.partifymesh_20">         <atom type="Source">sy_cmd_icons</atom>     <atom type="Location">0 0 20 20</atom>   </hash>
 
    <!-- Second row of image -->
    <hash type="Icon" key="sy.partifymesh_32">         <atom type="Source">sy_cmd_icons</atom>     <atom type="Location">0 20 32 32</atom>  </hash>
  </atom>
 
</configuration>

Icon Resources

The following .png file is provided as an example of the icon images that commands mapped to UI buttons may implement, the display of which is an automated process. When the command returns 'false'/LXe_CMD_DISABLED from basic_Enable, the system automatically generates a desaturated version of the icon image for the button. See the icon resources article for more details.

Sy cmd icons.png - 20px on first row, 32px on second row.

More Information