Python API Overview

From The Foundry MODO SDK wiki
Jump to: navigation, search

Persistent Interpreter

In addition to the Python contexts that are used to execute scripts, there is also a persistent interpreter. This can execute lines or blocks like an interactive Python session, especially through the command history. This is also used as the context for Python-based server classes.

Headless modo can be run using a Python shell by starting it with the -console:python command argument. You then get the ">>>" prompt and can enter multi-line Python expressions and statements.

Direct Bindings

The "lx" Python module now supports new classes and other attributes that provide much more direct and immediate access to modo internals. The objects map directly to SDK interfaces, and classes created in Python can be exported to act as native objects inside modo.

Service Objects

Service SDK interfaces can be instanced from the "lx.service" module. For example, to get the current time from the selection service do this:

  sel_svc = lx.service.Selection()
  t = sel_svc.GetTime()

This is almost exactly equivalent to the C++ code which does the same thing.

 CLxUser_SelectionService sel_svc;
 double t = sel_svc.GetTime ();

Imported Objects

Objects are imported from modo into Python as the return value from a method returning an object. In most cases this will be an object implementing the primary interface for the returned object. For example, getting an item from a scene returns an item object.

  render_item = scene.AnyItemOfType(render_type)
  lx.out(render_item.UniqueName())

This is similar to the C++ code, although that creates the wrapper first and passes it in to the method to get initialized.

 CLxUser_Item render_item;
 scene.AnyItemOfType (render_type, render_item);
 
 std::string str;
 render_item.GetUniqueName (str);
 printf ("%s\n", str.c_str ());

Some methods return objects of unknown type. In that case the object has to be "cast" to the desired type by creating a wrapper object and initializing it with the unknown object. In this example a ValueReference object -- which can be anything -- returns an unknown object which is then cast to a Deformer.

  def = lx.object.Deformer(val_ref.GetObject())

Polymorphism

Casting can also be used to access the polymorphic interfaces on an object. For example a command object has a Command interface as its main API, but presents an Attributes interface as well.

  cmd  = cmd_service.Spawn(0, "item.name")
  attr = lx.object.Attributes(cmd)

The set() method on objects can also be used to initialize a wrapper from other existing objects.

  attr = lx.object.Attributes()
 
  cmd = cmd_svc.Spawn(0, "item.channel")
  attr.set(cmd)
  attr.SetString(0, "enable")

Exported Objects

Exporting objects means declaring them as Python classes and passing those objects into the object arguments of appropriate methods. This class derives from Visitor (aka ILxVisitor), so it has only the vis_Evaluate() method. This will be used when enumerating polygons to count triangles and non-triangles.

 import lxifc
 
 class TriangleVisitor(lxifc.Visitor):
     def __init__(self,poly):
         self.poly = poly
         self.n_tri = 0
         self.n_not = 0
     def vis_Evaluate(self):
         if (self.poly.VertexCount() == 3):
             self.n_tri += 1
         else:
             self.n_not += 1

The base classes for the various types of exported objects are defined in the lxifc module, and a class can inherit from multiple interface classes.

Enumeration starts with a mesh, gets a polygon accessor, and then passes a new instance of the class for enumeration.

 poly = mesh.PolygonAccessor()
 tvis = TriangleVisitor(poly)
 poly.Enumerate(lx.symbol.iMARK_ANY, tvis, 0)

Symbols

Symbolic values are available as either lx.symbol or lx.result. Symbols are basically all the LX defines, minus the LX; results are the LXe_ defines minus that prefix. Some examples of symbols:

 lx.symbol.s_ACTIONLAYER_SETUP
 lx.symbol.sSELTYP_ITEM
 lx.symbol.iMARK_ANY
 lx.symbol.fRAY_SCOPE_IMPLICITSURF
 lx.symbol.e_FAILED
 lx.result.FAILED

Help

The methods of the various objects have some minimal docs, at least sufficient to see the calling convention. For example, if you evaluate the following Python:

 lx.service.Log().CreateEntryMessage.__doc__

You get:

 LogEntry object = CreateEntryMessage(integer type,string message)

This tells you that LogService.CreateEntryMessage() takes two arguments and returns a LogEntry object. You have to refer to the primary SDK docs to discover that the integer "type" argument is actually an LxResult.

Storage Buffers

Some SDK methods require allocated data buffers. This is an unfortunate aspect of natural C that doesn't map directly to anything in Python. In order to call those methods we provide a storage object, which operates as a proxy for buffers.

For example, the C method for reading a pixel in an image is declared like this:

        LxResult
GetPixel (
        LXtObjectID              self,
        unsigned int             x,
        unsigned int             y,
        LXtPixelFormat           type,
        void                    *pixel)

The caller provides a pixel format and a buffer to receive the color data. The length and type of the buffer depends on the format, so LXiIMP_GREY8 needs a one-byte buffer and LXiIMP_RGBAFP needs a four-float buffer. To call this method from Python requires allocating a storage object of the right size.

 pixel = lx.object.storage()
 pixel.setType('f')
 pixel.setSize(3)
 
 image.GetPixel(x, y, lx.symbol.iIMP_RGBFP, pixel)
 rgb = pixel.get()

The setSize method allocates the buffer. Color values are deposited into it by the image method and can be read out in Python as a tuple using the get method.

As another example, the GetLine() method for images is defined like so:

        const void *
GetLine (
        LXtObjectID              self,
        unsigned int             y,
        LXtPixelFormat           type,
        void                    *buf)

It also takes a pixel type, plus a temp buffer that the client provides for the image to use if the native image format needs to be converted to the requested type. The return value is a pointer to the line, either from the image itself or copied into the temp buffer. Creating and allocating the buffer can be combined into a single statement, and calling the method returns an argument.

 tmp = lx.object.storage('b', w * 4)
 line = image.GetLine(y, lx.symbol.iIMP_RGBA32, tmp)

The returned line is another storage object. Since it was just created it needs to be initialized as well based on the type and amount of data that we expect to find there.

 line.setType('b')
 line.setSize(w * 4)

Once initialized it can be iterated, in this example to extract all the alphas.

 alpha_list = []
 for i in range(w):
     line.offset(i * 4)
     rgba = line.get(4)
     alpha_list.append(rgba[3])

Because GetLine() returns a const buffer, attempts to write to the buffer will fail.

Python Servers

It's possible to use classes declared in the top-level context (the persistent interpreter) as servers. Your exported Python class becomes a first-class plug-in. This is the minimum command server that I've been able to test:

 import lxu.command
 
 class HelloCmd(lxu.command.BasicCommand):
     def basic_Execute(self, msg, flags):
         lx.out("Hello World!")
 
 lx.bless(HelloCmd, "hello.world")

The HelloCmd class inherits from the implementation class for Command allowing it to act as a Command COM object. The class implements the two required methods -- Enable() and Execute() -- with Enable() just returning OK. The lx.bless() call promotes the exported class to a server. The type of server comes from the first inherited type, and the name is given by the second argument.

It's now possible to fire the command just like any other.

 lx.eval("hello.world")

Because these objects are created by modo on demand, the __init__ method (if any) should only take self as an argument.

Server Tags

Some servers need to support tags. This is done using an optional third dictionary argument giving the tag name and value. This lx.bless() call declares a saver class as an image saver server.

 tags = {
     lx.symbol.sSRV_USERNAME: "Super Saver",
     lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_IMAGE,
     lx.symbol.sSAV_DOSTYPE : "SSS"
 }
 lx.bless(MyImageSaver, "superSaver", tags)

Auto-Import

Python servers can be added to the system by executing their lx.bless() call from the main interpreter, which can be done by importing their modules. This can also be done automatically by adding *.py files to the "lxserv" sub-directory anywhere on the script path as defined during startup. Any modules found in any of these directories are automatically imported inside the lxserv module. Since this is accessible from the persistent interpreter, it's easy to access the internal state of your server module for testing or other purposes.

The startup script path contains:

  • resource:
  • module:Scripts
  • configs:
  • scripts:
  • kits:
  • any kit 'import' path

The following Python will query the script path for a specific system:

ps = lx.service.Platform()
[ ps.ImportPathByIndex(i) for i in range(ps.ImportPathCount()) ]

Utilities

Utility classes are in the lxu package.

 import lxu.select

Selections

Selection utility classes have a current() method to return the list of selected elements.

Scene selection:

 scsel = lxu.select.SceneSelection()
 current_scene = scsel.current()

Item selection:

 itsel = lxu.select.ItemSelection()
 for item in itsel.current():
     lx.out(item.UniqueName())

Attributes

The DynamicAttributes class exports an Attributes interface, and stores values in a list.

  • dyna_Add(name, type) -- add an attribute of the given name and type, which starts unset
  • dyna_SetType(index, type) -- change a type or clear an attribute's value
  • dyna_IsSet(index) -- return true if the attribute is set

These methods implement the Attributes interface, and can be called to get and set values:

  • attr_Count()
  • attr_Name(index)
  • attr_TypeName(index)
  • attr_Lookup(name)
  • attr_Type(index)
  • attr_Value(index,writeOK)
  • attr_GetInt(index)
  • attr_SetInt(index,val)
  • attr_GetFlt(index)
  • attr_SetFlt(index,val)
  • attr_GetString(index)
  • attr_SetString(index,val)

This is used to create object that present an Attributes interface.

 class MyAttrs(lxu.attributes.DynamicAttributes):
     def __init__(self):
         lxu.attributes.DynamicAttributes.__init__(self)
         self.dyna_Add('name', lx.symbol.sTYPE_STRING)
         self.dyna_Add('size', lx.symbol.sTYPE_DISTANCE)

Python Samples

API Code Snippets and Example Scripts

More Information