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
count
start
dirty
test
mouse move
handle
adjust
dirty
test
build
draw
mouse up
end
spacebar
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...