Sub-Object Selection
See Also: Class CommandMode, Class Interface, Class XFormModes, Class BaseObject, Class Modifier.
Certain modifiers may wish to provide the system with different levels of sub-object selection. The are two general types of sub-object selection:
Selection of the modifier's gizmo. A modifier's gizmo is a visual representation in the scene the user can manipulate. This can be something as simple as the 3D box used by the bend modifier or something more complex like a FFD lattice. The Bend modifier provides sub-object levels of Center and Gizmo.
Selection of components of the object flowing through the geometry pipeline. If the object in the pipeline is a TriObject, for example, the sub-object selection levels may be things such as vertices, faces, and edges. The Edit Spline modifier supplies sub-object levels for Spline, Segments and Vertex.
A modifier registers its sub-object selection levels when its BeginEditParams() method is called. A method of the Interface class named RegisterSubObjectTypes() handles this. It provides the system with a list of strings. These strings are the names of the sub-object selection levels that will appear in the Sub-Object drop down list. For example, the following code would register two sub-object levels, "Gizmo" and "Center".
const TCHAR *ptype[] = { "Gizmo", "Center" };
ip->RegisterSubObjectTypes( ptype, 2 );
When the user presses the Sub-Object button or changes the selection in the sub-object drop down list the modifier is notified. The system calls the Modifier's ActivateSubobjSel() method. In its implementation of this method the modifier is expected to provide an instance of a class derived from CommandMode to support Move, Rotate, Uniform scale, Non-uniform scale, and Squash. These modes replace their object level counterparts although the user still uses the move/rotate/scale toolbar buttons to activate these modes.
Certain sub-object levels may not support all of these modes. For example, the Bend modifier (derived from SimpleMod) has two sub-object levels: "Gizmo" and "Center". The Gizmo level supports Move, Rotate, Non-uniform scale, Uniform scale and Squash while the Center level only supports Move. Any modes that the modifier does not support will have the corresponding button grayed out in the toolbar.
In addition to move/rotate/scale, a plug-in may provide other modes. For instance the Edit Mesh modifier has an extrude mode where the user can click and drag to interactively extrude faces of a mesh.
Users will expect sub-object move, rotate and scale transformations to behave like their object level counterparts. To make this very easy to implement, there are a set of standard modes defined by the system that a developer may use. These modes handle all this logic internally -- the developer simply uses them. Most modifiers will be able to use these modes. The sample code below demonstrates how the modes are used. This example is from the implementation of class SimpleMod.
In the class declaration the mode pointers are defined:
class SimpleMod : public Modifier {
...
protected:
static MoveModBoxCMode *moveMode;
static RotateModBoxCMode *rotMode;
static UScaleModBoxCMode *uscaleMode;
static NUScaleModBoxCMode *nuscaleMode;
static SquashModBoxCMode *squashMode;
...
In the BeginEditParams() method the modes are allocated:
void SimpleMod::BeginEditParams( IObjParam *ip, ULONG flags,
Animatable *prev )
{
...
// Create sub object editing modes.
moveMode = new MoveModBoxCMode(this,ip);
rotMode = new RotateModBoxCMode(this,ip);
uscaleMode = new UScaleModBoxCMode(this,ip);
nuscaleMode = new NUScaleModBoxCMode(this,ip);
squashMode = new SquashModBoxCMode(this,ip);
...
}
In the method ActivateSubobjSel() the modes are activated. This method is defined in BaseObject.
void SimpleMod::ActivateSubobjSel(int level, XFormModes& modes )
{
switch ( level ) {
case 1: // Modifier box
modes = XFormModes(moveMode,rotMode,nuscaleMode,
uscaleMode,squashMode,NULL);
break;
case 2: // Modifier Center
modes = XFormModes(moveMode,NULL,NULL,NULL,NULL,NULL);
break;
}
...
}
When the item is finished being edited the modes can be deleted:
void SimpleMod::EndEditParams( IObjParam *ip, ULONG flags,
Animatable *next)
{
...
ip->DeleteMode(moveMode);
ip->DeleteMode(rotMode);
ip->DeleteMode(uscaleMode);
ip->DeleteMode(nuscaleMode);
ip->DeleteMode(squashMode);
if ( moveMode ) delete moveMode;
moveMode = NULL;
if ( rotMode ) delete rotMode;
rotMode = NULL;
if ( uscaleMode ) delete uscaleMode;
uscaleMode = NULL;
if ( nuscaleMode ) delete nuscaleMode;
nuscaleMode = NULL;
if ( squashMode ) delete squashMode;
squashMode = NULL;
}
For a complete list of the classes that may be used see List of Standard Sub-Object Modes.
Both the standard transformation modes and custom modes are expected to treat sub-object selection in the same way 3ds max handles object selection. This means all the keypress options, such as Ctrl to add to a selection and Alt to subtract from a selection, should work in the same manner. To avoid forcing plug-in developers to implement all this logic, a SelectionProcessor class is provided. This class represents a mouse proc derived from the MouseCallback class. When you create an instance of the SelectionProcessor class you give its constructor a pointer to another MouseCallback -- the mouse proc to handle the mode being implemented. The SelectionProcessor receives the mouse input first. It processes the mouse input using a standard selection logic and then calls the MouseCallback it was given.
The sample code below is from \MAXSDK\SAMPLES\MESH. It shows how the extrude command mode of the Edit Mesh modifier uses the SelectionProcessor class to handle selection yet performs its own work after the selection process has been handled. The extrude command mode allows the user to select items with the mouse using all the standard logic such as box selection, and using the Ctrl and Alt keys. If the user drags the mouse however, the extrude operation is performed. By using the selection processor all the logic for selection is handled automatically.
The overall manner this is set up is as follows:
A command mode has a method MouseProc() that returns an instance of MouseCallBack to handle the user / mouse interaction. In the extrude command mode's implementation of MouseProc() it returns an instance of a selection processor (which is derived from MouseCallBack). The constructor for the selection processor takes a pointer to another MouseCallBack. This is the extrude mouse callback. When the command mode is processing mouse input, the selection processor mouse callback is called first. It processes all the logic for the selection. If the user drags the mouse however the selection processor calls the extrude mouse callback to handle the extrude operation. Below are the class definitions showing how this is set up.
The mouse callback below implements the user / mouse interaction for the extrude operation.
class ExtrudeMouseProc : public MouseCallBack {
private:
MoveTransformer moveTrans;
EditMeshMod *em;
IObjParam *ip;
IPoint2 om;
public:
ExtrudeMouseProc(EditMeshMod* mod, IObjParam *i)
: moveTrans(i) {em=mod;ip=i;}
int proc(
HWND hwnd,
int msg,
int point,
int flags,
IPoint2 m );
};
The code below defines a class from GenModSelectionProcessor. The constructor passes on the mouse proc for the extrusion interaction to the base class GenModSelectionProcessor. This allows the selection processor to call the extrusion mouse proc.
class ExtrudeSelectionProcessor : public GenModSelectionProcessor {
protected:
HCURSOR GetTransformCursor();
public:
ExtrudeSelectionProcessor(ExtrudeMouseProc *mc,
Modifier *m, IObjParam *i)
: GenModSelectionProcessor(mc,m,i) {}
};
The command mode declares an instance of the selection processor and extrude mouse proc. The extrude mouse proc is passed into the selection processor.
class ExtrudeCMode : public CommandMode {
private:
ChangeFGObject fgProc;
ExtrudeSelectionProcessor mouseProc;
ExtrudeMouseProc eproc;
EditMeshMod* em;
public:
ExtrudeCMode(EditMeshMod* mod, IObjParam *i) :
fgProc(mod), mouseProc(&eproc,mod,i), eproc(mod,i) {em=mod;}
int Class() { return MODIFY_COMMAND; }
int ID() { return CID_EXTRUDE; }
MouseCallBack *MouseProc(int *numPoints)
{ *numPoints=2; return &mouseProc; }
ChangeForegroundCallback *ChangeFGProc()
{ return &fgProc; }
BOOL ChangeFG( CommandMode *oldMode )
{ return oldMode->ChangeFGProc() != &fgProc; }
void EnterMode();
void ExitMode();
};
See the source code in EDITMOPS.CPP for the full implementation of the methods of these classes.
Modifier Methods
The overall process of sub-object selection relies upon the implementation of several methods in the modifier. These methods are from class BaseObject. These involve selecting, identifying, moving, rotating and scaling sub-object components. These methods are:
virtual void ClearSelection(int selLevel);
This method is called to clear the selection for the given sub-object level. All sub-object elements of this type should be deselected.
virtual void SelectSubComponent(HitRecord *hitRec, BOOL selected, BOOL all, BOOL invert=FALSE)
This method is called to change the selection state of the component identified by hitRec.
virtual int SubObjectIndex(HitRecord *hitRec);
Returns the index of the sub-object element identified by the HitRecord hitRec. The sub-object index identifies a sub-object component. The relationship between the index and the component is established by the modifier. For example, an edit modifier may allow the user to select a group of faces and these groups of faces may be identified as group 0, group 1, group 2, etc. Given a hit record that identifies a face, the edit modifier's implementation of this method would return the group index that the face belonged to.
virtual void CloneSelSubComponents(TimeValue t);
This method is called to make a copy of the selected sub-object components.
virtual void Move(TimeValue t, Matrix3& partm, Matrix3& tmAxis,
Point3& val, BOOL localOrigin=FALSE )
When this method is called the plug-in should respond by moving its selected sub-object components.
virtual void Rotate(TimeValue t, Matrix3& partm, Matrix3& tmAxis,
Quat& val, BOOL localOrigin=FALSE )
When this method is called the plug-in should respond by rotating its selected sub-object components.
virtual void Scale(TimeValue t, Matrix3& partm, Matrix3& tmAxis,
Point3& val, BOOL localOrigin=FALSE )
When this method is called the plug-in should respond by scaling its selected sub-object components.
Sub-Object Selection in Edit Modifiers
A certain class of modifiers, referred to as edit modifiers, allow specific object types to be edited. For example, the Edit Mesh modifier allows TriObjects to be edited while the Edit Patch modifier allows patch objects to be edited. A generic Edit Point modifier would edit any object that supports the deformable interface.
The sub-object selection levels for these edit modifiers reflect the sub-object types for the object type that the edit modifier is designed to edit. For example, the Edit Mesh modifier supports three sub-object selection levels: vertex, face and edge. These correspond to actual elements of a TriObject. In contrast, the sub-object levels of the Bend modifier, are gizmo and center. These have no correspondence with the object that the bend modifier is modifying.
Edit modifiers typically allow the user to select sub-object elements of the object and perform at least the standard move/rotate/scale transformations. They may also support other operations such as the Edit Mesh modifier's extrude operation.
When the user creates sub-object selections with an edit modifier, they are typically stored in the object because they correspond to actual parts of the object. The sub-object selection is actually part of the object's state that flows up the pipeline. For example, the Edit Mesh modifier allows the user to select vertices, faces or edges. If a Bend modifier is then applied after the Edit Mesh modifier, it will operate on the selected vertices or faces. If the user goes back in the history to the Edit Mesh modifier and changes the selection, the set of vertices that the bend affects will change. Essentially just editing the selection is a form of editing an object because the selection is part of the object.
Edit modifiers have to store the selection themselves. If the input to an edit modifier changes, the modifier will need to reapply its modifications. Since changing the selection set is a modification to the object, the edit modifier will need to recreate the selection set.
Selection sets are not the only thing edit modifiers need to store. If an edit modifier moves points on a modifier, then it needs to store, in some form, the changes it made to the points. For example, the Edit Mesh modifier stores changes made to the vertices of a mesh by maintaining an array of 3D vectors that represent a delta for each vertex. This delta is applied to an object by simply adding each delta to its corresponding vertex. This brings up a couple of issues:
What happens if the number of vertices of the input object change? Since the vertex index is used to find its corresponding delta, if the topology of an object changes, a vertex's index might change. If this is the case the user is given a warning message that a modifier exists in the stack that depends on topology (see the method Modifier::DependOnTopology() for the details on how an edit modifier informs the system it is dependent on topology). If the user selects OK from the warning message dialog the topology may be changed. The least that an edit modifier should do is not reference a vertex out of the range of an object's vertex array. The Edit Mesh modifier continually scales the length of its delta array to match the length of the vertex array of the incoming object. This ensures that the deltas are all applied to legitimate vertices, but if the topology changes, the effect is rarely useable.
The user may use the volumetric selector modifier to help eliminate this problem. This modifier uses spheres, 3D boxes, and cylinders to define a 3D region. Points inside the region become selected while points outside are not. This method of selection is independent of topology and therefore does not exhibit the previously-described problems.
Modifiers can be instanced and therefore a modifier, in general, isn't just applied to a single object. This raises the question of what happens when an edit modifier is instanced. It must store its changes to a particular object somewhere. One of the fields of the ModContext is a pointer to a LocalModData derived class. The most convenient place to store this data is here in the ModContext. When an object is copied, the ModContext (and therefore the LocalModData) is also copied making it possible to copy objects with edit modifiers applied to them.
Named Sub-Object Selection Sets
A modifier that supports sub-object selection can choose to support named sub-object selection sets. This allows the user to save and restore certain sub-object selection sets and not have to pick them over and over. The modifier indicates it wishes to do so by returning TRUE from the following method of BaseObject.
virtual BOOL SupportsNamedSubSels();
Returns TRUE if the plug-in supports named sub-object selection sets; otherwise FALSE.
A modifier that wishes to support this capability maintains its list of named sub-object selections. When the user enters sub-object selection mode the modifier should add its named selection sets into the drop down using the following method of Class Interface:
virtual void AppendSubObjectNamedSelSet(const TCHAR *set)=0;
A modifier may call this method to add sub-object named selection sets to the named selection set drop down list in the 3ds max toolbar. This should be done whenever the selection level changes (in the Modifiers BaseObject::ActivateSubobjSel() method).
The following methods of BaseObject are called when the user picks items from the drop down list:
virtual void ActivateSubSelSet(TSTR &setName);
When the user chooses a name from the drop down list this method is called. The plug-in should respond by selecting the set identified by the name passed.
virtual void NewSetFromCurSel(TSTR &setName);
If the user types a new name into the named selection set drop down then this method is called. The plug-in should respond by creating a new set and give it the specified name.
virtual void RemoveSubSelSet(TSTR &setName);
If the user selects a set from the drop down and then chooses Remove Named Selections from the Edit menu this method is called. The plug-in should respond by removing the specified selection set.
The following methods from Class Interface also deal with named sub-object selection sets:
virtual void ClearSubObjectNamedSelSets()=0;
This method clears the named selections from the drop down.
virtual void ClearCurNamedSelSet()=0;
This method clears the current edit field of the named selection set drop down.
For sample code that deals with named sub-object selection sets see \MAXSDK\SAMPLES\MODIFIERS\EDITMESH.CPP.