Python Plugin Structure
Last modified: 10 March 2025In this section, we will begin our introduction of LightWave Python by dissecting a Modeler Command Sequence script included with LightWave.
note
The complete text for this script can be found in:
support/plugins/scripts/Python/Modeler/CommandSequence/enumerate_surfaces.py
This example script is called “Enumerate Surfaces”, and its task is to identify all surfaces defined for all selected polygons, and append a sequential numeric value to each. We shall look at the individual pieces of the script, but at any time, you should feel free to load the script into your favorite text editor to see the plug-in source code as a whole.
All example Python scripts are formatted according to the recommended Python module organization. (see also this stackoverflow article
It is important that you are aware that, with few exceptions, certain plug-in elements that will be included in this analysis are absolutely required for an external Python script to be considered a LightWave plug-in. Having said that, however, there are some exceptions to this rule, and they are considered “single-shot” plug-in types. These type exceptions (introduced in LightWave v11.5) are discussed in the “Single-Shot” Exceptions section.
External Directives
The script starts out by providing some directives to “external agencies” – in other words, conveying important information to processes that might execute or otherwise process the contents of the file.
#! /usr/bin/env python
# -*- Mode: Python -*-
# -*- coding: ascii -*-
The first line is a traditional UNX directive known as a “she-bang” (or “pound-bang”). This is used by the UNX shell to determine what external application will be responsible for executing the file. This is similar in nature, for example, to the way Windows associates a file extension with an application. The inclusion of this directive is simply in the interests of completeness, as the script will typically not be executed directly from a UN*X shell.
The next line is a directive that provides a hint to text editors of the type of the contents of the file (in this case, “Python” code).
The last directive indicates the encoding of the text found within the file. In this case (and in the case of all the Python example scripts), the encoding is ASCII.
Module docstring
As with all included example scripts, this script has a Python module docstring that describes its purpose.
"""
This is a LightWave Command Sequence plug-in (Modeler) that appends a
running enumeration to the existing surface names of all selected
polygons in the foreground layer.
"""
Module Imports
Next are the Python imports of required modules. Along with the Python ‘sys’ module, this action pulls in the ‘lwsdk’ module, which provides your script with access to the exported LightWave SDK elements within the Python environment. All your plug-in’s interactions with LightWave will take place through the facilities provided by this package.
import sys
import lwsdk
In this form, all interactions with the ‘lwsdk’ module must be prefaced with that module’s name (or “namespace prefix”). An alternate form of import can be used to pull in all the contents of the module into the current script’s environment, providing direct access to the ‘lwsdk’ elements (i.e., no prefix required):
from lwsdk import *
This approach is discouraged, however, as it not only pollutes the script’s global namespace, but also increases the chances that an element name within the ‘lwsdk’ namespace will collide with an element name within the script’s global namespace.
If you are confident that names will not collide, you can also pull in individual elmements from the ‘lwsdk’ namespace for direct usage within your script using the selective import format:
from lwsdk import LWMessageFuncs, AFUNC_OK
In this case, these two elements may be used without the required ‘lwsdk’ namespace prefix.
Module Metadata
Python module metadata is used to provide more detailed information about the plug-in.
__author__ = "Deuce Bennett"
__date__ = "Aug 30 2024"
__copyright__ = "Copyright (C) 2024 LightWave Digital"
__version__ = "1.0"
__maintainer__ = "Deuce Bennett"
__email__ = "deuce@lightwave3dx.com"
__status__ = "Example"
__lwver__ = "2024.1.0"
With the exception of the last line __lwver__
, all of this metadata information is optional (as you can see from the “Hello, World” script at the start of this section), and is included only to follow good Python form. It is entirely ignored by The PCore System when analyzing and executing LightWave Python plug-ins.
warning
As mentioned, the "__lwver__" metadata tag is not ignored by PCore. It is a required item that not only flags the Python file as being a LightWave plug-in, but also declares the minimum version of LightWave with which the script will successfully function.
If you forget to include this metadata tag in your Python script, PCore will ignore it as a non-plug-in Python file.
The values that can appear in the __lwver__
metadata tag follow the pattern:
__lwver__ = "<major>.<minor>.<patch>"
Example: __lwver__= "2024.0.1"
Where each element is an integer value. For example, this means that a version value for LightWave v11 can appear as “11”, “11.0” or even “11.0.0”.
It appears simply as “11” above because the <minor>
and <patch>
values are optional. If the script had declared a minimum version of “11.5”, then it would contain elements that were only available in the 11.5 version of the LightWave SDK (or elements added by the PCore system), and will likely fail if it were executed in an earlier version.
note
As you can see in the case of this example script (and most example scripts, for that matter), it requires at least LightWave v11 in order to properly operate.
Be aware that this mechanism is not the only means of making your Python script function with a specific version of the LightWave. Using Python-specific mechanisms, you can specify a minimum version of LightWave that is earlier than certain elements within your script, but then you can adjust execution at run-time using Python’s try/except error trapping to function in a newer environment.
For example:
[...]
__lwver__ = "2024"
[...]
# see if an 2024 SDK item is available to us in this environment
bobwidget = None
try:
# get an instance of BobWidget
bobwidget = lwsdk.BobWidget()
except:
pass
if bobwidget:
# function with the v2024 SDK item
else:
# fall back to a v11 means of doing things
[...]
Interface Subclass
The real meat of a LightWave Python plug-in comes from subclassing one of the Handler Interfaces classes. It is via this mechanism that LightWave’s PCore subsystem can ‘glue’ your Python code to a given LightWave plug-in architecture, making it a first-class citizen of the product’s environment.
In the case of the CommandSequence example, we will subclass the Command Sequence Class‘s ICommandSequence interface, creating a new class with which the PCore system can instance and interact. Subclassing ICommandSequence is done using the normal Python class inheritance mechanism:
[...]
class enumerate_surfaces(lwsdk.ICommandSequence):
def __init__(self, context):
super(enumerate_surfaces, self).__init__()
In the case of this particular plug-in, the only action taken in its initialization function is to initialize its superclass instance.
Note the context argument provided to the class’s __init__()
function. It is always the case that a handler will be called with a context argument.
In most handler types, the value of this argument will have some specific type (or an opaque value that must be converted to a required type). In the rest, the context argument is only a placeholder with no relevant use.
The LightWave SDK documents the value of this context parameter on a class-by-class basis, so please check there for information about the context provided to a specific plug-in type.
In the case of CommandSequence, the context argument has no relevance, and will contain a value of ‘None’.
Method Override
If you examine the ICommandSequence interface (see the Command Sequence Class), you’ll note that it contains a single method called process(). When the CommandSequence plug-in is invoked by Modeler, this is the entry point it will use to activate the plug-in. In order to receive that activation, your Python script must “override” this method (i.e., provide its own version with the same argument counts) that has been inherited from the ICommandSequence base class.
note
If you fail to override an inherited base class method in your script, the PCore system simply stubs that function internally. This means that the expected callback method is always available, but will only be invoked within your script if you provide the override.
We add a method to the enumerate_surfaces class called process()
with the same argument count defined by the ICommandSequence interface’s method of the same name.
In this way, we effectively replace – override – the inherited method:
[...]
# LWCommandSequence -----------------------------------
def process(self, mod_command):
[...]
Since we are examining the anatomy of a LightWave Python plug-in (and not necessarily the functionality of the * enumerate_surfaces* plug-in), we will leave the gritty details of this method until later in this section. For now, simply note that the process()
method receives an instance of the LWModCommand access class through which the script will perform its mesh-editing tasks.
(You can review this class in the Command Sequence Class section.)
Plugin Metadata
Each LightWave plug-in must provide some amount of metadata that describes the plug-in itself. The main metadata container is called the ServerRecord. In C, this is a structure (refer to the LightWave SDK headers and documentation for a complete description). In Python, the ServerRecord takes the form of a map container declared at the global level. The PCore system will search the Python script for an instance of this value (it must actually be named “ServerRecord”) to determine what plug-ins are contained within.
warning
As with the __lwver__ metadata tag, if a ServerRecord instance cannot be found within the script’s global environment, then it will be considered a non-plug-in Python file.
A ServerRecord map contains a key/value pair for each plug-in defined within the file. The key value is an instance of the plug-in class’s Factory generator. In the case of our enumerate_surfaces plug-in, an instance of the CommandSequenceFactory is created. Examining the structure of CommandSequenceFactory class in Command Sequence Class, you will note that it expects a “name” parameter followed by a “klass” type reference.
When we generate an instance of CommandSequenceFactory in constructing our ServerRecord entry, we provide a “name” that will be used by LightWave to identify the plug-in (this is the internal plug-in name, not a user-friendly version; that will be defined later). We then provide a reference to the class to be created by the Factory each time LightWave requests a new instance.
[...]
ServerRecord = { lwsdk.CommandSequenceFactory("LW_PyEnumSurfaces", enumerate_surfaces) : ... }
The value part of the key/value pair is an instance of a ServerTagInfo. This is also a C structure defined within the LightWave SDK (see those documents for a full description). In defining a Python plug-in, you must construct a ServerTagInfo list to be provided as the value of the ServerRecord entry.
The ServerTagInfo defines numerous tag values for a plug-in. Those typically defined for a Python plug-in are:
[...]
ServerTagInfo = [
( "Python Enumerate Surfaces", lwsdk.SRVTAG_USERNAME | lwsdk.LANGID_USENGLISH ),
( "Enumerate Surfaces", lwsdk.SRVTAG_BUTTONNAME | lwsdk.LANGID_USENGLISH ),
( "Utilities/Python", lwsdk.SRVTAG_MENU | lwsdk.LANGID_USENGLISH )
]
USERNAME
From the SDK docs regarding SRVTAG_USERNAME
:
“The name displayed to the user in LightWave’s interface. Multiple user names for different locales
can be provided by combining this type code with different language IDs. LightWave attempts to
select the name that’s most appropriate for the locale of the user’s machine. Unlike the internal
server name, there are no restrictions on what the string may contain.”
In the case of this example, the user name string is in US English, so the LANGID_USENGLISH is included to indicate that. In fact, all server tag texts in all the example Python scripts use LANGID_USENGLISH
as their encoding.
BUTTONNAME
From the SDK docs regarding SRVTAG_BUTTONNAME
:
“The string that will appear on a button or in a popup list used to invoke your plug-in.
This is usually an abbreviated version of your user name.”
MENU
Excerpted from the SDK docs regarding SRVTAG_MENU
:
“For plug-ins that can be activated as commands or tools (all Modeler classes, plus generics in Layout), the menu string specifies the location of the plug-in’s node in LightWave’s menu system. Like command groups, the menu string can refer to predefined or custom nodes.
They can also specify a ‘path’ resembling a filename, with optional root menu nodes followed by a colon and other nodes separated by forward slashes, and the nodes can be a mix of predefined and custom.”
As you will note from this portion of the documentation, the use of the SRVTAG_MENU
value in your ServerTagInfo
structure will only have relevance in CommandSequence or Layout Generic plug-in types. In all other cases, LightWave will simply ignore it.
Enumerating Surfaces
def process(self, mod_command):
mesh_edit_op = mod_command.editBegin(0, 0, lwsdk.OPSEL_USER)
if not mesh_edit_op:
print >>sys.stderr, 'Failed to engage mesh edit operations!'
return lwsdk.AFUNC_OK
polys = []
edit_op_result = mesh_edit_op.fastPolyScan(mesh_edit_op.state, self.fast_poly_scan, (polys,), lwsdk.OPLYR_FG, 1)
if edit_op_result != lwsdk.EDERR_NONE:
mesh_edit_op.done(mesh_edit_op.state, edit_op_result, 0)
return lwsdk.AFUNC_OK
c = 1
for poly in polys:
poly_info = mesh_edit_op.polyInfo(mesh_edit_op.state, poly)
surfname = "%s%02d" % (poly_info.surface, c)
edit_op_result = mesh_edit_op.polSurf(mesh_edit_op.state, poly, surfname)
if edit_op_result != lwsdk.EDERR_NONE:
break
c = c + 1
mesh_edit_op.done(mesh_edit_op.state, edit_op_result, 0)
return lwsdk.AFUNC_OK
We begin by initiating a mesh-edit operation on line 2. This informs Modeler of our intent to modify the current object’s mesh in the active layers based on any geometry that the user has explicitly selected (OPSEL_USER). Of course, if no geometry has been explicitly selected, then all mesh is considered selected implicitly.
To make sure all is well, the return value of the editBegin() call is checked to make sure it is not None. A return of None means something went wrong with our request. The user is notified of the failure via a console message, and the plug-in terminates with a return.
Note the print statement in the code:
print >>sys.stderr, 'Failed to engage mesh edit operations!'
Output from a LightWave Python script is automatically routed to the PCore Console. If the output is simple, meaning it is just a print, then output is simply accumulated in the Console. However, if the script output is directed to stderr ( as it happening above), this channel is handled differently from standard output. Text routing through the stderr channel will cause the PCore Console to open automatically if it is not already visible to the user, and the text displayed will be highlighted in red. _images/ConsoleError.png
Our next task, to identify all the polygons upon which we will work, takes place on line 9. This is done using an iterative function provided by the SDK called fastPolyScan().
note
You can read more about this, and other, scanning functions in the LightWave SDK documentation.
This function will iterate over all the currently selected polygons, invoking our script code for each. In order to do this, we must provide a callback function for the SDK to use. In enumerate_surfaces
, we define a callback that accepts the proper arguments, and provide this to the fastPolyScan()
function by a direct reference. The callback method is rather unimaginatively named fast_poly_scan()
:
def fast_poly_scan(self, poly_list, poly_id):
poly_list.append(poly_id)
return lwsdk.EDERR_NONE
This small function does nothing more than accept the identifier of a single polygon and then append it to a Python list that we provide when invoking fastPolyScan(). If the function were to return anything other than EDERR_NONE
, the scanning process would terminate, and the result would be returned from the call to fastPolyScan()
.
Once we have identified all the relevant polygons, we then loop over each (lines 15-21). For each polygon identifier in the list, we make a call to polyInfo() (line 16) to retrieve information about it. One of the elements of that information is the surface name to which the polygon is currently assigned. Using that, we construct a new name (line 17) with a numeric increment appended, and then assign the polygon to the new surface using the polSurf() call on line 18. While all goes well, the loop increments the enumerating value, and proceeds to the next polygon.
When all selected polygons have been assigned to new, enumerated surface names, the editing operation is complete, and the result of the last edit operation is provided back to Modeler on line 23 to conclude the editing session. If the last result was EDERR_NONE
, then the edit operation is applied to the mesh, otherwise all changes are discarded.
Single Shot Exceptions
“Single-shot” plug-in architectures are those that are not required to be persistent. Persistence in a LightWave plug-in is required when the product may invoke elements of the plug-in at any given time. Because of this, the plug-in is required to remain active in memory, but on “stand-by”, until LightWave requires its services.
Plug-ins that perform all their processing when they are activated, and then go away, are not concerned with persistence. They perform in a “single shot”. In this situation, the lack of persistence means that no callbacks are required, and therefore, no programming contract is in force (see Handler Interfaces for more detail about the programming-by-contract design).
Each application – Layout and Modeler – has such “single-shot” plug-in types. In Layout, the Generic Class is such a type. In Modeler, the Command Sequence Class type also performs in this way. In both cases, Python scripts need not be formatted by the strict terms of the programming-by-contract paradigm, and may use highly lax coding design.
warning
“Single-shot” formats, by definition, lack the explicit elements that denote a LightWave plug-in. This results in “grey-area” handling of Python scripts, in that scripts that cannot explicitly be identified as LightWave plug-ins, are implicitly identified as “single-shot” plug-ins of the type dependent on the application. By this logic, any Python script can be a LightWave Generic (or CommandSequence) plug-in, even if it has absolutely no relation to LightWave. Attempts to execute such implicit plug-ins may (and probably will) result in errors.
Generic: Revisiting “add_null.py”
The “add_null.py” example script, found in your Scripts folder, is an example of the programming-by-contract formatting that most plug-ins require. However, because it is a Generic plug-in, it can take advantage of the “single-shot” format, simplifying it to just the following:
try:
import lwsdk
except:
import sys
print "This is a LightWave Python Generic plug-in. Please execute it within LightWave."
sys.exit(1)
ga = lwsdk.GenericAccess()
if ga.valid():
name = ga.commandArguments()
if name is None:
name = 'Null'
ga.evaluate("AddNull %s" % name)
ga.evaluate("Position 0 1 0")
interface_info = lwsdk.LWInterfaceInfo()
if not (interface_info.generalFlags & lwsdk.LWGENF_AUTOKEY):
ga.evaluate("CreateKey %f" % interface_info.curTime)
Note that pains are taken to make sure that the script, if executed in an environment outside of LightWave, properly identifies itself and terminates.
Line 8 uses a special helper function called GenericAccess(). This function provides access to an instance of the LWLayoutGeneric
class that would have been passed to the process()
method of a lwsdk.IGeneric
subclass. All values available from that passed argument are also available in this instance.
CommandSequence: Re-visiting “make_test.py”
Similar to “add_null.py”, the CommandSequence example script “make_test.py” can also be simplified in its code requirements. A version of “make_test.py” that uses “single-shot” formatting would look like:
try:
import lwsdk
except:
import sys
print "This is a LightWave Python CommandSequence plug-in. Please execute it within LightWave."
sys.exit(1)
mod_command = lwsdk.ModCommand()
if mod_command.valid():
radius = lwsdk.Vector(0.5, 0.5, 0.5)
nsides = 24
nsegments = 12
center = lwsdk.Vector(0.0, 0.0, 0.0)
cs_options = lwsdk.marshall_dynavalues((radius, nsides, nsegments, center))
cs_makeball = mod_command.lookup("MAKEBALL")
result, dyna_value = mod_command.execute(cs_makeball, cs_options, lwsdk.OPSEL_USER)
cs_options = lwsdk.marshall_dynavalues('2')
cs_setlayer = mod_command.lookup("SETLAYER")
result, dyna_value = mod_command.execute(cs_setlayer, cs_options,lwsdk.OPSEL_USER)
cs_options = lwsdk.marshall_dynavalues(([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5], None))
cs_makebox = mod_command.lookup("MAKEBOX")
result, dyna_value = mod_command.execute(cs_makebox, cs_options, lwsdk.OPSEL_USER)
cs_options = lwsdk.marshall_dynavalues('1')
result, dyna_value = mod_command.execute(cs_setlayer, cs_options, lwsdk.OPSEL_USER)
cs_options = lwsdk.marshall_dynavalues('2')
cs_setblayer = mod_command.lookup("SETBLAYER")
result, dyna_value = mod_command.execute(cs_setblayer, cs_options, lwsdk.OPSEL_USER)
As with the Generic example, this script ensures that non-LightWave execution is trapped and handled gracefully. Also, on line 8, an instance of the LWModCommand class is accessible to “single-shot” scripts using the ModCommand() helper function.
First-Class Citizens
Like their “programming-by-contract” brothers, “single-shot” plug-ins share in the privilege of being first-class plug-in citizens within LightWave. They are installed like other plug-ins, and added to the interface as buttons, just like other plug-ins.
Some aspects of this feature are important to note. Most importantly, since there is no required ServerRecord found in a “single-shot” plug-in, one is constructed for them by PCore. In doing so, certain “template” actions are taken for each “single-shot” plug-in.
The file name of the plug-in (sans the extension) is used to derive certain required values. The text of the file name is manipulated in two ways to construct both the display name (the lwsdk. SRVTAG_USERNAME
value of a “programming-by-contract” plug-in), and the plug-in name (the first argument provided to the Factory when an instance is created).
Let’s take the “add_null.py” script as our example. The base of the file name is extracted, giving us the string “add_null”. This value is then used to construct the plug-in name by first converting all spaces to underscores (in this particular case, there are no spaces), and adding a Python-specific identifying prefix, resulting in the value “Py_add_null”.
The base file name is then also used verbatim as the display name, giving in this case “add_null”. This value is what will appear in plug-in lists on the application’s user interface.
Knowing these things, and with a bit of forethought, you can improve the appearance of the “single-shot” plug-in’s displayed name by making sure the filename itself is formatted in exactly the way you wish it to appear on the interface.
For example, instead of the rather dull “add_null.py”, you might name your plug-in “Add Null.py”. This would result in “Py_Add_Null” and “Add Null” for the plug-in and display name, respectively.