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 the data 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 call lookup. In our plug-in, the command code returned by lookup is stored in a static variable. We only call lookup the first time through makebox, when our static variable hasn't been initialized yet.
argc, argv
The argument list for the MAKEBOX command. argv is an array of DynaValues, and argc is the number of elements in the argv 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.