Guidelines and notes

Python Editor

This can help speeding up the coding process and exploration phase.

  • Tab Shows word completion dialog when the text cursor is positioned behind a dot.
  • Ctrl+Enter executes the selected text. Without a selection, all text is executed.
  • Ctrl+Shift executes the current line
  • Ctrl+/ Comments or un-comments the selected text (toggle)
  • Ctrl+] Indents the selected text
  • Ctrl+[ Unindents the selected text
  • Appending a question mark to an object and pressing tab displays the doc-string of the object

The scripting layout contains a menu with access to various code templates and common folders.

_images/PythonScriptEditor.png

The Python Script Editor Palette can be opened in a separate floating window from the Layout menu as shown on the right.

Properties

Properties can be recognized by the keywords Getter and Setter listed in the documentation.

Commands and undo

Undo context for servers and PySide

A core principle of Modo’s core SDK is embedding code that makes changes to a scene’s state into new Modo commands.

They provide an undo context that is a basic requirement when creating new plugin servers such as Listeners and Modifiers or when creating windows using PySide, where a missing undo context leads to error messages. Writing new kinds of servers require the plugin author to provide an undo context by creating and using custom commands.

Code that is executed from the Python script editor is embedded into a command internally for the convenience of quick prototyping and exploration.

The use of the TD SDK from fire and forget style scripting is partially limited because of being executed in a separate ‘throw away’ Python interpreter that does not allow the creation of persistent servers because of it’s temporary nature.

Please have a look at this explanation on the Modo SDK wiki for more detailed insight about Commands and Plug-ins

Note that it is not allowed for servers to run commands while an undo process it happening, this potential issue can be avoided by testing for a valid undo state beforehand.

In the Event Listener Callbacks the undo state is tested like so:

undoService = lx.service.Undo()
if not undoService.State() == lx.symbol.iUNDO_ACTIVE:
    return

Commands

The Command System is Modo’s basic form of automation and the native way of integration with the user interface. As the TD SDK works in conjunction with commands, knowing their syntax and basic usage is needed.

Hit F5 to open the script history, which lists commands that have been fired by the user when interacting with the Modo in the ‘History’ tab. There is a recording button to the bottom right that lets the user record commands into macros.

_images/CommandHelp.png

The ‘Commands’ tab lets you browse and inspect the descriptions and signatures of all known commands. Checking the ‘F’ button to the top right allows for filtered searching.

To call a Modo Command from Python use the lx.eval function as in this example:

# This renames the item 'mesh021' to 'Jack'.
lx.eval('item.name Jack item:mesh021')

In the above image, arguments preceded with a question mark indicate that they can be queried and the ones surrounded by <> are optional. The ampersand (&) indicates an exo pointer that will internally look up the item by the provided ident ‘mesh021’. Arguments can be optionally specified by keyword using a colon as in the above image

Examples:

# Here ? is passed for the first argument ("name"). In actual code, it is appended to the argument's name.
# This prints the name of the selected objects
lx.eval('item.name name:?')
# Result: ('Jack', 'Camera')

# One can choose to not specify the argument's name. Then, the command's order of arguments is used implicitly,
# so the question mark below is assumed to refer to "name"
lx.eval('item.name ?')

# Now a second argument "item" is given. Now rather than the name of all the selected items, the name of a specific item - "mesh021" is returned.
lx.eval('item.name ? item:mesh022')
# Result: Jack

The output of commands and log messages are printed to the Event Log view.

When passing a string containing spaces to a command, it needs to be surrounded by quotes or curly braces.

Print text into the Event Log from Python is possible using the lx.out method:

lx.out('Hello world!')

To learn more about Modo’s command system please refer to the Command section in the Modo SDK wiki

A table of the command modifiers for quick reference can be found here

Creating Modo Commands in Python

In order to be automatically registered at startup, Python files that define commands are placed in the “lxserv” folder inside the scripts- or kit directory.

_images/CommandTemplateMenu.png
  • In the script editor, select Add New->Command Plugin Kit from the drop down menu at the top right.
  • Enter a name for the Kit in the popup dialog, for example ‘MyPlugin’
  • Enter a file name for first command you are adding, for example ‘cmd_cloneItem’.
  • This should open a template file in an external text editor.

Template:

import lx
import lxifc
import lxu.command


class CmdMyCustomCommand(lxu.command.BasicCommand):
    def __init__(self):
        lxu.command.BasicCommand.__init__(self)

    def cmd_Flags(self):
        return lx.symbol.fCMD_MODEL | lx.symbol.fCMD_UNDO

    def basic_Enable(self, msg):
        return True

    def cmd_Interact(self):
        pass

    def basic_Execute(self, msg, flags):
        lx.notimpl()

    def cmd_Query(self, index, vaQuery):
        lx.notimpl()


lx.bless(CmdMyCustomCommand, "replace.me")

The remaining steps are optional.

As commands can be only blessed (registered) once per Modo session it is advisable to keep the main code in a separate file to the commands and have the command import and/or reload the main code file as a Python package. This avoids having to re-bless the command while it allows to keep making changes to the code in the files.

_images/OpenContentFolder.png
  • From the Modo’s main menu, select System->Open Content Folder, and open your plugin folder under Kits, that you just created.

  • We want to create a folder that can be imported as a python package. It is important to choose a name that is unique

  • in order to avoid clashing with other installed python package names. Don’t call it something generic as ‘dev’ or ‘modules’. Let’s name it ‘myPythonModule’.

  • Create a ‘__init__.py’ file inside that package folder and paste the code below into it:

    import modo
    
    def cloneSelectedItems():
        # makes duplicates of the selected items and suffixes names with '_copy'
        scene = modo.Scene()
        for item in scene.selected:
            duplicate = scene.duplicateItem(item)
            duplicate.name = '%s_copy' % item.name
    
    # Note that Modo already has a superior command to this called 'item.duplicate', this is only an example
    
  • Now edit the python file containing the template command class that was generated for you, ‘cmd_cloneItem.py’ in this case

Add an import statement somewhere at the top:

import myPythonModule
  • Rename the class name to CmdCloneItem:

    class CmdCloneItem(lxu.command.BasicCommand):
    
  • Replace the basic_Execute function to look like this:

    def basic_Execute(self, msg, flags):
        reload(myPythonModule)
        myPythonModule.cloneSelectedItems()
    

Be sure to remove the reload statement later for release, it is handy to have the code update every time the command is run during development but redundant for the end user.

  • Lastly, choose a name for the command to be called by:

    lx.bless(CmdCloneItem, "item.clone")
    
  • After restarting Modo test the command by entering the command into the command prompt and pressing enter.

  • Make some changes to the __init__.py file and run the command again. It should reflect your changes.

The big picture

Overview of a Modo scene

This refers to Modo’s core SDK but also applies to the TDK SDK wrappers for the most part.

Scenes

Items

Tags

Channels

Actions

Packages

Graphs

Container

The objects in the scene. Has a type that defines channels and what interfaces can be used

String data attached to an item or polygon. Assemblies and Actors share the type of group and are distinguishable by different string tags.

Hold properties and values of items inside actions

Contains channel’s values, in the form of values (constant), envelopes (animation curves), gradients (constant function) or as baked animation sequence.

Extend channels and interfaces of items by attaching to them.

Define relationships between items

Graphs

There are two basic types of graphs: The ItemGraph for connections from item to item and the ChannelGraph for connections from channel to channel. There are many specialized graph types and new custom types can be created using Modo’s core SDK.

Some of the he most common graphs are:

Constant String Purpose
LXsGRAPH_PARENT “parent” Defines the parenting relationships between items
LXsGRAPH_DEFORMERS “deformers” Deformer items link to the targets and overrides with the deformers graph ...
None “deformTree” ... They can be grouped and organized with the deform-tree graph
LXsGRAPH_XFRMCORE “xfrmCore” Transform items
LXsGRAPH_SOURCE “source” Mesh Instances
LXsGRAPH_CHANLINKS “chanLinks” Links between channels
LXsGRAPH_CHANMODS “chanMods” Channel modifiers

The full list of constants can be found in the lxidef.h header file in the SDK or the lx.symbol Python module.

Actions As mentioned earlier, the data of channels lives in actions. Think of them as layers in an image editing software.

Action Purpose
Edit action Channel changes are usually temporarily stored here and delegated to a appropriate destination action when the scene is updated
User actions If the channel is assigned to an action by the user, the data will be stored here.
Scene action Animated channel data is otherwise stored in the scene action.
Setup action Static channel values are stored at the bottom of the stack, the setup action.

ItemGraphs in the TD SDK

Here is how to find the graphs currently connected to an item

import modo

# Grabbing the mesh and camera that come with the blank scene
mesh = modo.Mesh('Mesh')
camera = modo.Camera('Camera')

# Parent the mesh to the camera
lx.command('item.parent', item = mesh.id, parent=camera.id, inPlace=1, position=0)

# Add a deformer to the mesh
lx.command('item.addDeformer', type='deform.bend', mesh=mesh.id)

# This method returns a list containing the string names of the connected graphs
print mesh.itemGraphNames
# Result: ['deformers', 'parent', 'xfrmCore']

# Conversely, this method returns the ItemGraph objects
print mesh.itemGraphs
# Result: [modo.ItemGraph('mesh002', 'deformers'), modo.ItemGraph('mesh002', 'parent'), modo.ItemGraph('mesh002', 'xfrmCore')]

# When we parented the mesh, it was connected to the camera through the 'parent' graph.
# (Also, the 'deformers' connection exists because we added a deformer earlier, and 'xfmCore' exists because
# transform Items were added and connected automatically during the parenting)

# This will print all items that are connected via the 'parent' graph, revealing a forward connection to the camera.
print mesh.itemGraph('parent').connectedItems
# Result: {'Forward': [modo.Camera('camera003')], 'Reverse': []}

# Looking at the 'deformers' graph we get a similar result
print mesh.itemGraph('deformers').connectedItems
# Result: {'Forward': [], 'Reverse': [modo.GeneralInfluenceDeformer('genInfluence025')]}

# An ItemGraph in the TD SDK is treated as container object and has two ways to access it's members:

# Using the forward / reverse methods:
print mesh.itemGraph('deformers').reverse(0)
# Result: modo.GeneralInfluenceDeformer('genInfluence025')

# Using angular brackets. The ItemGraph's direction is reverse by default
print mesh.itemGraph('deformers')[0]
# Result: modo.GeneralInfluenceDeformer('genInfluence025')

# The direction of the ItemGraph can be changed using the setReverse method
parentGraph = mesh.itemGraph('parent')
parentGraph.setReverse(False)
print parentGraph[0]
# Result: modo.Camera('camera003')

# ItemGraphs initialized with different items can be connected using the >> operator
# Please not that this operator is not yet available for all possible cases.
# This parents the mesh to the light item
light = modo.DirectionalLight('Directional Light')
lightsParentGraph = light.itemGraph('parent')
parentGraph >> lightsParentGraph

Note that the schematic view does not reflect any particular graph as such and is considered a separate UI construct.

Graphs are not actually contained in items either, accessing them as members is a convenience construct of the TD SDK.

Accessing internal core SDK objects

The TD SDK classes embed core SDK objects through composition. The methods of these wrapped objects are not directly visible using introspection tools like ‘dir’, but can be used nevertheless.

The purpose is to keep the interface of the TD SDK classes separate to the interfaces of the wrapped objects.

Core SDK method calls are forwarded to the embedded object by the classes’ __getattr__ method if the method is not found in the class itself.

Many classes additionally have a property to return the object directly, for introspection and auto completion in the Python script editor.

modo.Item.internalItem returns a lxu.object.Item
modo.MeshGeometry.internalMesh returns a lx.object.Mesh
modo.MeshVertex.accessor returns a lxu.object.Point
modo.MeshPolygon.accessor returns a lxu.object.Polygon
modo.MeshEdge.accessor returns a lxu.object.Edge

In the core SDK, items are passed on and referred to by their idents.

Vertex, Poly and Edge-components of meshes are passed on and referred to by their IDs of type long, rather than by index.

Note that when retrieving multiple geometry accessors of one component type at once, they will all point to the same component because the geometry object in the TD SDK only keeps one shared accessor and ‘selects’ individual components on it in the property’s getter method beforehand. This can lead to confusion when used outside of a loop:

vertex_a = cube.geometry.vertices[0].accessor
print vertex_a.Index()
# Result: 0
print vertex_a.ID()
# Result: 91382072

vertex_b = cube.geometry.vertices[1].accessor
print vertex_a.Index()
# Result: 1
print vertex_a.ID()
# Result: 91382032

# vertex_a now points to vertex 1 because the method SelectByIndex(1) got called on shared accessor internally.

As a consequence one should not collect the accessor objects themselves, but rather their ID’s for later re-use. The core SDK way of iterating geometry components is by using visitor classes. TD SDK’s mesh editing methods are less efficient in comparison but easier to use.

As the TD SDK is being worked on, not all functionality is yet wrapped. Accessing the underlying objects might be helpful for accessing otherwise unreachable parts of Modo’s core Python SDK.

The corresponding documentation can be found on the Modo SDK wiki

Item idents

Items in Modo are internally referred to by a unique string called Ident, composed of the item type followed by numbers.

It is the string part seen when printing an item or it’s .id property:

print modo.Item('Cube').id
# Result: mesh021

The idents are always unique and persistent, with the exception that they can change when scenes are imported.

Finding an item’s channel name

The Channel Reference provides detailed information for the channels of all known item types.

The internal channel names are usually a shorter version that what is visible in the UI’s channel view.

The easiest way to find the corresponding channel name is to change the value in the UI and then look at the Command History (hit F5 to open it, make sure the ‘History’ tab is selected).

In this case the resulting line reveals that the changed channel name is “pfiltEnable”:

item.channel surfEmitter$pfiltEnable true

The channels available to an item can also be listed using the property Item.channelNames:

from pprint import pprint
import modo

item = modo.Item('Mesh')
pprint( item.channelNames )

# Result:
#['localMatrix',
# 'parentMatrix',
# 'wposMatrix',
# 'wrotMatrix',
# 'wsclMatrix',
# ...

Known Issues

Mesh editing related change in Modo 10.2

Instancing only works for meshes

To delete vertex maps, currently the Remove() method of the accessor needs to be used: myMap.accessor.Remove(). In the future MeshMaps will additionally support the del operator.