Let's Make Another Box
Author Ernie Wright
Date 1 June 2001
In the first installment of this tutorial, we looked at a simple plug-in that creates a
box in Modeler by calling the MAKEBOX
command. In this installment we'll assume
you're comfortable with the plug-in basics covered there. We'll
call MAKEBOX
in a different way, and we'll add a user interface. The complete
source is in sample/boxes/box2/box.c
.
Our Second Box Plug-in
In this plug-in, we'll move the box creation out of the activation function into its
own function, and we'll use lookup
and execute
rather than evaluate
to issue the MAKEBOX
command.
Here's our new makebox
function.
int makebox( LWModCommand *local, float *size, float *center, int nsegments ) { static LWCommandCode ccode = 0; DynaValue argv[ 3 ]; argv[ 0 ].type = DY_VFLOAT; argv[ 0 ].fvec.val[ 0 ] = center[ 0 ] - 0.5f * size[ 0 ]; argv[ 0 ].fvec.val[ 1 ] = center[ 1 ] - 0.5f * size[ 1 ]; argv[ 0 ].fvec.val[ 2 ] = center[ 2 ] - 0.5f * size[ 2 ]; argv[ 1 ].type = DY_VFLOAT; argv[ 1 ].fvec.val[ 0 ] = center[ 0 ] + 0.5f * size[ 0 ]; argv[ 1 ].fvec.val[ 1 ] = center[ 1 ] + 0.5f * size[ 1 ]; argv[ 1 ].fvec.val[ 2 ] = center[ 2 ] + 0.5f * size[ 2 ]; if ( nsegments ) { argv[ 2 ].type = DY_VINT; argv[ 2 ].ivec.val[ 0 ] = argv[ 2 ].ivec.val[ 1 ] = argv[ 2 ].ivec.val[ 2 ] = nsegments; } else argv[ 2 ].type = DY_NULL; if ( !ccode ) ccode = local->lookup( local->data, "MAKEBOX" ); return local->execute( local->data, ccode, 3, argv, 0, NULL ); }
Now we're getting into some stuff! Before I explain it, you might wonder why we'd
bother with this apparently more complicated method at all, when we can use the simpler
evaluate
function. One answer is speed.
The lookup
and execute
functions are Modeler's native
mechanism for processing commands. When you use evaluate
instead, Modeler parses
the command string you pass and then calls the lookup
and execute
functions itself. You save some time by using lookup
and execute
directly, rather than building an evaluate
string in your plug-in that Modeler is
just going to take apart again.
Also on the plus side, you only have to write your makebox
function once.
After that, you can call it as often as you like with a single line of code, and you can
cut and paste it into other plug-ins. The modlib SDK
sample is a library of about a hundred functions like makebox
, each of which
issues one of the Modeler commands.
And if the low and high corner arguments to the MAKEBOX
command are
inconvenient, you can make up your own, as we did above with the size and center
arguments. Obviously, your makebox
could call evaluate
, but if you're
going to the trouble to write a makebox
at all, you might as well go all the way
and use the faster lookup
and execute
.
Arguments
Except for the last line, makebox
spends all of its time constructing the
arguments for the execute
function, one of which is the argument list for the MAKEBOX
command. Let's take a closer look at all of these arguments.
int execute( void *mddata, LWCommandCode ccode, int argc, DynaValue *argv, EltOpSelect opsel, int *result );
mddata
- As with
evaluate
, this is thedata
field of the LWModCommand structure, which provides Modeler the context in which the command will be processed. You never need to know what's in this data. You just have to pass it to the functions that require it. ccode
- A command code obtained by calling
lookup
. A code is used to specify the command instead of the name of the command (in our case,MAKEBOX
) because it's faster. Modeler doesn't have to do a lot of string comparisons to figure out which command you're issuing. The string search need only be done once per Modeler session, when you calllookup
. In our plug-in, the command code returned bylookup
is stored in a static variable. We only calllookup
the first time throughmakebox
, when our static variable hasn't been initialized yet. argc, argv
- The argument list for the
MAKEBOX
command.argv
is an array of DynaValues, andargc
is the number of elements in theargv
array. DynaValues are the union of a number of different data types. To initialize a DynaValue, you set the type field, then set the value of the union member appropriate for the type. DynaValues allow you to create a single array in which each element can be a different data type. opsel
- This is the selection mode for the command. It determines which existing geometry your
command will interact with. You want some commands to work only on the polygons the user
has selected, or on all polygons regardless of the selection, and so on. This is ignored
for
MAKEBOX
, so we just set it to 0. result
- A few commands return command-specific result codes in this argument.
MAKEBOX
isn't one of them, so we set this to NULL.
Before we move on to the interface part, there's one other thing to notice here. The
third argument to the MAKEBOX
command is optional. You don't have to specify a
number of segments. To support this in our makebox
function, we allow the nsegments
argument to be 0. In that case, the third element of the argv
array is a
DynaValue of type DY_NULL
. The DY_NULL
type serves as a placeholder when
the argument list for a command contains an optional argument that you aren't supplying.
The Interface
Our interface function displays a modal input window, or panel, that looks like this.
This is built with XPanels, a component of the platform-independent user interface API. The panel layout and event handling in XPanels are highly automated, so to create this panel, all we have to do is create a list of controls and initialize them, display the panel, and then collect the results.
Here's the source code for our interface function.
int get_user( LWXPanelFuncs *xpanf, double *size, double *center, int *nsegments ) { LWXPanelID panel; int ok = 0; enum { ID_SIZE = 0x8001, ID_CENTER, ID_NSEGMENTS }; LWXPanelControl ctl[] = { { ID_SIZE, "Size", "distance3" }, { ID_CENTER, "Center", "distance3" }, { ID_NSEGMENTS, "Segments", "integer" }, { 0 } }; LWXPanelDataDesc cdata[] = { { ID_SIZE, "Size", "distance3" }, { ID_CENTER, "Center", "distance3" }, { ID_NSEGMENTS, "Segments", "integer" }, { 0 } }; LWXPanelHint hint[] = { XpMIN( ID_NSEGMENTS, 0 ), XpMAX( ID_NSEGMENTS, 200 ), XpDIVADD( ID_SIZE ), XpDIVADD( ID_CENTER ), XpEND }; panel = xpanf->create( LWXP_FORM, ctl ); if ( !panel ) return 0; xpanf->describe( panel, cdata, NULL, NULL ); xpanf->hint( panel, 0, hint ); xpanf->formSet( panel, ID_SIZE, size ); xpanf->formSet( panel, ID_CENTER, center ); xpanf->formSet( panel, ID_NSEGMENTS, nsegments ); ok = xpanf->post( panel ); if ( ok ) { double *d; int *i; d = xpanf->formGet( panel, ID_SIZE ); size[ 0 ] = d[ 0 ]; size[ 1 ] = d[ 1 ]; size[ 2 ] = d[ 2 ]; d = xpanf->formGet( panel, ID_CENTER ); center[ 0 ] = d[ 0 ]; center[ 1 ] = d[ 1 ]; center[ 2 ] = d[ 2 ]; i = xpanf->formGet( panel, ID_NSEGMENTS ); *nsegments = *i; } xpanf->destroy( panel ); return ok; }
Dissecting the Interface
Let's take a closer look.
int get_user( LWXPanelFuncs *xpanf, double *size, double *center, int *nsegments )
The first argument to get_user
is a pointer to LWXPanelFuncs. When we get to
our activation function, I'll explain where this comes from. But for now, it's a structure
containing the functions we need to work with our panel. The other three arguments are
used to initialize our controls, and they'll be modified with the values entered by the
user. Note that size
and center
are arrays of three doubles, while nsegments
is a pointer to a single integer.
LWXPanelID panel; int ok = 0;
LWXPanelID is just an opaque pointer that LightWave uses to identify our panel. The
double and integer pointers will be used when we retrieve the user's entries from the
controls, and ok
will be true if the user presses OK to dismiss the panel.
enum { ID_SIZE = 0x8001, ID_CENTER, ID_NSEGMENTS };
XPanels uses integer codes to identify panel controls. User-defined controls have IDs that start at 0x8001.
LWXPanelControl ctl[] = { { ID_SIZE, "Size", "distance3" }, { ID_CENTER, "Center", "distance3" }, { ID_NSEGMENTS, "Segments", "integer" }, { 0 } }; LWXPanelDataDesc cdata[] = { { ID_SIZE, "Size", "distance3" }, { ID_CENTER, "Center", "distance3" }, { ID_NSEGMENTS, "Segments", "integer" }, { 0 } };
These two arrays define our controls. They look redundant, and to a certain extent, for what we're doing, they are. XPanels distinguishes between controls (the widgets drawn on your panel) and data descriptions, which define how you'll represent the values of your controls. You can define controls that don't have corresponding data descriptions, and vice versa. This amount of abstraction is useful for more sophisticated panels, but we're setting up the simplest kind of relationship between our controls and their values, so our control list and our data descriptions are parallel.
Note that XPanels, as of this writing, doesn't support controls of type
integer3.
We could simulate one with three separate integer
controls, but for the sake of simplicity I chose not to do this. As a result, our makebox
will create the same number of segments along all three axes.
LWXPanelHint hint[] = { XpMIN( ID_NSEGMENTS, 1 ), XpMAX( ID_NSEGMENTS, 200 ), XpDIVADD( ID_SIZE ), XpDIVADD( ID_CENTER ), XpEND };
XPanels automates most aspects of control layout. The rules it uses resemble those used to build LightWave's own interface, so your plug-in's panels are aesthetically and functionally consistent with the rest of the program. In exchange for this, you must sacrifice some low-level control over the appearance of your panel. You can't specify the pixel positions of your controls, for example.
Instead, you use hints to describe your controls and the appearance of your panel in a more abstract way. Here we define a sane range for the segments control and add dividers between the controls. The positions and sizes of the controls, their labels, and decorations like the dividers are all calculated for us. You can also use hints to group controls, put controls on different tabs, establish dependencies between controls, and lots of other things.
panel = xpanf->create( LWXP_FORM, ctl ); if ( !panel ) return 0;
This is where we create the panel. If panel creation fails for some reason, we return 0, which is also what we return when the user presses the Cancel button.
XPanels supports two kinds of panels, called forms and views. Views are designed primarily for the panels associated with handler class plug-ins in Layout. Views work with instances, the unique data pointers returned by each invocation of a handler plug-in. But you're free to choose. You can use forms in your handlers, and we could have used a view here. Forms are just a little easier to grasp initially.
xpanf->describe( panel, cdata, NULL, NULL ); xpanf->hint( panel, 0, hint ); xpanf->formSet( panel, ID_SIZE, size ); xpanf->formSet( panel, ID_CENTER, center ); xpanf->formSet( panel, ID_NSEGMENTS, nsegments );
These calls initialize the panel. The last two arguments to the describe
function are NULL because our panel is a form. If it had been a view, these arguments
would contain pointers to our get and set callbacks. The IDs in the formSet
calls
are value IDs corresponding to entries in the data description array (as opposed to
control IDs from our control array). This is a distinction without a difference for us
now, but I wanted to plant this in the back of your mind for a time when it will
make a difference.
ok = xpanf->post( panel );
The post
function displays the panel and waits for the user. Using post
makes the panel modal, which just means that everything else stops until the user presses
OK or Cancel on the panel.
if ( ok ) { double *d; int *i d = xpanf->formGet( panel, ID_SIZE ); size[ 0 ] = d[ 0 ]; size[ 1 ] = d[ 1 ]; size[ 2 ] = d[ 2 ]; d = xpanf->formGet( panel, ID_CENTER ); center[ 0 ] = d[ 0 ]; center[ 1 ] = d[ 1 ]; center[ 2 ] = d[ 2 ]; i = xpanf->formGet( panel, ID_NSEGMENTS ); nsegments[ 2 ] = nsegments[ 1 ] = nsegments[ 0 ] = *i; }
If the user presses OK, we retrieve the value of each control and store it for use
later in our plug-in. The formGet
function returns a pointer to a variable of the
appropriate type for the value.
xpanf->destroy( panel ); return ok; }
We're done with the panel, so we destroy it. If we needed to open this panel more than once, we could create it once, post it as many times as we need it, then destroy it when we exit.
Activation
All that remains is our activation function, which really hasn't changed very much.
XCALL_( int ) Activate( long version, GlobalFunc *global, LWModCommand *local, void *serverData ) { LWXPanelFuncs *xpanf; double size[ 3 ] = { 1.0, 1.0, 1.0 }; double center[ 3 ] = { 0.0, 0.0, 0.0 }; int nsegments = 1; if ( version != LWMODCOMMAND_VERSION ) return AFUNC_BADVERSION; xpanf = global( LWXPANELFUNCS_GLOBAL, GFUSE_TRANSIENT ); if ( !xpanf ) return AFUNC_BADGLOBAL; if ( get_user( xpanf, size, center, &nsegments )) makebox( local, size, center, nsegments ); return AFUNC_OK; }
We've added storage for the box parameters so that these can be user-defined, and we
call our get_user
and makebox
functions rather than sprintf
and
evaluate
. We've also added a couple of lines having to do with that LWXPanelFuncs
pointer, and as promised, I'll now explain where that pointer comes from.
LWXPanelFuncs *xpanf; xpanf = global( LWXPANELFUNCS_GLOBAL, GFUSE_TRANSIENT ); if ( !xpanf ) return AFUNC_BADGLOBAL;
The second argument to every activation function is the global function. The
globals returned by this function are services provided by LightWave. The XPanels global
is a common example. When called with an LWXPANELFUNCS_GLOBAL
argument, the
global function returns a pointer to an LWXPanelFuncs structure containing the functions
you need to build and display an XPanels interface. The Globals
page of the SDK describes the global function in detail and lists the globals available in
LightWave.
What's Next
We'll be taking our user interface with us into the next
installment, but we'll be leaving behind the MAKEBOX
command as we explore
the construction of boxes from individual points and polygons.