Python Command Overview
- 1 Python API Command Overview
- 1.1 The Entire Command
- 1.2 Breakdown
- 1.2.1 The Header
- 1.2.2 The Command Class & Init
- 1.2.3 Argument Definition (Optional)
- 1.2.4 User Friendly Command Display
- 1.2.5 User Friendly Argument Display
- 1.2.6 Command Flags
- 1.2.7 Main Execution
- 1.2.8 Blessing the Command
- 1.3 Sample Command Help
Python API Command Overview
This page will attempt to give some "boilerplate" code for creating a Python API command (with arguments) in MODO. There is much more you can do with commands, but this should cover common uses.
We'll start with listing the entire code and then breaking it down.
The Entire Command
#!/usr/bin/env python import lx import lxu.command class MyCommand_Cmd(lxu.command.BasicCommand): def __init__(self): lxu.command.BasicCommand.__init__ (self) self.dyna_Add ("myBoolean", lx.symbol.sTYPE_BOOLEAN) self.dyna_Add ("myDistance", lx.symbol.sTYPE_DISTANCE) self.basic_SetFlags (1, lx.symbol.fCMDARG_OPTIONAL) self.dyna_Add ("myString", lx.symbol.sTYPE_STRING) self.basic_SetFlags (2, lx.symbol.fCMDARG_OPTIONAL) def basic_ButtonName(self): return "My Command" def cmd_Tooltip(self): return "Tooltip for my command." def cmd_UserName(self): return "My Command Dialog" def arg_UIHints (self, index, hints): if index == 0: hints.Label ("A Boolean") elif index == 1: hints.Label ("A Distance") elif index == 2: hints.Label ("A String") def cmd_Flags(self): return lx.symbol.fCMD_UNDO | lx.symbol.fCMD_MODEL def basic_Execute(self, msg, flags): boolean = self.dyna_Bool (0, False) distance = self.dyna_Float (1, 0.0) string = self.dyna_String (2, "") lx.out("Boolean: %s" % boolean) if not self.dyna_IsSet (0): lx.out("...but you didn't set it.") lx.out("Distance: %sm" % distance) if not self.dyna_IsSet (1): lx.out("...but you didn't set it.") lx.out("String: %s" % string) if not self.dyna_IsSet (2): lx.out("...but you didn't set it.") lx.bless (MyCommand_Cmd, "my.command")
#!/usr/bin/env python import lx import lxu.command
This piece simply tells MODO this is a Python file, and imports the basic modules we need for creating a command. You may require other modules depending on the functions you intend to carry out in your command.
The Command Class & Init
class MyCommand_Cmd(lxu.command.BasicCommand): def __init__(self): lxu.command.BasicCommand.__init__ (self)
This section defines a new class which inherits from lxu.command.BasicCommand.
The __init__ function is called when the command is loaded by MODO at start up.
At the very least, this function should contain lxu.command.BasicCommand.__init__ (self)
Argument Definition (Optional)
self.dyna_Add ("myboolean", lx.symbol.sTYPE_BOOLEAN) self.dyna_Add ("mydistance", lx.symbol.sTYPE_DISTANCE) self.basic_SetFlags (1, lx.symbol.fCMDARG_OPTIONAL) self.dyna_Add ("mystring", lx.symbol.sTYPE_STRING) self.basic_SetFlags (2, lx.symbol.fCMDARG_OPTIONAL
In this example, we have also defined arguments for our command. These are not required, but must go inside the __init__ function if you wish your command to have arguments. They are added to the command in the order they are defined here and assigned corresponding indices. NOTE: This order is important for accessing the argument values later on, as we will be accessing them via their index.
At their most basic, arguments are defined with a name and a type. The name is the internal name of the argument and should not contain spaces - we will give them user-friendly display names later on. The names can be used by the Command Help for this command to define user-friendly display names from a config file (which allows for localisation), but we won't be covering that here.
By default, all arguments are required to be set for the command to run. If, when the command is called and all the required arguments are not set, a dialog will be displayed to the user allowing them to enter the values.
However, you'll note that we've specified flags on two of the arguments. The flags are specified by referencing the index of the argument they apply to (see, order is important!) and giving the flag itself.
In this case, we've specified the flag of "OPTIONAL", which means that the user does not have to specify a value for the second and third arguments. We will have to make sure that we assign any unspecified arguments default values in the main execution of the command later on.
As a brief aside...
Although MODO appears to have several different argument types, they are all user-friendly wrappers for the storage of 4 core types; integers, floats, strings and objects.
You'll have seen these friendly wrappers for things like distance fields, where you can enter a value in metres, millimetres, feet, microns, etc... However, internally, these are read and stored as a simple float value which is the displayed distance as metres.
Similarly, angle fields, where you can enter a value in degrees, are stored internally as a float of the displayed angle in radians.
Boolean values (often shown as checkboxes or toggle buttons) are simply stored as integers which are 0 or 1.
NOTE: These internal values are what you'll be dealing with when you write commands.
User Friendly Command Display
def basic_ButtonName(self): return "My Command" def cmd_Tooltip(self): return "Tooltip for my command."
Here we define the string shown on the button when the command is placed in the UI.
We also define the tooltip shown when the user hovers over the button.
Both of these are optional.
NOTE: These should not really be defined inside the command, but instead placed in an accompanying config file using Command Help, which would allow for localisation of the names and separates the command's functionality from it's display. It's generally good practise to follow if you can.
User Friendly Argument Display
def cmd_UserName(self): return "My Command Dialog" def arg_UIHints (self, index, hints): if index == 0: hints.Label ("A Boolean") elif index == 1: hints.Label ("A Distance") elif index == 2: hints.Label ("A String")
These are only really relevant if you have arguments on your command. Also optional and also should really be defined in a Command Help config rather than here.
Here we define the title of the command dialog that pops up if the user didn't specify all required arguments (the User Name).
And we also define the user-friendly names of the arguments - these are the labels placed next to the fields in the dialog popup. Again, like many argument attributes, these are specified by their index.
def cmd_Flags(self): return lx.symbol.fCMD_UNDO | lx.symbol.fCMD_MODEL
This is a very important part of a command if you are using the Python API (or TD SDK) to edit the scene in the command execution. The command flags tell MODO what the command is expected to do when it executes and how to handle it.
In our case, we specify the standard flags; MODEL and UNDO. These flags are bit masks and as such are joined together into a single integer return value using the pipe | separator.
The MODEL flag tells MODO that we will be editing a part of the scene. The name is slightly misleading as it implies changes to a mesh only, however it means any change to anything in the scene; channels, meshes, selection changes, adding or removing items, etc...
The UNDO flag is specified by default as part of the MODEL flag, however it's not harmful to add the flag to be clear. This tells MODO that the command should be undoable and that it should set up an undo state for it. NOTE: It is very important that this flag is set, as changing the scene without this flag set causes instability in MODO and usually leads to a crash (if not immediately then very soon).
Generally, these should be your standard flags unless you have specific reason to change them.
def basic_Execute(self, msg, flags): boolean = self.dyna_Bool (0, False) distance = self.dyna_Float (1, 0.0) string = self.dyna_String (2, "") lx.out("Boolean: %s" % boolean) lx.out("Distance: %sm" % distance) if not self.dyna_IsSet (1): lx.out("...but you didn't set it.") lx.out("String: %s" % string) if not self.dyna_IsSet (2): lx.out("...but you didn't set it.")
This is the meat of the command - the code that's actually run when the command is fired.
Here, we're not doing anything other than reading the arguments and writing to the Event Log.
You can see here that those core types are how we read the arguments in the command, with dyna_Bool (a friendly wrapper for dyna_Int, checking if it's equal to 1 or 0), dyna_Float and dyna_String. These are accessed via index, the same indices we've used throughout the command. Optionally, a default value is given as the second parameter, which is the value returned if the argument has not been set by the user.
We can also make use of the command's dyna_IsSet function, which will return True or False depending on whether the argument with that index was specified by the user (this function is what is used internally for the dyna_Float and related functions, to determine whether to return the default value or not).
It is important to note that any arguments which have UI hints (such as minimum or maximum values - including text hints) are for UI purposes only, and that arbitrary values can be entered as arguments. This means that if you have a UI hint that gives it a range of 10-50, the user can still enter 12000 manually from a script or the command entry, or enter an out-of-range integer instead of a text hint string. So be sure to manage such values and take appropriate action if such values would cause problems in your code (e.g. aborting execution or limiting the value supplied by the user to the desired range).
Also note that, as this is part of the Python API, we can freely use lx.eval() for calling commands and querying values, just like regular fire-and-forget Python scripts.
Making Main Execution Useful
This is a simple outline of writing a command with arguments. It doesn't actually do anything. For further reading, you can find other examples of code to go into the Execute function on the wiki (such as Creating a Selection Set).
Blessing the Command
Here, we call the bless command. This promotes the class we created to be a fully fledged command inside of MODO, as opposed to simply a Python script. It takes two arguments. One is the command class we defined, the second is the command that we'll want to assign to this inside MODO.
This means that the script is not run via the usual @scriptName.py command that fire-and-forget scripts are. Instead, this command is run be entering my.command as it is a proper command in MODO.
lx.bless (MyCommand_Cmd, "my.command")
Sample Command Help
Throughout this page, it's been noted that we should really be using Command Help for defining the user-friendly names of things.
This is an example of what the command help for this command would look like. This would be inside a .cfg file that is distributed with the .py file of the actual command.
If distributed with this config, you would be able to remove all of the above command's functions which relate to returning user-friendly names for things and MODO would use this file to get the relevant values automatically.
Also worth noting that these can be localised by duplicting the <hash type="Command" key="my.command@en_US">' fragment and it's contents and changing the @en_US to @[localised language code] then replacing the contents of the atom fragments accordingly.
By default, MODO will look for @en_US and this will also be the fallback if the user's specified language isn't found.
<?xml version="1.0"?> <configuration> <atom type="CommandHelp"> <!-- note the command's name in here; my.command --> <hash type="Command" key="my.command@en_US"> <atom type="UserName">My Command Dialog - also shown in the command list.</atom> <atom type="ButtonName">My Command</atom> <atom type="Tooltip">My command's tooltip</atom> <atom type="Desc">My command's description - shown in the command list.</atom> <atom type="Example">my.command true 1.5 "hello!"</atom><!-- An example of how my command would be called - shown in the command list. --> <hash type="Argument" key="myBoolean"> <atom type="UserName">A Boolean</atom> <atom type="Desc">The boolean argument of my command - shown in the command list.</atom> </hash> <hash type="Argument" key="myDistance"> <atom type="UserName">A Distance</atom> <atom type="Desc">The distance argument of my command - shown in the command list.</atom> </hash> <hash type="Argument" key="myString"> <atom type="UserName">A String</atom> <atom type="Desc">The string argument of my command - shown in the command list.</atom> </hash> </hash> </atom> </configuration>