Tutorial: Box4

LightWave

Part 3 Articles Table of Contents

A Tool Box

Author  Ernie Wright
Date  3 July 2001

In the first three installments of this tutorial, I introduced the basics of plug-in creation, including the organization of plug-ins into classes, the use of the SDK headers, the activation function, the server record, and function pointers. We walked through the build process with a specific compiler. We used globals that allowed us to create a user interface, query the surface list, and convert between text and binary representations of numbers. We learned how to process a command line so that our plug-in can be run in batch mode. And we created a box in Modeler, both by issuing a command (in two different ways) and by calling mesh edit functions.

In this final installment, we'll apply what we've learned to create a tool, a plug-in that interacts with the user in the same way that Modeler's native tools do. The user will be able to click and drag in Modeler's interface to position and size our box, and our non-modal panel will open when the user requests our numeric options.

Unlike the first three versions of our box plug-in, this one isn't a CommandSequence plug-in. Modeler tools are of the MeshEditTool class. The complete source code for the box tool can be found in sample/boxes/box4/box.c, tool.c and ui.c. Because it's difficult to use a windowed debugger to trace the execution of code that responds to mouse clicks and drags, I've also written debug versions of the tool and interface modules called wdbtool.c and wdbui.c that write event information to a file. wdbtool.c contains a few lines of Windows-specific code related to hooking mouse events. Hopefully they can be easily replaced for use with other operating systems.

The Basic Idea

Tool plug-ins supply a set of callbacks, functions that Modeler calls while the tool is active. These callbacks respond to user actions by drawing the tool and generating the geometry that the tool creates or modifies.

A handle is a point that the user can grab and move to change the operation of the tool. Our plug-in will support two handles, one for the center of the box, and the other at the (+x, +y, +z) corner to control the size. (More sophisticated tools will usually support many more handles.) The following table shows the user clicking to establish the box center, then dragging to pull out the corner handle. The callbacks are listed in the order in which they're typically called during each part of this operation.

mouse down mouse move mouse up spacebar
  • count
  • start
  • dirty
  • test
  • handle
  • adjust
  • dirty
  • test
  • build
  • draw
  • end
  • done

We'll cover the implementation of each of these callbacks, pretty much in the same order. But before we do that, note that all of the callbacks take an LWInstance (a pointer to void) as their first argument. This is the tool's instance, a structure we design to hold all of the information we need to maintain the tool's state and generate the geometry. Our instance data structure is called BoxData. One of these is allocated in our activation function, and it persists until the user is finished with the tool.

Count, Start

These two callbacks are related. They're only called when the user clicks the left mouse button to begin dragging the tool, and start is only called if count returns 0.

   static int Count( BoxData *box, LWToolEvent *event )
   {
      return box->active ? 2 : 0;
   }

From our tool's point of view, there are two different kinds of mouse down events. The first is the initial mouse down, before any box has been dragged out and before we've drawn the handles. For that case, our box->active is FALSE, and our Count returns 0, so that our Start will be called. The other kind of mouse down occurs after the first one. The user is modifying an existing box, rather than starting a new one. In this second case, Count returns 2 (because we have 2 handles), and Modeler doesn't call Start.

   static int Start( BoxData *box, LWToolEvent *event )
   {
      int i;

      if ( !box->active )
         box->active = 1;

      for ( i = 0; i < 3; i++ ) {
         box->center[ i ] = event->posSnap[ i ];
         box->size[ i ] = 0.0;
      }
      calc_handles( box );

      return 1;
   }

When Modeler calls Start, the user has just clicked the left mouse button to begin a new box. We make sure box->active is now TRUE, and we set the size of the box to 0 and the center to the point at which the user clicked. We initialize the precalculated handle positions, then return 1, the index of the second handle, to indicate that the user has grabbed the sizing handle. While the left mouse button remains down, the handle callback will only be called for this handle.

Dirty, Test

These two callbacks are also somewhat related. The dirty callback tells Modeler whether the tool needs to be redrawn on the screen. The test callback tells Modeler whether the tool needs to create new geometry or discard existing geometry.

   static int Dirty( BoxData *box )
   {
      return box->dirty ? LWT_DIRTY_WIREFRAME | LWT_DIRTY_HELPTEXT : 0;
   }

Dirty is only concerned with the tool's appearance to the user. After the initial mouse down for a new box, box->dirty is FALSE, since we haven't drawn anything yet, and we tell Modeler that nothing needs to be redrawn. During mouse move events, our Adjust callback is called, and this sets box->dirty to TRUE so that we get redrawn to follow the user's mouse moves. We're also dirty after receiving reset and activate events in our Event callback. Our Draw callback sets box->dirty to FALSE again after redrawing the tool.

   static int Test( BoxData *box )
   {
      return box->update;
   }

Like box->dirty, our Adjust and Event callbacks set box->update, depending on our tool's state at that point. Build also sets it (to LWT_TEST_NOTHING) after creating the box geometry. Our Test just returns the value in box->update.

Handle

This callback tells Modeler about one of our handles.

   static int Handle( BoxData *box, LWToolEvent *event, int handle,
      LWDVector pos )
   {
      if ( handle >= 0 && handle < 2 ) {
         pos[ 0 ] = box->hpos[ handle ][ 0 ];
         pos[ 1 ] = box->hpos[ handle ][ 1 ];
         pos[ 2 ] = box->hpos[ handle ][ 2 ];
      }
      return handle + 1;
   }

Handle is called during mouse moves, but only for the handle the user is currently moving. It's also called right after mouse down, if Count returns a non-zero number of handles. In that case, it's called for every handle, and Modeler uses the positions to determine which handle the user has selected. The return value is the priority of the handle, which is used to decide between handles that overlap visually (have the same apparent position in the viewport). When the user points to two or more overlapping handles, Modeler chooses the one with the highest priority.

Adjust

The adjust callback is called during mouse moves to tell you that a handle is being dragged.

   static int Adjust( BoxData *box, LWToolEvent *event, int handle )
   {
      if ( event->portAxis >= 0 ) {
         if ( event->flags & LWTOOLF_CONSTRAIN ) {
            int x, y, xaxis[] = { 1, 2, 0 }, yaxis[] = { 2, 0, 1 };
            x = xaxis[ event->portAxis ];
            y = yaxis[ event->portAxis ];
            if ( event->flags & LWTOOLF_CONS_X )
               event->posSnap[ x ] -= event->deltaSnap[ x ];
            else if ( event->flags & LWTOOLF_CONS_Y )
               event->posSnap[ y ] -= event->deltaSnap[ y ];
         }
      }

Before we move the handle, we check whether its new position should be quantized or fixed by a constraint. Typically, this is to account for the user holding down the Ctrl key. The fact that Modeler doesn't do this for us means that we aren't required to honor this convention, but in our case (and in most cases), we have no reason not to.

      if ( handle == 0 ) {  /* center */
         box->center[ 0 ] = event->posSnap[ 0 ];
         box->center[ 1 ] = event->posSnap[ 1 ];
         box->center[ 2 ] = event->posSnap[ 2 ];
      }
      else if ( handle == 1 ) {  /* corner */
         box->size[ 0 ] = 2.0 * fabs( event->posSnap[ 0 ]
            - box->center[ 0 ] );
         box->size[ 1 ] = 2.0 * fabs( event->posSnap[ 1 ]
            - box->center[ 1 ] );
         box->size[ 2 ] = 2.0 * fabs( event->posSnap[ 2 ]
            - box->center[ 2 ] );
      }

      calc_handles( box );
      box->dirty = 1;
      box->update = LWT_TEST_UPDATE;
      return handle;
   }

If the user's moving the center handle, we set the box center to the new position, and if the user is moving the size handle, we recalculate the size. In both cases, we precalculate the handle positions for the next Handle and Draw calls, and we tell Modeler that we need to be both redrawn and rebuilt.

Build

Finally! This callback creates geometry based on what the user is doing.

   static LWError Build( BoxData *box, MeshEditOp *edit )
   {
      makebox( edit, box );
      box->update = LWT_TEST_NOTHING;
      return NULL;
   }

All we have to do here is call our old friend makebox, passing it the MeshEditOp and the size and center set by the user. And since we've just built the geometry, we set box->update to NOTHING.

Draw

Here we draw the tool itself. We don't have to draw the geometry we create, since Modeler takes care of that for us.

   static void Draw( BoxData *box, LWWireDrawAccess *draw )
   {
      if ( !box->active ) return;
      draw->moveTo( draw->data, box->hpos[ 0 ], LWWIRE_SOLID );
      draw->lineTo( draw->data, box->hpos[ 1 ], LWWIRE_ABSOLUTE );
      box->dirty = 0;
   }

To keep this simple, we're drawing a single line segment connecting our two handles. More typically, you'll draw a bounding box or some other representation of the scope of your tool's effects, and you'll draw the handles in some way, so that the user knows where they are.

Help

The help callback returns a line of text that Modeler draws while the tool is selected. Modeler calls Help whenever Dirty returns the LWT_DIRTY_HELPTEXT bit. It also calls Help each time the user moves the mouse cursor to a new viewport, so that you can return a different string for each view.

   static const char *Help( BoxData *box, LWToolEvent *event )
   {
      static char buf[] = "Box Tool Plug-in Tutorial";
      return buf;
   }

Event

This is called when the user drops, resets or re-activates the tool.

   static void Event( BoxData *box, int code )
   {
      switch ( code )
      {
         case LWT_EVENT_DROP:
         if ( box->active ) {
            box->update = LWT_TEST_REJECT;
            break;
         }

The user can drop a tool by clicking in a blank area of Modeler's interface outside the viewports. Generally this means that the user wants to discard the geometry created with the tool, so if we've created some geometry (box->active is TRUE), we set box->update to LWT_TEST_REJECT, so that Modeler will discard the geometry the next time it calls Test. If box->active is FALSE, we fall through to the next case, treating a drop like a reset.

         case LWT_EVENT_RESET:
            box->size[ 0 ] = box->size[ 1 ] = box->size[ 2 ] = 1.0;
            box->center[ 0 ] = box->center[ 1 ] = box->center[ 2 ] = 0.0;
            strcpy( box->surfname, "Default" );
            strcpy( box->vmapname, "MyUVs" );
            box->update = LWT_TEST_UPDATE;
            box->dirty = 1;
            calc_handles( box );
            break;

A reset event occurs when the user selects the Reset action on Modeler's Numeric panel. We set all of the box parameters to default values and set our state variables so that Modeler will both rebuild and redraw us.

         case LWT_EVENT_ACTIVATE:
            box->update = LWT_TEST_UPDATE;
            box->active = 1;
            box->dirty = 1;
            break;
      }
   }

An activate event can be triggered from the Numeric window or with a keystroke, and it should restart the edit operation with its current settings.

End, Done

These sound confusingly alike. The end callback is called at the completion of a mouse down, mouse move, mouse up sequence. While the tool is selected, you may get any number of end calls. The done callback is called when the user is finished with the tool and has deselected it, and it's typically used to free memory allocated by the activation function.

   static void End( BoxData *box, int keep )
   {
      box->update = LWT_TEST_NOTHING;
      box->active = 0;
   }

Our End sets box->update to NOTHING and box->active to FALSE, the state we want our tool data to be in the next time Count is called.

   static void Done( BoxData *box )
   {
      free( box );
   }

Our Done frees the BoxData structure.

The Interface

The panel we create for a tool is displayed inside Modeler's Numeric panel when the tool is active. We don't open it ourselves. We create the panel in yet another callback, and Modeler takes care of opening or closing it. The panel becomes just another way for the user to interact with the tool. As settings are changed on the panel, the geometry is changed and the tool is redrawn, just as if the user were dragging the mouse in the viewport.

So our panel is now non-modal. It differs from previous incarnations of our interface in a couple of other ways, too. Since tools use instances (our BoxData structure), it's more natural to make our panel an LWXP_VIEW instead of an LWXP_FORM. And the surface name list is built with the popup callbacks I avoided in Part 3.

   LWXPanelID Panel( BoxData *box )
   {
      LWXPanelID panel;

      static LWXPanelControl ctl[] = {
         { ID_SIZE,     "Size",      "distance3"  },
         { ID_CENTER,   "Center",    "distance3"  },
         { ID_SURFLIST, "Surface",   "iPopChoice" },
         { ID_VMAPNAME, "VMap Name", "string"     },
         { 0 }
      };
      static LWXPanelDataDesc cdata[] = {
         { ID_SIZE,     "Size",      "distance3" },
         { ID_CENTER,   "Center",    "distance3" },
         { ID_SURFLIST, "Surface",   "integer"   },
         { ID_VMAPNAME, "VMap Name", "string"    },
         { 0 }
      };
      LWXPanelHint hint[] = {
         XpLABEL( 0, "Box Tutorial Part 4" ),
         XpPOPFUNCS( ID_SURFLIST, get_surfcount, get_surfname ),
         XpDIVADD( ID_SIZE ),
         XpDIVADD( ID_CENTER ),
         XpEND
      };

The control and data description arrays are the same as before, with one important difference: they've been declared static. Our panel is no longer modal. It persists after the Panel function returns, and the control and data descriptions must also.

The XpSTRLIST hint has been replaced by an XpPOPFUNCS hint that tells XPanels to use the get_surfcount and get_surfname callbacks with the surface name popup. These callbacks will be called to initialize the list each time the user clicks on it to open it. They use the same techniques for enumerating the surfaces in Modeler that init_surflist used in Part 3.

      panel = xpanf->create( LWXP_VIEW, ctl );
      if ( !panel ) return NULL;

      xpanf->describe( panel, cdata, Get, Set );
      xpanf->hint( panel, 0, hint );

      return panel;
   }

Recall that in Part 3, the third and fourth arguments to describe were NULL. Since our panel is a view, we now pass get and set callbacks.

Get, Set

It's easy to get these two mixed up. Just try to remember that the names are from LightWave's point of view, not yours (you're the server, LightWave is the client). XPanels calls the Get callback when it wants to get the value of a control from you. It calls the Set callback when it wants you to write the value of a control into your instance data.

   static void *Get( BoxData *box, unsigned long vid )
   {
      static int i;

      switch ( vid ) {
         case ID_SIZE:      return &box->size;
         case ID_CENTER:    return &box->center;
         case ID_SURFLIST:  i = get_surfindex( box->surfname );
                            return &i;
         case ID_VMAPNAME:  return &box->vmapname;
         default:           return NULL;
      }
   }

Get is usually pretty straightforward. Just return a pointer to the appropriate element of your instance data.

   static int Set( BoxData *box, unsigned long vid, void *value )
   {
      const char *a;
      double *d;
      int i;

      switch ( vid )
      {
         case ID_SIZE:
            d = ( double * ) value;
            sbox.size[ 0 ] = box->size[ 0 ] = d[ 0 ];
            sbox.size[ 1 ] = box->size[ 1 ] = d[ 1 ];
            sbox.size[ 2 ] = box->size[ 2 ] = d[ 2 ];
            break;

         case ID_CENTER:
            ...

Set adds a few wrinkles. The first is that you generally need to cast the value argument before assigning its contents to your instance data, so it's convenient to have temporary pointers of the right type handy. The second, for us, is that we'd like to keep a local copy of the instance, so that we can use it to initialize the tool instance the next time the user activates the tool. The user's perception of this is that the tool "remembers" what was done previously. So all of our assignments are duplicated for the local copy.

         default:
            return LWXPRC_NONE;
      }

      box->update = LWT_TEST_UPDATE;
      box->dirty = 1;
      calc_handles( box );
      return LWXPRC_DRAW;
   }

Lastly, when the value of a control changes, we want to tell Modeler to redraw and rebuild us the next time it calls Dirty and Test, so we set box->update and box->dirty accordingly and precalculate the positions of our handles.

The Activation Function

Our activation function is significantly different from the ones in previous installments of this tutorial. Instead of being finished when the function returns, tool plug-ins haven't really begun yet.  The only thing a tool's activation function is required to do, and all ours does, is create an instance and tell Modeler where to find the callbacks. In this sense, Modeler tools are like Layout handlers, which supply callbacks that Layout later calls during animation and rendering.

   XCALL_( int )
   Activate( long version, GlobalFunc *global, LWMeshEditTool *local,
      void *serverData )
   {
      BoxData *box;

      if ( version != LWMESHEDITTOOL_VERSION )
         return AFUNC_BADVERSION;

Note that the third argument is now LWMeshEditTool instead of LWModCommand. Each plug-in class gets its own local data. As always, the first thing we do is ensure that the version of this structure in our copy of the headers is the same as the version being passed to us by Modeler.

      if ( !get_xpanf( global )) return AFUNC_BADGLOBAL;
      box = new_box();
      if ( !box ) return AFUNC_OK;
      local->instance = box;

The get_xpanf and new_box functions are in ui.c, since that's where the LWXPanelFuncs and LWSurfaceFuncs pointers and the local copy of the box settings are stored and used. get_xpanf gets the globals used by the interface, and new_box allocates a BoxData and initializes it with default values (or values remembered from previous invocations). The BoxData will be freed when Done is called.

      local->tool->done   = Done;
      local->tool->help   = Help;
      local->tool->count  = Count;
      local->tool->handle = Handle;
      local->tool->adjust = Adjust;
      local->tool->start  = Start;
      local->tool->draw   = Draw;
      local->tool->dirty  = Dirty;
      local->tool->event  = Event;
      local->tool->panel  = Panel;

      local->build        = Build;
      local->test         = Test;
      local->end          = End;

      return AFUNC_OK;
   }

And we're done! After returning from the activation function, Modeler will start calling your callbacks through the function pointers you've supplied.

Server Tags

Finally, note that we've added server tags to the ServerRecord array.

   static ServerTagInfo srvtag[] = {
      { "Tutorial: Box 4",    SRVTAG_USERNAME | LANGID_USENGLISH },
      { "create",             SRVTAG_CMDGROUP },
      { "objects/primitives", SRVTAG_MENU },
      { "Tut Box 4",          SRVTAG_BUTTONNAME },
      { "", 0 }
   };

These are explained in detail on the Common Elements page of the SDK. The user name appears in the interface in plug-in lists and popup menus. The server name is used if this isn't supplied, but there are lexical constraints on server names (they can't contain spaces, for example) that make them less user-friendly. Modeler is currently ignoring the MENU and CMDGROUP tags, but it may not in the future.

What's Next

Unless you had the evidence in front of you, you might not believe that a 40-page tutorial about writing box plug-ins was possible. But on this thin pretext, we've briefly visited most of the important techniques used to write plug-ins for LightWave Modeler. In the future, we might be seeing even more boxes on a similar tour of Layout...