Function Publishing System
Class FPInterface, Class FPMixinInteface, Class FPInterfaceDesc, Class Interface_ID, Class FPFunctionDef, Class FPPropDef, Class FPActionDef, Class FPParamDef, Class FPParams, Class FPEnum, Class FPInterfaceCallback, Class FPValue, Class FPParamOptions, Class FPValidator, Class ClassDesc2, Class ActionTable.
Abstract
What is Function Publishing?
The Function Publishing System, new to 3ds max R4, is a new system that allows plugins to publish their major functions and operations in such a way that code outside the plugin can discover and make enquiries about these functions and is thus able to call them though a common calling mechanism. The whole system is very similar to Window’s COM and OLE Automation systems and share many similar concepts in the architecture. However, the Function Publishing System is not based on COM and OLE but instead is a custom architecture more suited and optimized for MAX. The Function Publishing API serves a number of purposes which allow 3rd party developers to open up important portions of their plugins for use by external sources, allowing for users to extend and control these directly. Some of the purposes of the API are:
-
· Modularizing plugin code into various "engines" that are able to supply services to other parts of 3ds max and other 3rd party plugins and can be delivered to the user through various different user-interfaces.
-
· Providing automatic scriptability by exposing the published functions directly to MAXScript.
-
· Providing alternate means of invoking plugin functions in the UI, such as via the new manipulator system, scripted menus, quad menus, hot keys, macroScripts, toolbar buttons, etc.
-
· Allowing the MAXScript Macro Recorder to automatically generate calls to the published functions, in the event that these are invoked by the expanded ParamMap2 system or other UI mechanisms such as hot keys or menu items.
-
· Facilitate automatic generation of COM interfaces and OLE Type Libraries in such a way that external COM clients can invoke the published functions in the plugin code.
What would a plugin publish?
The various kinds of functions published by a plugin usually fall into the following categories. You can, however, publish anything you want.
-
· Important algorithms in the plugin, for example, the Edit Mesh modifier might publish its face extrude and mesh attach functions, or the flex modifier its soft-body dynamics algorithms. In these cases, the functions would be parameterized in the most general way, independent of any current scene state or UI mode in MAX, for example, the face extrude might take a Mesh, a set of faces and a distance.
-
· Functions that enquire about or affect the state of one of the plugin's objects in the scene. Usually, these are unnecessary if the plugin stores its state as parameters in ParamBlock2s, which are already accessible externally, but in cases were this is not done or certain kinds of state are not cleanly accessible via the ParamBlock2 system, extra functions may be published by the plugin. These usually take an instance of one of the plugin's objects as one of their parameters, and should be independent of any UI mode, for example, they should not require that the object being manipulated be the current focus in the Command Panel. Prior to R4, plugins that wished to provide scripter access to internal functions would use the MAXScript SDK to provide scripter wrapper functions. In R4, the recommended and much simpler technique, is to use the Function Publishing system, as this not only provides automatic scriptability, but is a general mechanism for any external system to control and use the plugin.
-
· UI action functions. These basically provide a programmatic way of "pressing" buttons and keys in the UI for a plugin and are specifically meant to be UI modal. They take no parameters, since these are defined by the current state in the UI. For example, the vertex delete action function for an Editable Mesh object would operate on the current vertex selection for the current object in the Modify panel. These are not unlike the keyboard ShortcutTables that plugins could publish in R3, but by publishing them as action functions, any external system can effectively control the UI of a plugin. As well as being automatically exposed in the scripter like other published functions, Action functions are automatically entered into the new ActionTable system. This is a generalization of the R3 ShortcutTable system and basically holds all the commands and actions that may be bound to hotkeys or added to menus or put in buttons on a toolbar using the R4 CUI system, so that by publishing your action functions using the FnPub system, you are making them automatically available for binding to hotkeys and placing in menus and toolbar buttons. Action functions are treated specially in the FnPub system, and have extra descriptor data for things like menu item text, tooltip text, enable predicates, etc.
Introduction to Function Publishing
The Function Publishing API
The Function Publishing API consists of two main components, a function descriptor system and a function calling mechanism. The descriptor system is used by the publishing component to declare the functions it is publishing and provide necessary descriptive data and may be used by a component’s client to enquire about published functions. The calling system is used by a component’s client to invoke one of the published functions.
Interfaces
Functions are published in one or more Interfaces by your plugin. If you have a large number of functions to be published, you might organize them functionally into different interfaces to make them more manageable for the user. For example, EditMesh might publish vertex functions, edge functions and face functions in separate interfaces. Action functions must be published in their own (set of) interface(s). Each interface is represented by an instance of a class derived from the base class FPInterface. You normally create these instances as static objects using the FPInterface constructor, in much the same way as ParamBlock2 descriptors are created. Each interface contains a list of the functions it publishes and the parameters they take, along with type and name and other descriptive info for all these things. All the interfaces for a particular plugin class are kept in it's ClassDesc object. An external system can find out about the interfaces you publish by calling various query methods on ClassDesc that access these interface definition objects. As well as these enquiry or 'reflection' methods, an FPInterface also has the calling methods for actually invoking a particular function in the interface, so that if something has hold of one of your interfaces, it can call any of its published functions.
Additionally, a Mixin interface is provided which can be multiply-inherited by a plugin's class and returned via its implementation of the above new GetInterface(Interface_ID) method, in the way that most existing object-based interfaces are now implemented.
The FPInterface class is defined in the header file maxsdk\include\iFnPub.h, along with all the other FnPub classes & macros described in these notes.
Direct and Indirect Calling
The FnPub system lets you set up interfaces so that functions in them can be called directly, as virtual member functions of the interface object, or indirectly via a dispatching method that takes a runtime function ID and a table of parameters. This is roughly equivalent to the dual vtable and IDispatch interface schemes in COM. Typically, you provide the virtual interface in a public header file so that it can be compiled against by other plugins and they can call the virtual functions on an interface object directly. The indirect call mechanism is used by MAXScript and other external systems that look for the published interface functions at runtime using the interface metadata.
Interface and Function IDs
Each interface is uniquely indentified by an Interface_ID, which is a new class in the R4 SDK. This class is structurally very similar to Class_ID, containing two randomly-chosen longwords to provide a unique global ID. It is defined in maxsdk\include\maxtypes.h
Each function in an interface is identified by an integer ID of type FunctionID. This is similar to the BlockID and ParamID's used in the paramblock2 system. This ID is used in the dispatch-based calling mechanism to identify the function to call.
Interface Organization
The map is bounded by BEGIN_FUNCTION_MAP and END_FUNCTION_MAP and contains one entry for each function in the interface. The map entry macros used come from a set named according to the number of arguments and whether the function is void. In this case, FN_2 is used for a 2 argument function returning a value and VFN_3 is used for a void function taking 3 arguments. The FN_2 macro, for example, takes the function ID, the return type, the virtual interface method name, and the types of the two arguments. FN_VA, VFN_VA, FNT_VA, etc., FUNCTION_MAP entry macro variants have same args as _0 macros and specify that the function takes a variable number of arguments (passed directly in FPParams instances). There are more details on these macros in the ref section below. There is a separate declaration macro, DECLARE_DESCRIPTOR(<class>) that must be specified in the interface descriptor class. In the implementation class for a static or action or core interface, you provide a single descriptor declarator with the DECLARE_DESCRIPTOR() macro, giving the current class as the macro parameter and a function map using BEGIN_FUNCTION_MAP and END_FUNCTION_MAP macros as before. The DECLARE_DESCRIPTOR(<class>) is only required for FPStaticInterface subclasses; FPMixinInterface subclasses should only have the FUNCTION_MAP/END_FUNCTION_MAP map table.
Within your plugin, each interface is usually organized into 3 separate sections of code:
-
· The public virtual interface along with the Interface_ID and function ID definitions in a public header file.
-
· the implementation interface, which inherits from the virtual interface, and contains both the function implementations and a function dispatch map used by the indirect calling methods. The FnPub system provides a set of macros to ease the definition of these maps.
-
· the interface descriptor, usually a static instance of the implementation interface. The constructor for this uses the same varargs technique used by the ParamBlockDesc2 constructor, enabling descriptive info for all the functions in the interface to be supplied in one constructor call.
Here's an (imaginary & simplified) example of an interface on EditMesh called 'FaceOps' with two functions, delete and extrude:
1. The public interface (in a public header file)
#include "iFnPub.h"
#define EM_FO_INTERFACE Interface_ID(0x434455, 0x65654)
#define GetEMFaceOpsInterface(cd) \
(EMFaceOps *)(cd)->GetFPInterface(EM_FO_INTERFACE)
enum { em_delete, em_extrude, };
class EMFaceOps : public FPStaticInterface
{
public:
virtual int Delete (Mesh* m, BitArray* faces)=0;
virtual void Extrude(Mesh* m, BitArray* faces, float amt)=0;
};
This defines the Interface_ID for the 'FaceOps' interface and provides a helper macro for getting the interface pointer from the plugin's ClassDesc. This is followed by an enum for the two function IDs and then the virtual interface class itself in which each function is declared as a pure virtual function. The return and parameter types of these functions are restricted to a fixed set, defined below. Fixing this set is necessary so that systems that access the interface metadata or use the dispatch form of calling have a known set of types to deal with. External code that wishes to call one of these functions, and has access to this public header can do so as in the following example:
EMFaceOps* efi = GetEMFaceOpsInterface(edmeshCD);
...
efi->Extrude(mesh, faces, 10.0);
To call this function indirectly, you call the Invoke() method on the interface, giving it the function ID and an FPParams object containing all the parameters, as in the following example:
FPParams p (3, TYPE_MESH, mesh,
TYPE_BITARRAY, faces,
TYPE_FLOAT, 10.0);
FPValue result;
FPInterface efi = edmeshCD->GetFPInterface("FaceOps");
efi->Invoke(em_extrude, result, &p);
x = result.i;
Any function result is passed back in an FPValue instance, given as one of the args to Invoke, which is a type-tagged variant structure that can hold any one of the FnPub supported types. The FPParams class contains a convenience varargs constructor for building parameter sets as shown. An FPParams instance contains a Tab<> of FPValue instances, each holding a parameter. This example demonstrates how to look up an interface by name in a ClassDesc. The Invoke() function has several overloads, for calling functions with and without results and parameters.
2. The implementation class (in the .cpp implementation file)
class EMFaceOpsImp : public EMFaceOps
{
DECLARE_DESCRIPTOR(EMFaceOpsImps)
BEGIN_FUNCTION_MAP
FN_2(em_delete, TYPE_INT, Delete, TYPE_MESH, TYPE_BITARRAY)
VFN_3(em_extrude, Extrude, TYPE_MESH, TYPE_BITARRAY, TYPE_FLOAT)
END_FUNCTION_MAP
int Delete(Mesh* m, BitArray* faces)
{
//... do the delete
return face_count;
}
void Extrude(Mesh* m, BitArray* faces, float amt)
{
//... do the extrude
}
};
The interface implementation class specializes the virtual interface class defined in the public header. It can contain any implementation-required data members and utility methods, but must at least contain implementations for each of the virtual methods defined in the virtual interface class.
The key component shown above is the function map which generates the indirect call dispatcher used by the Invoke() method. This dispatcher unbundles the parameters from the indirect call parameter structure, forwards them to the correct implementation method and bundles the return value into an FPValue. It is specified here using the map macros that come with the FnPub system (in iFnPub.h), in a manner somewhat similar to the message maps in MFC. You can implement this dispatcher by hand, but it is advised that you use the map macros.
The map is bounded by BEGIN_FUNCTION_MAP and END_FUNCTION_MAP and contains one entry for each function in the interface. The map entry macros used come from a set named according to the number of arguments and whether the function is void. In this case, FN_2 is used for a 2 argument function returning a value and VFN_3 is used for a void function taking 3 arguments. The FN_2 macro, for example, takes the function ID, the return type, the virtual interface method name, and the types of the two arguments. There are more details on these macros in the ref section below. There is a separate declaration macro, DECLARE_DESCRIPTOR(<class>) that must be specified in the interface descriptor class. In the implementation class for a static or action or core interface, you provide a single descriptor declarator with the DECLARE_DESCRIPTOR() macro, giving the current class as the macro parameter and a function map using BEGIN_FUNCTION_MAP and END_FUNCTION_MAP macros as before. The DECLARE_DESCRIPTOR(<class>) is only required for FPStaticInterface subclasses; FPMixinInterface subclasses should only have the FUNCTION_MAP/END_FUNCTION_MAP map table.
3. The interface definition (in the .cpp implementation file)
static EMFaceOpsImp emfi (
EM_FO_INTERFACE, _T("FaceOps"), IDS_EMFO, &edmeshCD, 0,
em_delete, _T("delete"), IDS_DELETE, TYPE_INT, 0, 2,
_T("mesh"), IDS_MESH, TYPE_MESH,
_T("faces"), IDS_FACES, TYPE_BITARRAY,
em_extrude, _T("extrude"), IDS_EXTRUDE, TYPE_VOID, 0, 3,
_T("mesh"), IDS_MESH, TYPE_ MESH,
_T("faces"), IDS_FACES, TYPE_BITARRAY,
_T("amount"), IDS_AMOUNT, TYPE_FLOAT,
end
);
A distinguished instance of the interface implementation class is constructed and registered with the owning ClassDesc. This is the instance given out when the CD is asked for an interface via its GetFPInterface() method. The instance has data members that contain all the metadata for the interface. You normally build this instance as a static in the implementing .cpp file, in a manner similar to ParmBlock2 descriptor instances.
In the example above, the instance 'emii' is statically declared using the FPInterface's varargs constructor. The initial arguments to this constructor provide the Interface_ID, the internal fixed name, localizable descriptor string resID, owning ClassDesc and flags bits. This is followed by a list of function descriptors which themselves each have lists of parameter descriptors. The em_delete function description, for example, consists of a fixed name, a localizable description string resID, a return type, flag bits and a count of the number of parameters. For each parameter, there is a line giving parameter name, description string resID and type.
FnPub Object Based Mixin Interfaces
A variant of the FPInterface, known as a Mixin interface, is provided abd can be multiply-inherited by a plugin's class and returned via its implementation of the above new GetInterface(Interface_ID) method, in the way that most existing object-based interfaces are now implemented. Here's an example setup. First, the public header that would be used by an SDK-level client of the interface:
// interface ID
#define FOO_INTERFACE Interface_ID(0x342323, 0x55664)
#define GetFooInterface(obj) \
((FooInterface*)obj->GetInterface(FOO_INTERFACE))
// function IDs
enum { foo_move, foo_setRadius, };
// mixin interface
class FooInterface : public FPMixinInterface
{
BEGIN_FUNCTION_MAP
VFN_1(foo_move, Move, TYPE_POINT3);
VFN_1(foo_setRadius, SetRadius, TYPE_FLOAT);
END_FUNCTION_MAP
FPInterfaceDesc* GetDesc();
virtual void SetRadius(float radius)=0;
virtual void Move(Point3 p)=0;
};
This is much the same as existing stand-alone FnPub interfaces, except that the function map is put into the virtual interface class, rather than the implementing interface class. Then, in the plugin class, you inherit the mixin interface (you are "mixing" it into the class). You provide implementations for the interface's virtual methods in the main class and an implementation of GetInterface() that returns the object cast to the interface, exactly as you would have done for old-style object-based interfaces:
class MyObject : public SimpleObject2, public FooInterface
{
public:
...
void SetRadius(float radius);
void Move(Point3 p);
...
FPInterface* GetInterface(Interface_ID id) {
if (id == FOO_INTERFACE)
return (FooInterface*)this;
else
return SimpleObject2::GetInterface(id);
}
...
}
Note that the GetInterface() method needs to cast the 'this' to the mixin interface class so the correct vtable is used (this was the case with old-style object interfaces, as well). It also calls SimpleObject2::GetInterface() if the id doesn't match so that other base class interfaces and stand-alone interfaces, if any, are made available to the caller. Finally, you provide a descriptor for the interface, as with stand-alone interfaces, but in this case, you must make it an instance of FPInterface, not FooInterface or MyObject, and must specify the FN_MIXIN flag to denote the interface as mixin:
static FPInterfaceDesc foo_mixininterface(FOO_INTERFACE,
_T("foo"), 0,
&myObjDesc, FP_MIXIN,
foo_move, _T("move"), 0, TYPE_VOID, 0, 1,
_T("vector"), 0, TYPE_POINT3,
foo_setRadius, _T("setRadius"), 0, TYPE_VOID, 0, 1,
_T("radius"), 0, TYPE_FLOAT,
end
);
FPInterfaceDesc* FooInterface::GetDesc()
{
return &foo_mixininterface;
}
This static instance provides the interface metadata and is recorded in the ClassDesc. To access the interface metadata, you must get this instance directly from the ClassDesc via ClassDesc::GetInterface(), and not via Animatable::GetInterface() on an object.
All this is pretty-much the same work you would do to provide an Animatable::GetInterface() interface currently in MAX. The extra stuff is the FUNCTION_MAP and the interface descriptor. The SDK-level clients use the interface in exactly the same way, but now the scripter and other external systems can find and use these interfaces automatically at runtime.
Mixin object-based interfaces are accessible in the scripter in similar way to stand-alone published interfaces. In particular, the functions are available in a struct function package which is itself accessed as a property on the plugin class object. For example, if the above example plugin class is named 'MyObject' in the scripter, the functions in its 'foo' mixin interface would be accessed as:
MyObject.foo.move
MyObject.foo.setRadius
The functions are effectively 'generic' functions that require an instance of the plugin as the first argument, resulting in:
MyObject.foo.move $baz [10,10,10]
MyObject.foo.setRadius $bar 123.4
Publishing Mixin Interface on Arbitrary Classes
You can publish FnPub mixin interfaces on any class, not just Animatable subclasses. To do this, you inherit from, IObject, a new virtual base class in the FnPub system. This provides an API for querying and iterating FPMixinInterfaces that the class wishes to publish, similar to Animatable::GetInterface(). The API that the publishing class must implement is as follows:
class IObject
{
public:
// object/class name
virtual TCHAR* GetName()=0;
// iterate over all interfaces...
virtual int NumInterfaces()=0;
virtual FPInterface* GetInterface(int i)=0;
// get ID'd interface
virtual FPInterface* GetInterface(Interface_ID id)=0;
// IObject ref management (can be implemented by dynamically-
// allocated IObjects for ref-count based lifetime control)
virtual void Acquire() { };
virtual void Release() { };
};
There is a corresponding new ParamType2 type code, TYPE_IOBJECT, that allows instances of these classes to be passed and returned in FPInterface methods, providing a simple form of user-defined type, in the sense that these instance collections are passed as interfaces rather than pointers (similar to COM). MAXScript has been extended to provide wrapper value classes for IObjects and so this mechanism provides a light-weight alternative to the MAXScript SDK facilities for adding new wrapper value classes to the scripter.
MAXScript also calls the Acquire() and Release() methods on IObjects as it creates and collects these wrappers, so that IObject objects can keep track of MAXScript's extant references to them. For example:
class IFoo1 : public FPMixinInterface
{
virtual void Frabulate(Point3 p)=0;
...
};
class IFoo2 : public FPMixinInterface
{
...
};
class Foo : public IObject, public IFoo1, public IFoo2
{
// Foo methods
...
// IObject methods
TCHAR* GetName() { return _T("Foo"); }
int NumInterfaces() { return 2; }
FPInterface* GetInterface(int i)
{
if (i == 0) return (IFoo1*)this;
if (i == 1) return (IFoo2*)this;
}
FPInterface* GetInterface(Interface_ID id)
{
if (id == FOO_INTERFACE_1) return (IFoo1*)this;
if (id == FOO_INTERFACE_2) return (IFoo2*)this;
}
// IFoo1 methods
void Frabulate(Point3 p) { ... }
...
// IFoo2 methods
...
};
Instances of Foo can be passed as parameters and results in FnPub interface descriptors and function maps by declaring them as TYPE_IOBJECT values, and they will get cast to IObject* automatically. For example, in a plugin that uses the Foo class, a method in one of its FPInterfaces might want to return a Foo instance. The method might be defined as:
Foo* GetFoo(INode* object)
{
Foo* x = new Foo (...);
...
return x;
}
Since Foo is not a supported base ParamType2 type but is derived from IObject, the return value of the above method can be declared using TYPE_IOBJECT in the FUNCTION_MAP as:
FN_1(my_getFoo, TYPE_IOBJECT, GetFoo, TYPE_INODE);
and in the descriptor constructor as:
my_getFoo, _T("getFoo"), 0, TYPE_IOBJECT, 0, 1,
_T("object"), 0, TYPE_INODE,
In MAXScript, calling getFoo() would return an IObject value that has two interface properties, ifoo1 and ifoo2, and these in turn would expose their methods as properties:
f = getFoo $
f.ifoo1.frabulate [10,0,0] // call the IFoo1 frabulate method
Passing FPInterfaces as Parameters & Results
FPInterfaces themselves can now be passed directly as parameters and results via the new type, TYPE_INTERFACE. These turn up in MAXScript as instances of a new value class, FPInterface, with all the interface's methods accessible as properties.
In cases where you have an class publishing a single mixin interface, it is possible to pass instances directly as TYPE_INTERFACE and let the FPInterface* type-cast implied by TYPE_INTERFACE extract the mixin interface. This is a useful simple alternative to the IObject scheme for publishing a mixin interface on non-Animatables, since you don't actually need to implement the IObject protocol in these cases. If the Foo example above had a single mixin, it would publish it in a similar way to the following:
class IFoo : public FPMixinInterface
{
virtual void Frabulate(Point3 p)=0;
...
};
class Foo : public OtherBaseClass, public IFoo
{
// Foo methods
...
// IFoo methods
void Frabulate(Point3 p) { ... }
...
};
The same Foo*-returning GetFoo() method in the previous example would then be declared using TYPE_INTERFACE in the FUNCTION_MAP as:
FN_1(my_getFoo, TYPE_INTERFACE, GetFoo, TYPE_INODE);
and in the descriptor constructor as:
my_getFoo, _T("getFoo"), 0, TYPE_INTERFACE, 0, 1,
_T("object"), 0, TYPE_INODE,
In this case, calling getFoo() in MAXScript would return an FPInterface value that exposes the IFoo methods directly as properties.
f = getFoo $
f.frabulate [10,0,0] // call the Foo1 frabulate method
Note again this scheme won't work for class publishing multiple multiple mixins since implied FPInterface* cast becomes ambiguous - you must use the IObject mechanism in this case.
FP_CORE Interfaces
There is an interface descriptor flag, FP_CORE, which must be specified on Core interface descriptors. Static Core interface descriptors are now automatically registered with the Core, so you do not need to explicitly call RegisterCOREInterface() on them. The code examples below include a Core interface using this flag. The following example taken from the DragAndDrop manager interface.
1. The public interface definition (in a public header file)
class IDragAndDropMgr : public FPStaticInterface
{
public:
virtual void EnableDandD(BOOL flag)=0;
virtual BOOL IsEnabled()=0;
virtual BOOL EnableDandD(HWND hwnd, BOOL flag,
DragAndDropHandler* handler = NULL)=0;
virtual BOOL DropPackage(HWND hwnd, POINT& point,
URLTab& package)=0;
virtual BOOL DownloadPackage(URLTab& package, TCHAR* directory,
HWND hwnd = NULL)=0;
virtual TCHAR* GetStdDownloadDirectory()=0;
};
2. The implementation class (in the .cpp implementation file)
Note in this example, a couple of the interface methods use a new type, URLTab, which is not a ParamType2 type. This is a Tab<TCHAR*> specialization and so provides overloads that take Tab<TCHAR*>s and convert them to URLTabs, specifically for dispatch-based calls.
class DragAndDropMgr : IDragAndDropMgr
{
...
public:
void EnableDandD(BOOL flag) { global_enable = flag; }
BOOL IsEnabled() { return global_enable; }
BOOL EnableDandD(HWND hwnd, BOOL flag,
DragAndDropHandler* handler = NULL);
BOOL DropPackage(HWND hwnd, POINT& point, URLTab& package);
BOOL DropFiles(HWND hwnd, HDROP hDrop);
BOOL DownloadPackage(URLTab& package, TCHAR* directory,
HWND hwnd = NULL);
TCHAR* GetStdDownloadDirectory();
// variants for FnPub interface that take Tab<TCHAR*>s
BOOL DropPackage(HWND hwnd, POINT& point, Tab<TCHAR*>& package);
BOOL DownloadPackage(Tab<TCHAR*>& package, TCHAR* directory);
DECLARE_DESCRIPTOR(DragAndDropMgr)
// dispatch map
BEGIN_FUNCTION_MAP
VFN_1(dndmgr_globalEnableDnD, EnableDandD, TYPE_BOOL);
FN_0(dndmgr_isEnabled, TYPE_BOOL, IsEnabled);
FN_2(dndmgr_enableDandD, TYPE_BOOL, EnableDandD,
TYPE_HWND, TYPE_BOOL);
FN_3(dndmgr_dropPackage, TYPE_BOOL, DropPackage,
TYPE_HWND, TYPE_POINT_BR, TYPE_STRING_TAB_BR);
FN_2(dndmgr_downloadPackage, TYPE_BOOL, DownloadPackage,
TYPE_STRING_TAB_BR, TYPE_STRING);
FN_0(dndmgr_downloadDirectory, TYPE_STRING,
GetStdDownloadDirectory);
END_FUNCTION_MAP
...
};
3. The descriptor, note the FP_CORE flag.
FP_CORE descriptors are automatically registered with RegisterCOREInterface().
DragAndDropMgr dragAndDropMgr(DND_MGR_INTERFACE, _T("dragAndDrop"),
IDS_DND_INTERFACE, NULL, FP_CORE,
dndmgr_globalEnableDnD, _T("globalEnableDragAndDrop"), 0,
TYPE_BOOL, 0, 1, _T("onOff"), 0, TYPE_BOOL,
dndmgr_isEnabled, _T("isEnabled"), 0, TYPE_BOOL, 0, 0,
dndmgr_enableDandD, _T("enableDragAndDrop"), 0, TYPE_BOOL, 0, 2,
_T("window"), 0, TYPE_HWND, _T("onOff"), 0, TYPE_BOOL,
dndmgr_dropPackage, _T("dropPackage"), 0, TYPE_BOOL, 0, 3,
_T("window"), 0, TYPE_HWND, _T("mousePoint"), 0,
TYPE_POINT_BR, _T("files"), 0, TYPE_STRING_TAB_BR,
dndmgr_downloadPackage, _T("downloadPackage"), 0, TYPE_BOOL, 0,
2, _T("files"), 0, TYPE_STRING_TAB_BR, _T("directory"), 0,
TYPE_STRING,
dndmgr_downloadDirectory, _T("geStdtDownloadDirectory"), 0,
TYPE_STRING, 0, 0,
end
Action Interfaces
A special kind of interface is the Action Interface. These interfaces only contain UI Action Functions that provide a programmatic way of "pressing" buttons and keys in the UI for a plugin. As mentioned in the opening section of this doc, these action fucntions have certain special characeteristics and possess additional descriptive metadata, relative to the functions we've described so far. Here's an annotated Action Interface example for the EdMesh system we looked at above.
1. The public interface (in a public header file)
#include "iFnPub.h"
#define EM_ACT_INTERFACE Interface_ID(0x65678, 0x123)
#define GetEMActionsInterface(cd) \
(EMActions*)(cd)->GetFPInterface(EM_ACT_INTERFACE)
enum {
ema_create, ema_create_enabled, ema_create_checked,
ema_delete, ema_delete_enabled,
};
class EMActions: public FPInterface
{
public:
virtual FPStatus Create()=0;
virtual FPStatus Delete()=0;
virtual BOOL IsCreateEnabled()=0;
virtual BOOL IsCreateChecked()=0;
virtual BOOL IsDeleteEnabled()=0;
};
As with normal function interfaces, we we have an Interface_ID defined and interface accessor helpers. There is also a function ID enum, but in this case there are more IDs than published actions. Each action function can have associated with it up to 3 predicate functions that users of the interface can call to determine status for the action. These are the isEnabled, isChecked, and isVisible predicates and they are used mostly by the UI elements that can be bound to an interface action to determine things like whether a button or menu item should greyed or a checkbutton should be highlighted or whether the action should be added to an about-to-be-displayed pop-up menu. The isEnabled & isVisible predicates are optional and default to always TRUE. The isChecked predicate is for actions that toggle a state, for example starting & stopping a mouse command mode. This predicate is also optional and defaults to FALSE, but it is required if the action is bound to TYPE_CHECKBUTTON via the ParamMap2 system.
Declarations for action functions along with any associated predicates are supplied in the virtual interface class. Action functions always take zero arguments and return an FPStatus result which indicates whether the action was succesfully performed or not (succes if FPS_OK, but it may return values such as FPS_ACTION_DISABLED if the action was not enabled at the time). Predicate functions always take zero arguments and return a BOOL.
Here are some action function call examples:
EMActions* emai = GetEMActionsInterface(edmeshCD);
...
if (emai->IsCreateEnabled()) // direct calls
emai->Create();
Indirect Calls:
if (emai->IsEnabled(ema_delete))
emai->Invoke(ema_delete);
This indirect example uses one of the predicate helper functions to call the isEnabled predicate in the interface for a given function, specified by FunctionID.
2. The implementation class (in the .cpp implementation file)
class EMActionsImp : public EMActions
{
public:
BOOL creating;
DECLARE_DESCRIPTOR(EMActionsImp)
BEGIN_FUNCTION_MAP
FN_ACT(ema_create, Create)
FN_PRED(ema_create_enabled, IsCreateEnabled)
FN_PRED(ema_create_checked, IsCreateChecked)
FN_ACT(ema_delete, Delete)
FN_PRED(ema_delete_enabled, IsDeleteEnabled)
END_FUNCTION_MAP
void Init()
{
// initialize any state data
creating = FALSE;
}
FPStatus Create()
{
//... start or stop the create mode
//...
creating = !creating; // toggle creating state
//...
return FPS_OK;
}
BOOL IsCreateEnabled()
{
//... determine if create enable, perhaps
// vertex subobjlevel test, etc.
}
BOOL IsCreateChecked()
{
return creating; // the current create state
}
//...
};
The action interface implementation class contains implementations for the actions and predicates, as required by the virtual interface. It also contains a FUNCTION_MAP, but in this case the actions and predicates entries in the map are defined with their own special FN_ macros, FN_ACT for actions and FN_PRED for predicates.
The Create action in this example corresponds to a vertex create mode start button and so it provides an isChecked predicate to tell what state the action is in, in this case checked => in create mode, not checked => not in create mode. The implementation interface contains a data element 'creating' to help keep track of this state. Note that it is intialized in a method called Init(). Since the varargs constructor is the normal way the interface instance is created, it guarantees to call this (virtual) init method for you; alternatively, you could make it a static data member and statically initialize it.
3. The interface definition (in the .cpp implementation file)
static EMActionsImp emai (
EM_ACT_INTERFACE, _T("Actions"), IDS_EMAI, &edmeshCD, FP_ACTIONS,
ema_create, _T("create"), IDS_CREATE, 0,
f_predicates, ema_create_enabled, ema_create_checked,
FP_NO_FUNCTION,
f_category, _T("creation"), 0,
f_iconRes, IDI_CREATE_ICON,
f_toolTip, IDS_VERTEX_CREATE_BY_CLICKIN,
f_ui, em_params, 0, TYPE_CHECKBUTTON, IDC_CREATE,
GREEN_WASH,
end,
ema_delete, _T("delete"), IDS_DELETE, 0,
f_isEnabled, ema_delete_enabled,
f_ui, em_params, 0, BUTTON, IDC_DELETE,
end,
end
);
The constructor for an Action Interface follows a slightly different syntax to a normal function interface. The header parameters are the same (Interface_ID, internal name, descriptor, CD, flags), the FP_ACTIONS flag must be specified to mark this as an action interface.
Each action function is specified by header parameters giving FunctionID, internal name, descriptor resID and flag bits. This is followed by one or more tagged action function options, in the same manner as parameter options in a ParamBlockDesc2 constructor. Each option defines a separate item of metadata for the function In the case of ema_create, the following are specified:
-
· 2 of the 3 possible predicates via their FunctionIDs,
-
· a category string, used by the UI customize dialogs to arrange actions into category groups
-
· a resID for an Icon resource that will be made available for use in toolbar buttons and other UI locations
-
· a toolTip string resID
-
· a UI specification that allows the parammap2 system to automatically connect a button to this action. This is described in more detail in the next section.
The possible options include:
f_category category name, as internal TCHAR* and localizable string resID,
defaults to interface name
f_predicates supply 3 functionIDs for isEnabled, isChecked, isVisible
predicates
f_isEnabled isEnabled predicate functionID
f_isChecked isChecked predicate functionID
f_isVisible isVisible predicate functionID
f_iconRes icon as resource ID
f_icon icon as UI .bmp filename, index pair, as per CUI icon specifications
f_buttonText button text string resID, defaults to function description
f_toolTip tooltip string resID, defaults to function description
f_menuText menu item text string resID, defaults to buttonText or function
description
f_ui UI spec if paramMap2-implemented UI (pmap blockID, mapID, control
type, button or checkbutton resID, hilight col if chkbtn)
f_shortcut default keyboard shortcuts for action functions
f_keyArgDefault marks a parameter as optional and supplies a default value. Optional parameters must come after the positional parameters. Example:
Meshop::buildMapFaces, _T("buildMapFaces"), 0, TYPE_VOID, 0, 3,
_T("source"), 0, TYPE_FPVALUE_BR,
_T("keep"), 0, TYPE_BOOL, f_keyArgDefault, FALSE,
_T("channel"), 0, TYPE_INT, f_keyArgDefault, 0,
f_inOut specifies whether _BR parameter is just for input, just for output or both via FPP_OUT_PARAM, FPP_IN_PARAM, or FPP_IN_OUT_PARAM argument. Default is FP_IN_OUT_PARAM.
f_macroEmitter Supply a pointer to FPMacroEmitter subclass instance to customize macro-recorder emission. The Emit() method is called on this instance with interface and function def pointers for the particular action function to be emitted.
The ActionTable system 'table ID' DWORD for Action interfaces is generated automatically from the Interface_ID for the Action interface (by xor-ing the two DWORDS in the interface ID). If this generates an ID that clashes with built-in table IDS (in the range 0-64), an error message is generated and you should choose a more random Interface_ID.
You define the UI context for the interface as a whole, rather than for each individual action function. The context ID should be specified immediately after the flag word in an Action interface descriptor constructor. For example:
static IKChainActionsImp sIKChainActionImp(
IKCHAIN_FP_INTERFACE_ID, _T("Action"), IDS_IKCHAIN_ACT,
&theIKChainControlDesc, FP_ACTIONS, kActionMainUIContext,
IKChainActionsImp::kSnap, _T("snap"), IDS_IKCHAIN_SNAP, 0,
...
The constant 'kActionMainUIContext' is one of the built-in ActionTable system contexts that you can use (defined in maxsdk\include\ActionTable.h), and is most likely the one to be used. If you create an entirely new context and context ID, you will need to register and control its activation yourself (see the IActionManager in maxsdk\include\ActioNTable.h for details).
The automatically-generated ActionTable for the Action interface can be accessed via a new FPInterface method: virtual ActionTable* GetActionTable();
By default, the action table is activated immediately and stays active throughout the 3ds max session. You can dynamically control this activation via a new FPInterface method: virtual void EnableActions(BOOL onOff);
This enables or disables the entire set of actions in the interface. You might do this if the actions are only to be active during certain times in the running of MAX. Usually, this control is achieved via ActionTable contexts.
ParamMap2 Buttons
The f_ui option for an Action Function can be used to make the ParamMap2 system connect the action function to a button in a ParamMap2-mediated rollup in your plugin's UI. This means you have to be using the ParamBlock2 system in the interface's plugin (ie, same ClassDesc2) and have at least one paramblock containing a paramMap whose rollup contains the button dialog item.
In the f_ui option, you first specify the BlockID of the paramblock containing this paramMap and then MapID of the map within that block (single map blocks use MapID 0). This is followed by a control type code, and its required parameters, as listed below:
TYPE_BUTTON, <dlg_item_ID>
a standard push button. The dialog item must be a 3ds max CustButton custom control.
TYPE_CHECKBUTTON, <dlg_item_ID>, <highlight_color>
a standard check button. Again, the dialog item must be a 3ds max CustButton custom control, the 2nd parameters is the button highlight color as a COLORREF word, either one of the predefined colors in maxsdk\include\custcont.h, or an RGB value using the system RGB() macro.
This is basically all you need to do. Whenever the specified map opens its rollup, say as part of a BeginEditParams, it will automatically look for any Action functions associated via an f_ui and will call that action function whenever the button is clicked. If an isEnabled predicate is supplied, the ParamMap2 will call it at various display update times to determine whether to enable or disable (gray out) the button. For CheckButtons, an isChecked predicate must be supplied and will be called at similar times to update the pressed or not-pressed state of the button display.
The FPInterface Class Hierarchy
The FPInterface class hierarchy is such that static and mixin interfaces and interface descriptors each have their own types.
FPInterface The base class for all interfaces, prime client type for using interfaces
FPInterfaceDesc Contains the interface metadata
FPStaticInterface Use as the base class for defining static or core virtual interface classes.
FPMixinInterface Use as the base class for defining object-based mixin interface
classes, use FPInterfaceDesc for mixin interface descriptors
The FPInterface class continues to provide all the original method and predicate invocation functions required by an SDK-level client of the interface. A pure virtual method, GetDesc(), should be used to get the FPInterfaceDesc instance for the interface. The metadata definition and accessing methods and the metadata data members now all live in the FPInterfaceDesc.
All the original interface query methods in the API (such as Animatable::GetInterface(id), ClassDesc2::GetInterface(id), GetCOREInterface(id)) still return FPInterface pointers, so you would use GetDesc() in constructs like the following to get at an interface's metadata:
FPInterface* fpi = GetCOREInterface(FOO_INTERFACE);
TCHAR* iname = fpi->GetDesc()->internal_name.data();
The FPInterface::GetDesc() method has a default implementation in FPInterfaceDesc (and so FPStaticInterface), simply returning 'this', since instances of these classes contain the metadata). FPMixinInterface subclasses provide implementations of GetDesc(), returning their associated FPInterfaceDesc instance.
Parameter Validation
An interface descriptor can now contain validation information for individual parameters, so that clients such as MAXScript can validate values given as parameters to FPInterface calls, prior to making the call. The validation info can be in the form of a range of values for int and float types, or more generally, a validator object that is called the validate a parameter value.
MAXScript now applies the validations if supplied and generates descriptive runtime errors if the validation fails.
The validation info is specified in the FPInterface descriptor in optional tagged entries following the parameters to be validated. The two possible tags are f_range and f_validator. Here's an example from a possible mixin interface to Cylinder:
static FPInterfaceDesc cylfpi (
CYL_INTERFACE, _T("cylMixin"), 0, &cylinderDesc, FP_MIXIN,
...
cyl_setRadius, _T("setRadius"), 0, TYPE_VOID, 0, 1,
_T("radius"), 0, TYPE_FLOAT, f_range, 0.0, 10000.0,
cyl_setDirection, _T("setDirection"), 0, TYPE_VOID, 0, 1,
_T("vector"), 0, TYPE_POINT3, f_validator, &cylValidator,
...
end
);
In this above example, the "radius" parameter is defined to have a range 0.0 to 10000.0. An f_range spec can only be used for int (TYPE_INT, TYPE_TIMEVALUE, TYPE_RADIOBTN_INDEX, TYPE_INDEX, TYPE_ENUM) and float parameters (TYPE_FLOAT, TYPE_ANGLE, TYPE_PCNT_FRAC, TYPE_WORLD, TYPE_COLOR_CHANNEL) and is given as a pair of low and high range values. The values must be floating point or integer as needed by the TYPE_xxx code, you cannot specify integer range values for float types and vice versa, hence the 0.0 and 10000.0 in the example above. MAXScript checks parameter values against these supplied ranges and will generate a descriptive error message for out-of-range values.
The "vector" parameter in the above example has a validator object specified. This must be a pointer to an instance of a class derived from the new class, FPValidator, defined in iFnPub.h. This is a virtual base class, containing a single method, Validate(), that is called to validate a prospective value for a parameter. You would typically subclass FPValidator in your code and provide an implementation of Validate() to do the validation. Here is the FPValidator virtual base class:
class FPValidator
{
public:
// validate val for the given param in function in interface
virtual bool Validate(FPInterface* fpi, FunctionID fid,
int paramNum, FPValue& val, TSTR& msg)=0;
};
The Validate() function is called with interface, function-within-interface and parameter-within-function identifiers and an FPValue to validate. It can optionally install an error message string in the 'msg' TSTR& parameter for the user of the validator to display. If there are many parameters to validate this way, you can choose to provide a separate subclass for each parameter or a single subclass and switch on the parameter identification supplied.
For the Cylinder example above, the "vector" parameter might be required to be given as a unit vector, so the validator might check for this as follows:
class CylValidator : public FPValidator
{
bool Validate(FPInterface* fpi, FunctionID fid, int paramNum,
FPValue& val, TSTR& msg)
{
if (fabs(Length(*val.p) - 1.0) > 1e-6)
{
msg = "Direction vector must be unit length."
return false;
}
else
return true;
}
}
Note that the type is already checked by the caller, since it has type info for the parameter, so you don't also need to check the FPValue type.
Note, also, that the FPValue& val argument given to Validate() is a reference to the actual parameter value to be given to the called function, so it is also possible to massage the value rather than reporting an error. For example, the above sample Validate() method could normalize the vector and always return true. The type must not be changed in this process, only the value can be adjusted.
A singleton instance of this FPValidator subclass would created, typically as a static instance, to be given in the f_validator specification: static CylValidator cylValidator;
If you need to get at the validation metadata, it is available in the new 'options' data member in an FPParamDef instance for a parameter. If non-NULL, this points to a FPParamOptions instance which contains the range or validator information for the parameter. See class FPParamOptions in iFnPub.h for details.
Exception Handling
FnPub interface functions can now report fatal error conditions to callers by using C++ exception-handling. There is a new exception base class, MAXException, defined in iFnPub.h that can be thrown directly, or subclassed as needed for error grouping. The class is defined as follows:
class MAXException
{
public:
TSTR message;
int error_code;
MAXException(TCHAR* msg, int code=0) : message(msg),
error_code(code) { }
};
It contains a message buffer and an optional error code. You would signal an error using the MAXException() constructor and the C++ throw statement, as in the following example:
...
if (discrim < 0.0) // oh-oh, not good
throw MAXException ("Unable to find root.", -23);
...
This signals a fatal error with the message and code shown. If the error occurs during a call to the function by MAXScript code, it will be trapped by MAXScript and the error message will be displayed and the running script will be terminated (but 3ds max will continue running). If the error occurs during a C++-level call, typically the outer 3ds max error catcher will catch and report the error and then exit MAX, or clients of the interface can install their own catch code.
Property accessors
It is possible to define selected methods in an FPInterface as 'property accessor' methods so that dispatch-based clients of the interface may present these methods as properties instead of functions. MAXScript, for example, now presents property accessors defined in this way as simple dot-notation properties of the interface. These accessors remain as ordinary methods in the
C++-level interface for direct C++ clients but are marked as accessors in the descriptor and function maps. A 'property' is typically defined by a pair of accessor methods, one for getting and one for setting, but you can also define read-only methods that have a single getter method. As an example, here's a rework of the above Cylinder interface in which the radius and direction are made properties.
class CylInterface : public FPMixinInterface
{
...
virtual void RemoveCaps()=0;
virtual void AddBend(float offset, float angle, float radius)=0;
...
virtual float GetRadius()=0;
virtual void SetRadius(float radius)=0;
virtual Point3 GetDirection()=0;
virtual void SetDirection(Point3 dir)=0;
...
BEGIN_FUNCTION_MAP
...
VFN_0(cyl_removeCaps, RemoveCaps);
VFN_3(cyl_addBend, AddBend, TYPE_FLOAT, TYPE_ANGLE,
TYPE_FLOAT);
...
PROP_FNS(cyl_getRadius, GetRadius, cyl_setRadius, SetRadius,
TYPE_FLOAT);
PROP_FNS(cyl_getDir, GetDirection, cy_setDir, SetDirection,
TYPE_POINT3_BV);
...
END_FUNCTION_MAP
FPInterfaceDesc* GetDesc();
};
Each property as a getter and setter virtual method. Their signatures must conform to the convention shown, namely, the getter takes no parameters and the getter one. The getter returns the same type as the setter and the setter type is void. There are also variants that take an explicit time as described below.
Properties have single entries in the function map, using one of the property-related entry macros. In this case, PROP_FNS() takes a getter ID, getter function, setter ID, setter function and finally the property type code. There is also a RO_PROP_FN() macro for read-only properties that takes a getter ID, getter FN and prop type. Finally, the are variants of PROP_FNS and RO_PROP_FN that indicate the accessors take an explicit TimeValue argument, presumably associated with animatable properties and indicating that the property access should be at the given time. These macros are PROP_TFNS and RO_PROP_TFN and takes the same arguments as their corresponding base macros. If the above example properties were time-sensitive, the definitions would be as follows:
class CylInterface : public FPMixinInterface
{
...
virtual void RemoveCaps()=0;
virtual void AddBend(float offset, float angle, float radius)=0;
...
virtual float GetRadius(TimeValue t)=0;
virtual void SetRadius(float radius, TimeValue t)=0;
virtual Point3 GetDirection(TimeValue t)=0;
virtual void SetDirection(Point3 dir, TimeValue t)=0;
...
BEGIN_FUNCTION_MAP
...
VFN_0(cyl_removeCaps, RemoveCaps);
VFN_3(cyl_addBend, AddBend, TYPE_FLOAT, TYPE_ANGLE,
TYPE_FLOAT);
...
PROP_TFNS(cyl_getRadius, GetRadius, cyl_setRadius, SetRadius,
TYPE_FLOAT);
PROP_TFNS(cyl_getDir, GetDirection, cy_setDir, SetDirection,
TYPE_POINT3_BV);
...
END_FUNCTION_MAP
FPInterfaceDesc* GetDesc();
};
Recall that there are time-sensitive variants for the FN_0, FN_1, etc., macros, namely TFN_0, TFN_1, etc. Functions specified by such macros in the function map have an implicit TimeValue last parameter. Note that in all the time-sensitive variants, the last TimeValue parameter is not defined as an explicit parameter in the FPInterface descriptor entry for that function. Such time-sensitive properties and functions are handled specially in MAXScript. It supplies the current MAXScript time, as defined by the current 'at time' context for this implicit parameter, making these functions and properties behave consistently with the rest of MAXScript.
Properties are defined in a special section in the FPInterface descriptor constructor following the function definitions, headed by the special tag 'properties'. They are also entered in the function map using new property-specific FUNCTION_MAP macros. Here's an example descriptor fragment:
static FPInterfaceDesc cylfpi (
CYL_INTERFACE, _T("cylMixin"), 0, &cylinderDesc, FP_MIXIN,
...
cyl_removeCaps, _T("removeCaps"), 0, TYPE_VOID, 0, 0,
cyl_addBend, _T("addBend"), 0, TYPE_VOID, 0, 3,
_T("offset"), 0, TYPE_FLOAT,
_T("angle"), 0, TYPE_ANGLE,
_T("radius"), 0, TYPE_FLOAT,
properties,
cyl_getRadius, cyl_setRadius, _T("radius"), IDS_RADIUS,
TYPE_FLOAT, f_range, 0.0, 10000.0,
cyl_getDirection, cyl_setDirection, _T("direction"), IDS_DIR,
TYPE_POINT3_BV,
...
end
);
The 'properties' section follows the function definitions. Each propery has a single entry defining the function IDs for the getter and setter functions, a fixed internal property name, a descriptor string resource ID and the property type. If the property is read-only and there is no setter function, specify FP_NO_FUNCTION for the setter ID. Each property definition can optionally be followed by parameter validation options, as described in the Parameter Validation section above, and these apply to the parameter given to the setter function during property assignment. In this case, the radius is range checked.
If you need to get at it, the property metadata is accessible in the 'props' data member in an FPInterfaceDesc. It is a Tab<> of pointers to FPPropDef class instances, which contain individual property metadata. See the FPPropDef class definition in iFnPub.h for details.
MAXScript now exposes properties defined in this way as direct dot-notation properties on the interface. So, were before the Get/SetRadius in the example cylinder mixin would have been accessed as functions:
r = $cyl01.cylMixin.getRadius()
$cyl01.cylMixin.setRadius 23
you now can use:
r = $cyl01.cylMixin.radius
$cyl01.cylMixin.radius = 23
This is true for all the types of FP interfaces that turn up in MAXScript, static, mixin and core. As a futher optimization, MAXScript now effectively promotes all interface methods and properties to the level of the interface, so if individual methods and properties have unqiue names within all the interfaces of an object or
class, you can elide the interface name. The above examples could now be written:
r = $cyl01.getRadius()
$cyl01. setRadius 23
and:
r = $cyl01.radius
$cyl01. radius = 23
If there is a naming conflict, you can always include the interface name level to resolve this.
Symbolic Enums
One or more symbolic enums, similar to C++ enums, can now be added to an FPInterface's metadata, and individual int parameters and/or results for functions in that interface can be defined as TYPE_ENUM and associated with one of the enum lists. This allows metadata clients to support symbolic encodings for these parameters and results, which MAXScript now does.
Enums are defined in the FPInterface descriptor following the function and property definitions as sets of string/code pairs. Each enum list is identified by a unique integer, similar to function IDs, which is used to associated a TYPE_ENUM parameter or result with its enum. IDs for these would normally be defined somewhere near the function IDs for an interface. For example:
// function IDs
enum { bmm_getWidth, bmm_getHeight, bmm_getType, bmm_copyImage, ...};
// enum IDs
enum { bmm_type, bmm_copy_quality, ...};
might be some of the IDs for a possible bitmap manager interface. The two enums provide symbolic codes for the bitmap type and copyImage quality defines in the "bitmap.h" SDK header, such as BMM_PALETTED, BMM_TRUE_32, COPY_IMAGE_RESIZE_LO_QUALITY, etc. In the descriptor for the interface, any enum lists follow the function and property definitions. They are introduced by the special tag, 'enums', as in the following example:
static FPInterfaceDesc bmmfpi (
BMM_INTERFACE, _T("bmm"), IDS_BMMI, NULL, FP_CORE,
...
bmm_copyImage, _T("copyImage"), ...
_T("copyType"), IDS_COPYTYPE, TYPE_ENUM, bmm_copy_quality,
...
properties,
geo_getType, geo_setType, _T("type"), 0, TYPE_ENUM, bmm_type,
enums,
bmm_type, 7,
"lineArt", BMM_LINE_ART,
"paletted", BMM_PALETTED,
"gray8", BMM_GRAY_8,
"gray16", BMM_GRAY_16 ,
"true16", BMM_TRUE_16,
"true32", BMM_TRUE_32,
"true24", BMM_TRUE_64,
bmm_copy_quality, 4,
"crop", COPY_IMAGE_CROP,
"resizeLo", COPY_IMAGE_RESIZE_LO_QUALITY,
"resizeHi", COPY_IMAGE_RESIZE_HI_QUALITY,
"useCustom", COPY_IMAGE_USE_CUSTOM,
end
);
In the above example, the enums are listed following the function & property definitions. They are introduced by the 'enums' tag and consist of an enum ID followed by a count of items, followed by that many string and code pairs. By attaching them to the interface like this, any number of functions and properties in the interface can use them.
The above example also has function and property definitions showing the use of TYPE_ENUM. The copyImage function takes a copyType parameter which uses the bmm_copy_quality enum and the type property uses the bmm_type enum. In all situations where TYPE_xxx types can be supplied in a descriptor, including the new property definitions, TYPE_ENUM can be used to indicate an int by-value type. TYPE_ENUM's must always be followed by an enum ID. This is the only case in which the type is specified as a pair of values. TYPE_ENUM parameters and results show up in MAXScript as # names. For example, if a bmm interface was in the variable 'bm1' and the bitmap type was BMM_GRAY_16,
bm1.type
-> #gray16
bm1.type = #true32 -- set it to #true24 (code is BMM_TRUE_24)
bm2 = bm1.copyImage #resizeHi
the integer TYPE_ENUM codes are translated back-and-forth to symbolic # names by MAXScript using the definitions in the FPInterface descriptor's enums. If you need to access the enum metadata in an FPInterfaceDesc, it is available in the 'enumerations' data member. This is a Tab<> of pointers to FPEnum class instances which themselves contain a Tab<> of name, code pairs. See class FPEnum in iFnPub.h for details.
Additional ParamType2 codes for Function Publishing and MAXScript
TYPE_FPVALUE // FPValue*, variant value
TYPE_VALUE // MAXScript Value*
TYPE_FPVALUE can be used to pass a variant data type, containing one of the types of data that FPValue can hold. This allows a function to be defined that can accept many types of values for a TYPE_FPVALUE parameter, the function can determine the type from the 'type' field in the FPValue passed. MAXScript supports TYPE_PFVALUE parameters and return values and will convert back-and-forth between the MAXScript types that correspond to the various FPValue types. TYPE_FPVALUE also supports TYPE_FPVAUE_BV and TYPE_FPVALUE_BR variants. MAXScript arrays will attempt to convert themselves into TYPE_XXX_TAB FPValues if the array elements are all of the same type, for example all integers generate a TYPE_INT_TAB, all scene nodes generate a TYPE_INODE_TAB, etc. If the array contains mixed value types or types not in the following list, a TYPE_FPVALUE_TAB will be generated, containing an FPValue* variant value for each element in the array. Supported homogeneous types are:
TYPE_INT_TAB
TYPE_FLOAT_TAB
TYPE_TIMEVALUE_TAB
TYPE_STRING_TAB
TYPE_NAME_TAB
TYPE_POINT3_TAB
TYPE_POINT2_TAB
TYPE_INODE_TAB
TYPE_REFTARG_TAB (all other ReferenceTarget*s, modifiers, mtls, ctlrs, etc.)
TYPE_VALUE is basically a fallback to be used in situations where MAXScript values of types not covered by FPValue types need to be passed into a published function. The called function is responsible for using the MAXScript SDK to convert to and from the Value* in TYPE_VALUE values.
Parameter/Result Types
More parameter/result types have been added and the type system has been generalized to allow you to specify pass-by-reference, pass-by-pointer and pass-by-value variations of the appropriate base types. This permits a wider range of interface method signatures in FnPub interfaces, particularly those with '&' reference types typically used for passing back values via parameters. The scripter has been upgraded to support all these new types and pass-by options (see also the MAXScript section here for details).
The type codes, defined in the ParamType2 enum, are arranged in sections with various suffixes signifying the sections. The main section gives the so-called 'base types', such as TYPE_INT, TYPE_FLOAT, and the other sections are variations derived from the base types. The variations are:
<base_type>_BV base type passed by value
<base_type>_BR base type passed by reference, used with & ref params & results
<base_type>_BP base type passed by pointer, used with * pointer params& results
<base_type>_TAB a Tab<> of the base type
<base_type>_TAB_BV a Tab<> of base type, the Tab<> is passed by value
<base_type>_TAB_BR a Tab<> of base type, the Tab<> is passed by reference
Since some base types are naturally passed by value or pointer, and the base Tab<> type is passed by pointer, so not all possible combinations are actually made available. For example, there is no TYPE_INT_BV, since TYPE_INT is already passed by value, and there is no TYPE_STRING_BP since TYPE_STRING is already passed by pointer.
Added base types
TYPE_POINT a Win32 POINT struct
TYPE_TSTR a 3ds max SDK TSTR class
TYPE_IOBJECT a new FnPub system IObject
TYPE_INTERFACE an FPInterface
TYPE_HWND a Win32 HWND handle
TYPE_NAME a variant of TYPE_STRING, meant to be interpreted as an interned symbol or
name in the client (eg, MAXScript represents these Name instances,
as in #foo, etc.)
All the new base types are passed naturally as pointers, so there are _BV and _BR variants for all of them (except for TYPE_HWND). There are also _TAB, _TAB_BR, _TAB_BV variants for them all. For reference, all the currently available types are in the ParamType2 enumeration in /include/iparmb2.h. Some base types are naturally passed by value, others by pointer. Basically, all the int and float-derived types are passed naturally by-value and all the rest by pointer. The by-value base types are:
ints:
TYPE_INT
TYPE_BOOL
TYPE_TIMEVALUE,
TYPE_RADIOBTN_INDEX
TYPE_INDEX
floats:
TYPE_FLOAT
TYPE_ANGLE
TYPE_PCNT_FRAC
TYPE_WORLD
TYPE_COLOR_CHANNEL
The FPValue class has a union containing fields for all the base and table types and show the natural passing mode for each. Note that the _BP by-pointer variants of the base types are passed as iptr & fptr pointer fields in the PValue union. For the types that are passed naturally by pointer, the _BV variants cause local copies to be made and owned by the FPValue carrying them. The FPValue destructor will free memory taken by these copies. Also, the _BR variants of naturally-pointer types need to supplied as pointers to the FPValue (or FPParams) constructors and are dereferenced at parameter delivery time. So, for example, the following FPInterface method:
Tab<int> Foo(INode* object, Tab<Point3*>& points);
would be declared in the FUNCTION_MAP as
FN_2(my_foo, TYPE_INT_TAB_BV, Foo, TYPE_INODE, TYPE_POINT3_TAB_BR);
and in the descriptor constructor as
my_foo, _T("foo"), 0, TYPE_INT_TAB_BV, 0, 2,
_T("object"), 0, TYPE_INODE,
_T("points"), 0, TYPE_POINT3_TAB_BR,
Supported Types
The type codes you can use to specify function return types or argment types are defined by the ParamType2 enumeration in \include\paramtype.h. Several of the type codes map to the same underlying C++ type, for example there are five float types. In most cases, the alternate codes imply different UI scaling Dimensions that are honored by systems like MAXScript. For example, using TYPE_ANGLE will cause MAXScript to convert back and forth between radians internally and degrees to the user, so you should use the most specific type code.
As you can see from the list below, all the values are pointer-sized or smaller, so they fit in a single pointer-sized union in an FPValue. This means any object larger than a pointer is normally passed by reference. To support passing and returning by-value, a variant set of types is provided that take local copies of the object; they have an _BV suffix in the type code name. This local copy is freed in the destructor of FPValue, so FPValues of these types effectively have the semantics of pass-by-value. Typically, you would use these by-value types for returning values that have been created locally in the called function, Point3s, strings, etc. All Tab<> types take a local copy of the table. All the reftarg types (Mtl*, INode*, Texmap*) are passed only as pointers, no attempt is made to keep local references in the FPValue or FPParam instances.
Built in data types (enum ParamType)
TYPE_USER
Built in data types (enum ParamType2)
TYPE_INDEX
TYPE_MATRIX3
TYPE_PBLOCK2
The following are only for published function parameter types, not pblock2 parameter types.
TYPE_ENUM
TYPE_VOID
TYPE_INTERVAL
TYPE_ANGAXIS
TYPE_QUAT
TYPE_RAY
TYPE_POINT2
TYPE_BITARRAY
TYPE_CLASS
TYPE_MESH
TYPE_OBJECT
TYPE_CONTROL
TYPE_POINT
TYPE_TSTR
TYPE_IOBJECT
TYPE_INTERFACE
TYPE_HWND
TYPE_NAME
TYPE_COLOR
TYPE_FPVALUE
TYPE_VALUE
TYPE_DWORD
TYPE_bool
The following are tables of the above data types (in the same order as base types).
TYPE_INDEX_TAB
TYPE_MATRIX3_TAB
TYPE_PBLOCK2_TAB
The following are only for published function parameter types, not pblock2 parameter types.
TYPE_ENUM_TAB, TYPE_VOID_TAB, TYPE_INTERVAL_TAB, TYPE_ANGAXIS_TAB, TYPE_QUAT_TAB, TYPE_RAY_TAB, TYPE_POINT2_TAB, TYPE_BITARRAY_TAB, TYPE_CLASS_TAB, TYPE_MESH_TAB, TYPE_OBJECT_TAB, TYPE_CONTROL_TAB, TYPE_POINT_TAB, TYPE_TSTR_TAB, TYPE_IOBJECT_TAB, TYPE_INTERFACE_TAB, TYPE_HWND_TAB, TYPE_NAME_TAB, TYPE_COLOR_TAB, TYPE_FPVALUE_TAB, TYPE_VALUE_TAB, TYPE_DWORD_TAB, TYPE_bool_TAB
The following pass by-ref types, implies & parameters, int& & float& are passed via .ptr fields, only for FnPub use. These are defined as TYPE_xxx + TYPE_BY_REF.
TYPE_FLOAT_BR, TYPE_INT_BR, TYPE_BOOL_BR, TYPE_ANGLE_BR, TYPE_PCNT_FRAC_BR, TYPE_WORLD_BR, TYPE_COLOR_CHANNEL_BR, TYPE_TIMEVALUE_BR, TYPE_RADIOBTN_INDEX_BR, TYPE_INDEX_BR, TYPE_RGBA_BR, TYPE_BITMAP_BR, TYPE_POINT3_BR, TYPE_HSV_BR, TYPE_REFTARG_BR, TYPE_MATRIX3_BR, TYPE_ENUM_BR, TYPE_INTERVAL_BR, TYPE_ANGAXIS_BR, TYPE_QUAT_BR, TYPE_RAY_BR, TYPE_POINT2_BR, TYPE_BITARRAY_BR, TYPE_MESH_BR, TYPE_POINT_BR, TYPE_TSTR_BR, TYPE_COLOR_BR, TYPE_FPVALUE_BR, TYPE_DWORD_BR, TYPE_bool_BR
The following pass by-ref Tab<> types, implies & parameters, int& & float& are passed via .ptr fields, only for FnPub use. These are defined as TYPE_xxx + TYPE_TAB + TYPE_BY_REF.
TYPE_FLOAT_TAB_BR, TYPE_INT_TAB_BR, TYPE_RGBA_TAB_BR, TYPE_POINT3_TAB_BR, TYPE_BOOL_TAB_BR, TYPE_ANGLE_TAB_BR, TYPE_PCNT_FRAC_TAB_BR, TYPE_WORLD_TAB_BR, TYPE_STRING_TAB_BR, TYPE_FILENAME_TAB_BR, TYPE_HSV_TAB_BR, TYPE_COLOR_CHANNEL_TAB_BR, TYPE_TIMEVALUE_TAB_BR, TYPE_RADIOBTN_INDEX_TAB_BR, TYPE_MTL_TAB_BR, TYPE_TEXMAP_TAB_BR, TYPE_BITMAP_TAB_BR, TYPE_INODE_TAB_BR, TYPE_REFTARG_TAB_BR, TYPE_INDEX_TAB_BR, TYPE_MATRIX3_TAB_BR, TYPE_TSTR_TAB_BR, TYPE_ENUM_TAB_BR, TYPE_INTERVAL_TAB_BR, TYPE_ANGAXIS_TAB_BR, TYPE_QUAT_TAB_BR, TYPE_RAY_TAB_BR, TYPE_POINT2_TAB_BR, TYPE_BITARRAY_TAB_BR, TYPE_CLASS_TAB_BR, TYPE_MESH_TAB_BR, TYPE_OBJECT_TAB_BR, TYPE_CONTROL_TAB_BR, TYPE_POINT_TAB_BR, TYPE_IOBJECT_TAB_BR, TYPE_INTERFACE_TAB_BR, TYPE_HWND_TAB_BR, TYPE_NAME_TAB_BR, TYPE_COLOR_TAB_BR, TYPE_FPVALUE_TAB_BR, TYPE_VALUE_TAB_BR, TYPE_DWORD_TAB_BR, TYPE_bool_TAB_BR
The following pass by-value types, implies dereferencing the (meaningful) pointer-based values, only for FnPub use. These are defined as TYPE_xxx + TYPE_BY_VAL.
TYPE_RGBA_BV, TYPE_POINT3_BV, TYPE_HSV_BV, TYPE_INTERVAL_BV, TYPE_BITMAP_BV, TYPE_MATRIX3_BV, TYPE_ANGAXIS_BV, TYPE_QUAT_BV, TYPE_RAY_BV, TYPE_POINT2_BV, TYPE_BITARRAY_BV, TYPE_MESH_BV, TYPE_POINT_BV, TYPE_TSTR_BV, TYPE_COLOR_BV, TYPE_FPVALUE_BV, TYPE_CLASS_BV
The following pass by-value Tab<> types, implies dereferencing the (meaningful) pointer-based values, only for FnPub use. These are defined as TYPE_xxx + TYPE_TAB + TYPE_BY+VAL.
TYPE_FLOAT_TAB_BV, TYPE_INT_TAB_BV, TYPE_RGBA_TAB_BV, TYPE_POINT3_TAB_BV, TYPE_BOOL_TAB_BV, TYPE_ANGLE_TAB_BV, TYPE_PCNT_FRAC_TAB_BV, TYPE_WORLD_TAB_BV, TYPE_STRING_TAB_BV, TYPE_FILENAME_TAB_BV, TYPE_HSV_TAB_BV, TYPE_COLOR_CHANNEL_TAB_BV, TYPE_TIMEVALUE_TAB_BV, TYPE_RADIOBTN_INDEX_TAB_BV, TYPE_MTL_TAB_BV, TYPE_TEXMAP_TAB_BV, TYPE_BITMAP_TAB_BV, TYPE_INODE_TAB_BV, TYPE_REFTARG_TAB_BV, TYPE_INDEX_TAB_BV, TYPE_MATRIX3_TAB_BV, TYPE_PBLOCK2_TAB_BV, TYPE_VOID_TAB_BV, TYPE_TSTR_TAB_BV, TYPE_ENUM_TAB_BV, TYPE_INTERVAL_TAB_BV, TYPE_ANGAXIS_TAB_BV, TYPE_QUAT_TAB_BV, TYPE_RAY_TAB_BV, TYPE_POINT2_TAB_BV, TYPE_BITARRAY_TAB_BV, TYPE_CLASS_TAB_BV, TYPE_MESH_TAB_BV, TYPE_OBJECT_TAB_BV, TYPE_CONTROL_TAB_BV, TYPE_POINT_TAB_BV, TYPE_IOBJECT_TAB_BV, TYPE_INTERFACE_TAB_BV, TYPE_HWND_TAB_BV, TYPE_NAME_TAB_BV, TYPE_COLOR_TAB_BV, TYPE_FPVALUE_TAB_BV, TYPE_VALUE_TAB_BV, TYPE_DWORD_TAB_BV, TYPE_bool_TAB_BV
The following pass by-pointer types for int & float types, implies * parameters, int* & float* are passed via .ptr fields, only for FnPub use. These are defined as TYPE_xxx + TYPE_BY_PTR.
TYPE_FLOAT_BP, TYPE_INT_BP, TYPE_BOOL_BP, TYPE_ANGLE_BP, TYPE_PCNT_FRAC_BP, TYPE_WORLD_BP, TYPE_COLOR_CHANNEL_BP, TYPE_TIMEVALUE_BP, TYPE_RADIOBTN_INDEX_BP, TYPE_INDEX_BP, TYPE_ENUM_BP, TYPE_DWORD_BP, TYPE_bool_BP
There are no specific by-pointer Tab<> types, all Tab<> types are by-pointer by default.
TYPE_MAX_TYPE
Published Functions and MAXScript
MAXScript automatically provides access to all functions published by a plugin via the FnPub system. The current scheme is experimental and may change somewhat. Each plugin class appears in MAXScript as a 3ds max class object, that can be used to construct instances of the plugin, do class tests, etc. If a plugin publishes interfaces, they are visible in MAXScript as properties on this class object. The internal name for the interface is used as the property name. All the functions in the interface are accessible as named properties on the interface. So, if the above example interfaces were published by EditMesh, the following script frags would work:
EditMesh.faceOps.extrude $foo.mesh #{1,2,3} 10
calls the Extrude function in the FaceOps interface on $foo's mesh, faces 1, 2 and 3, amount 10 units.
EditMesh.actions
retrieves and displays the action functions. Each interface is stored as a struct definition in the class object.
EditMesh.actions.create ()
starts (or stops) the create mode. This would (should!) have the side-effect of highlighting/unhighlighting the Create button in the EditMesh rollups. Calls to Action functions in MAXScript return true if the function returns FPS_OK and false otherwise.
if EditMesh.actions.create.isChecked() then ...
The predicate functions for an Action Function are available as properties on the action function object itself, as shown. You can determine if a predicate is supplied by asking:
if EditMesh.actions.create.isChecked != undefined
Extending Animatable::GetInterface()
The following have been added to class Animatable:
virtual FPInterface* GetInterface(Interface_ID id);
Any future object-based interfaces should be allocated unique Interface_IDs (you can use Gencid.exe for this) and made available through this call.
The default implementation of GetInterface(Interface_ID) looks up a standalone interface of the given ID on the object's ClassDesc. This gives access to standalone FnPub interfaces via any of a plugin's objects, without having to dig around for the ClassDesc, so you should fall back to calling the default implementation if you don't recognize an ID in your implementation of GetInterface(Interface_ID).
Global Functions related to Function Publishing
Prototype:
void RegisterCOREInterface(FPInterface* fpi);
Remarks:
This function is available in release 4.0 and later only.
This function registers an interface object. Creating Core interfaces is done in the normal FnPub manner, via public virtual interface headers and implementation interfaces though the FPInterface constructor as above, but by specifying NULL for the ClassDesc pointer. The core interfaces published this way are automatically available in the scripter, with each interface visible as a struct function package of the same name as the internal name of the interface.
Parameters:
FPInterface* fpi
The pointer to the function publishing interface class.
Prototype:
FPInterface* GetCOREInterface(Interface_ID id);
Remarks:
This function is available in release 4.0 and later only.
This function locates a Core interface object by its unique interface ID.
Parameters:
Interface_ID id
The unique interface ID of an interface object.
Return Value:
A pointer to the Core interface object associated with the specified interface ID.
Prototype:
int NumCOREInterfaces();
Remarks:
This function is available in release 4.0 and later only.
This function returns the number of available Core interfaces.
Prototype:
FPInterface* GetCOREInterface(int i);
Remarks:
This function is available in release 4.0 and later only.
This function returns a Core interface object by its index.
Parameters:
int i
The index of the Core interface object you wish to retrieve.
Return Value:
A pointer to the I-th Core interface object.
Prototype:
FPInterface* GetCOREInterfaceAt(int i);
Remarks:
This function is available in release 4.0 and later only.
This function returns a Core interface object by its index.
Parameters:
int i
The index of the Core interface object you wish to retrieve.
Return Value:
A pointer to the I-th Core interface object.
Prototype:
FPInterface *GetInterface(SClass_ID super, Class_ID cls, Interface_ID id);
Remarks:
This function is available in release 4.0 and later only.
This function is a global helper function that finds the ID’d interface for the given plugin class and superclass ID’s and saves client code from having to dig through the ClassDir to find ClassDesc’s.
Parameters:
SClass_ID super
The superclass ID of the plugin.
Class_ID cls
The class ID of the plugin.
Interface_ID id
The unique ID of the interface object to retrieve.
Return Value:
A pointer to the Core interface object associated with the specified class and superclass ID’s.