FAQ

From The Foundry MODO SDK wiki
Revision as of 13:24, 17 July 2014 by Shf (Talk | contribs)

Jump to: navigation, search

Contents

Frequently Asked Questions: SDK

Q: How do I get a service object?

A: Just declare it, and it is ready to go: TabbedArea

The constructor for the C++ wrapper calls lx::GetGlobal() to initialize the interface. There are cases where this can fail, however, so it may be necessary to initialize the service wrapper manually. For example, if you declare your service object as a static.

 	static CLxUser_SceneService	 srv_scene;

In this case the constructor is called as the plug-in code is loaded and before the API has been fully initialized. If you try to use a service that was created too early you will crash with a null 'm_loc' pointer. If that happens you just need to reinitialize the service later with the set() method:

 	srv_scene.set ();

Q: How do I read an item's channel value?

A: It depends on the context. Mostly you use a ChannelRead Interface, unless you are in a modifier of some kind, in which case a different API should be used.

The normal case also has two forms. The first allows you to access channel values stored in the base (edit) action. Given a scene, an item in the scene and the channel index, you can get a channel read object from the scene and use that to access the value of channels in the action. It should be noted that in python, a time needs to be provided even if it's not necessarily used. TabbedArea


There are alternate methods for reading different channel types. Channels can be read given their name rather than index, as well. In python, this is built into the Value user method, which can read float, integer, or string channels without needing to specify the type in advance. TabbedArea


The other form is for reading evaluated channels, like the various matricies which are computed from the transform channels. In that case you provide the time for the evaluation rather than the action name:

TabbedArea


If all you have is an item you can get a scene from that, and if you want to keep the index for faster access you can look it up from the item. TabbedArea

Q: How do I read a string channel?

A: use the ChannelRead Interface String() method!

Q: How do I set the default value of a string channel?

A: After adding the channel with the AddChannel Interface, you can obtain the default value object and call the methods of its Value Interface to set the string. The storage type of the channel has to be set to string.

    CLxUser_AddChannel  ac; // given
    LXtObjectID         obj;
    CLxUser_Value       val;
 
    ac.SetStorage (LXsTYPE_STRING);
    ac.SetDefaultObj (&obj);
    val.take (obj);
    val.SetString ("Default Value");

The somewhat misleadingly named SetDefaultObj() returns an object on which you can call its Value Interface methods to set the default value, in this case a string.

Example item plugin: Setting a string channels default value

Q: How do I write a value to an item's channel?

A: Get a ChannelWrite Object, initialize it and set the channel's value. The channel argument can be the channel index or the channel name: C++ will pick the right method for you. value can be integer, float or string.

	CLxUser_ChannelWrite	 chan;

	chan.from (item);
	chan.Set (item, channel, value);

Q: How do I get a CLxUser_Mesh from a mesh item?

A: There are two meshes you can get. If you want the base mesh -- the mesh that the user edits -- then you need to use the form of GetChannels which specifies the action layer, and use LXs_ACTIONLAYER_EDIT. This allows you to read the mesh channel from the action directly:

TabbedArea


If you want to access the mesh after deformation, then you want to read from the evaluated mesh channel. This is done by specifying the time at which you want to evaluate. You can then read the channel as a MeshFilter Interface which can be evaluated:

TabbedArea


Note that the channel is a Mesh Object in one case and a EvaluationStack Object in the other. You have to know the source of your ChannelRead Object to know which one you will get. Alternately you could query for the different interface types to probe the object as runtime.

The MeshFilter is also what you get from a modifier. You'd specify the mesh channel as an input and store its attribute index. During evaluation you'd read the channel as a MeshFilter:

	CLxUser_MeshFilter	 mfilt;
	CLxUser_Mesh		 mesh;

	if (m_attr.ObjectRO (i_mesh, mfilt)) {
		if (mfilt.GetMesh (mesh))
			np = mesh.NPoints ();
	}

Q: My plugin has a modifier and an item instance with an item drawing interface. How can I get the modifier object inside the instance's methods?

A: Read the modifier object from the appropriate item channel, and convert:

	CLxUser_ValueReference   ref;
	LXtObjectID		 obj;

	chan.Object (m_item, "myModifierObjectChannelName", ref);
	ref.GetObject (&obj);

	CLxSpawner<MyModifierClass>	 spawner ("myModSpawnName");
	MyModifierClass		*mod;

	mod = spawner.Cast ((ILxUnknownID)obj);

     // Do stuff to draw modifier here

	lx::ObjRelease (obj);

Q: What time is it?

A: Actually this is a fairly complex question. As a user the answer is simple -- the current time is shown on the time slider and you scrub it to change time. But when writing plug-ins you have to realize that nexus takes a much more holistic view of time and simple linear thinking can cause problems.

For commands that perform edits, or items that draw themselves in 3D, the current global time can generally be used. This is maintained as part of the selection system along with all the other user-controlled state that affects UI interaction.

TabbedArea


When evaluating the scene graph, however, time is not universal.

...

Q: How do I know when the current time changes?

A: Getting notified of global state changes is done though a Global Listener Object. This is an object that you create and export to modo, and your methods will be called when events happen. The easiest way to create a one-off object of this type is to use a Singleton Polymorph. The selevent_Time() method will be called with the new time as the user scrubs the timeline.

class CTimeChangeTracker :
		public CLxImpl_SelectionListener,
		public CLxSingletonPolymorph
{
   public:
	LXxSINGLETON_METHOD;

	CTimeChangeTracker ()
	{
		AddInterface (new CLxIfc_SelectionListener<CTimeChangeTracker>);
	}

		void
	selevent_Time (
		double			 time)
					 LXx_OVERRIDE
	{
		current_time = time;
	}
};

Since this is a singleton you'd store it as global state in your plug-in.

static CTimeChangeTracker      *time_tracker = 0;

The first time you need to start tracking time you create the object and register it with the ListenerService. Do not do this in your initialize() function since that may be too soon.

	CLxUser_ListenerService	 ls;

	time_tracker = new CTimeChangeTracker;
	ls.AddListener (*time_tracker);

When you are done tracking time changes you should unregister your listener.

	CLxUser_ListenerService	 ls;

	ls.RemoveListener (*time_tracker);
	delete time_tracker;

Q: How do I make my command button update when the enable state changes?

A: You need a notifier. Command notifiers send change flags on specific events to indicate that some aspect of the command's state may have changed. Flags can indicate the enable/disable state, the label, the value, or the datatype.

  • Notifiers can be added to a basic command by implementing basic_Notifier() which returns the name and arguments for each notifier by index.
  • Common notifiers for selection changes, mesh edits, etc, can be found in the notifier docs.
  • Changes to plug-in state can trigger notifications by declaring a notifier server. These are created by inheriting from CLxCommandNotifier.

Q: How do I write to the log?

A: Writing to the event log viewport can be done by deriving from the CLxLogMessage utility class (or the CLxLuxologyLogMessage class, which just adds a Lux copyright). This is done by objio.cpp to report load warnings and errors. See Writing to the Event Log for more detail.

The spikey tool sample uses a log block to display tool feedback. The current value is formatted into the block and displayed as part of the tool info viewport.

Writing to the debug output on stdout is possible using one of the variants of LogService::DebugOut(), as shown in the Hello World sample. You have to specify a level, and the default level for release builds is 'error' I think. Lower-level messages are filtered out. If you want to see all the debug output, start modo with the "-debug:verbose" command line switch.

Q: How do I open a file dialog from my command?

A: Normally this is done in your Interact() method, something like this:

	void
CMyLoadCommand::cmd_Interact ()
{
	/*
	 * Open the dialog using the "dialog.*" sub-commands. Works here
	 * because they are non-model, non-undoable commands.
	 */
	fire ("dialog.setup fileOpen");
	fire ("dialog.title {Load Animation}");
	fire ("dialog.fileTypeCustom {Quicktime Movie} {*.mov;*.mp4} mov");
	fire ("dialog.open");

	/*
	 * Query the result, getting a list of filenames.
	 */
	CLxUser_Command		 resCmd;
	CLxUser_ValueArray	 va;
	LXtObjectID		 obj;
	unsigned int		 n;

	check ( srv_cmd.NewCommand (resCmd, "dialog.result") );
	check ( srv_cmd.QueryIndex (resCmd, 0, va), LXe_FAILED );

	/*
	 * Although it's a list, there's only one filename (since this was
	 * a single-file dialog).  We'll set our filename argument to the
	 * first one in the list.
	 */
	n = va.Count ();
	if (!n)
		return;

	std::string		 filename;

	check ( va.String (0, filename) );
	check ( attr_SetString (0, filename.c_str ()) );
}

The fire() method is provided by inheriting from this utility class.

class CCommmandUtility {
   public:
   void        fire (const char *cmd)
   {
       check ( srv_cmd.ExecuteArgString (-1, LXiCTAG_NULL, cmd) );
   }

   CLxUser_CommandService         srv_cmd;
};

The check() functions are defined in the lx_err namespace.

Q: How do I determine the visibility of an item?

A: There's a hidden hVisible channel on locators that takes parent visibility into account. You just need to read that in an evaluated context.

CLxUser_ChannelRead     eval;

eval.from (item, time);
vis = eval.IValue (item, LXsICHAN_LOCATOR_HVISIBLE);

Q: How do I get my C++ implementation from a COM handle?

A: If you have an interface handle and you know the type of the C++ object that's implementing it, you can unwrap the COM object and get at the meaty C++ object inside. If this is one of your servers, then you just have to call lx::CastServer() with the server name:

	CMyClass *
Extract (
 	ILxUnknownID		 from)
{
	CMyClass		*mine;

	lx::CastServer (SERVER_NAME, from, mine);
	return mine;
}

If the COM object comes from a spawner then you need to use the Cast() method on the spawner:

	CMyClass *
Extract (
 	ILxUnknownID		 from)
{
	CLxSpawner<CMyClass>	 spawn ("myClass");

	return spawn.Cast (from);
}

You have to make sure that the interface pointer you have is actually implemented by your class. If you get an ILxItemID pointer, for example, you need to query for one of your specific implemented interfaces in order to be able to get your package instance class.

Q: How can my channel modifier read inputs at different times?

A: This is done by using methods on the Evaluation Interface. You need to cache this interface as part of your member data in your Allocate() method.

        LxResult
CTimeOffset::cmod_Allocate (
        ILxUnknownID		 cmod,
        ILxUnknownID		 eval,
        ILxUnknownID		 item,
        void		       **ppvData)
{
        m_eval.set (eval);
        ...
}

Then in the Evaluate() method you read the channels you want at the current time first (in this case the current time and an offset time value), then set the evaluation for an alternate time and read other inputs.

        LxResult
CTimeOffset::cmod_Evaluate (
        ILxUnknownID		 cmod,
        ILxUnknownID		 attr,
        void			*data)		
{
        CLxLoc_ChannelModifier	 chanMod (cmod);
        double			 time, dt, value;

        chanMod.ReadInputFloat (attr, INDEX_TIME,   &time);
        chanMod.ReadInputFloat (attr, INDEX_OFFSET, &dt);

        if (dt)
                m_eval.SetAlternateTime (time + dt);

        chanMod.ReadInputFloat (attr, INDEX_INPUT, &value);
        ...
}

Q: Can my modifier read from the setup action?

A: Yes. It's the same process as in the previous answer, but using Evaluation::SetAlternateSetup().

        m_eval.SetAlternateSetup ();

If you want to clear the alternate and read from the current time and action again, use ClearAlternate().

        m_eval.ClearAlternate ();

Q: Why won't my channel modifier write a matrix?

If you're trying to write a matrix in a channel modifier, you might try something like this:

        chanMod.WriteOutputVal (attr, 0, (void **)(&outMatrixPtr));
        for (int i = 0; i < 3; i++) {
                 for (int j = 0; j < 3; j++) {
                          (*outMatrixPtr)[i][j] = m[i][j];
                 }
        }

which wouldn't work. For whatever reason, if you want to write to matrix outputs, you need to write to every link individually. So your code would need to look like this:

        chanMod.OutputCount (0, &outCount);
        for (unsigned idx = 0; idx < outCount; idx++) {
                 chanMod.WriteOutputValByIndex (attr, 0, idx, (void **)(&outMatrixPtr));
                          for (int i = 0; i < 3; i++) {
                                   for (int j = 0; j < 3; j++) {
                                            (*outMatrixPtr)[i][j] = m[i][j];
                                   }
                          }
        }

Q: How do I read server tags on a package?

A: Packages are servers, so you can access them through the HostService Interface as a Factory Object. This function returns the value for any server tag given the server class, the name of the server, and the tag key.

        const char *
ServerTag (
        const char             *className,
        const char             *serverName,
        const char             *tagKey)
{
        CLxUser_HostService     hostSrv;
        CLxUser_Factory         factory;
        const char             *value;

        hostSrv.Lookup (fac, className, serverName);
        if (LXx_OK (fac.InfoTag (tagKey, &value))
                return value;

        return 0;
}

In order to use this function to read server tag for item types (packages), you just need to find the package name from the item type.

        bool
IsMask (
        CLxUser_Item           &item)
{
        CLxUser_SceneService    scnSrv;
        const char             *pkgName;

        scnSrv.ItemTypeName (item.Type (), &pkgName);
        return (ServerTag (LXa_PACKAGE, pkgName, LXsPKG_IS_MASK) != 0);
}

Q: What does cannot allocate an object of abstract type mean?

A: This means you have inherited from a superclass which has pure virtual methods, and you have failed to provide an implementation for one or more of those methods. In the context of the SDK, the most likely cause is that you're using an implementation class with required methods. For example, suppose your package instance inherits from the ChannelModItem Interface. It's not enough to simply inherit from the implementation.

class CInstance
        : public CLxImpl_PackageInstance,
          public CLxImpl_ChannelModItem
{
        ...
};

If you attempt to initialize a polymorph based on this class it will fail with the abstract type error. That's because the cmod_Flags() method is pure virtual and must be implemented by your class. Of course, for the channel modifier to do anything you need a flags method, so this isn't really a hardship. It's just something to be aware of when starting the implementation for your SDK objects.

Q: Is it possible to create SDK wrappers for other languages?

A: Yes. Yes it is.

Q: How do I change selections?

A: Changing selection is done using the SelectionService Interface. For each selection type you need its translator and its selection type ID code. In the Python API, the packet translation object doesn't have the autoInit method, but it's easy enough to initialize. You just need to allocate a SelectionType object for the Vertex Translator. TabbedArea

In order to operate on selections you need to create a packet from your data, and use the packet pointer to query or alter the selection. For example, to select a single vertex you would clear the current selection, create a packet for the vertex using the vertex ID and mesh item, then select the packet. TabbedArea

Changing selection is undoable, so these can only be called while executing an undoable command.

Q: How do I create a new XCode project for a modo plugin, from scratch?

A: Firstly, you don't want to be creating a bundle, you want to create a C/C++ Library, set the type to Dynamic.

Then add a new target to the project, of the same type, but this one is a static library. Add the .cpp files in the SDK common directory to this target and set the header search path to look in the SDK header directory.

Then select your plugin target and go to build phases, add the SDK target as a target dependency. Also add the SDK target under Link Binary with libraries.

That should be everything you need to do.

The easiest way to see how to setup the project files is to start with the sample plugins. There are both Xcode and Visual Studio projects included.

Q: How do I read a gradient channel?

A: A gradient channel is read using the ILxGradientFilter interface.

The evalulate method on the GradientFilter interface takes an input value (the X axis) and returns the corresponding output value or Y axis. In Python, gradient channels can be read into Value Objects, which are then read through a Gradient Filter Object just like C++.


TabbedArea

yAxis now holds the gradient channel value at the position on the gradient defined by xAxis.

Q: How can I make sure vtx_ReadChannels is called for every change in time

In vtx_LinkChannels, add a time channel:

 CLxUser_Evaluation	 ev (eval);
 idx_currentTime = ev.AddTime();

in vtx_ReadChannels that will be available and return the current time:

 CLxUser_Attributes	 at (attr);
 double currentTime = at.Float(idx_currentTime);

Q: How do I read the transform channels for a locator type item?

A: Unlike other 3D applications, modo allows you create multiple transforms (position, rotation and scale) on a single item, and allows you to reorder them to control how those transforms are applied. This results in an extremely flexible rigging workflow, allowing you to create complex transforms on a single item, without requiring a hierarchy of multiple items.

These transforms are stored as a separate item, connected to the locator type item that they want to manipulate, via a graph. This can be somewhat confusing, as you may try and search for the transform channels on the locator item and be unable to find them.

There are two ways to access and manipulate these transforms. Firstly, you can use the Locator Interface, this is an interface on the locator type item and has various methods for manipulating transforms. For example, if you want to get the main transform item for either rotation, scale or translation, you use the GetTransformItem method and pass it the type of transform you want. You can then read the channels on that item directly. There are also some helpful methods for getting and setting both the world and local transforms.

TabbedArea

The alternative method for getting transform items, is to walk the graph and find the transform item directly. This is a little more in-depth, but allows greater control over the values returned, and also allows you to manipulate the transform items themselves.

The transform items are linked to the locator item they provide transforms for, in the XfrmCore graph, with the transform items connecting into the locator item. A single locator type item could potentially have multiple transforms connected through this graph. The order they are connected is the reverse order of the order they appear in the channel list inside of modo.

TabbedArea

Q: How do I localize a path Alias?

A: Parse a string to FileService.ToLocalAlias which contains just the path alias you would like to resolve. The function will return the absolute pathspec in local OS specific format. If the alias could not be found the original string will be returned. You can pass a that contains a path alias and additional sub-folder content or filespec content and the alias portion will be resolved in place. If the Alias is not translated the original input string is returned unaltered.

Eg. Given a alias ArtisticGlassMaterials = H:\modo\content\Assets\Materials\Glass\Artistic TabbedArea

Q: How do I remove a listener object in Python?

A listener is an object you export to be called when changes occur. You create the object and add it, and then remove it when you are done listening. This is a bit tricky in Python, however, because the automatic type conversion in the API works against you. For example, let's say you do the obvious thing and try this:

  class MyListener(lxifc.SceneItemListener):
      pass
 
  lSrv = lx.service.Listener()
  listener = MyListener()
 
  lServ.AddListener(listener)
  lServ.RemoveListener(listener)

Your listener object will be added, by trying to remove it will result in a 'not found' exception. (In older versions of MODO this did not generate an error but left your listener object installed.) The problem is that passing a Python object to a method that expects a COM object will do the conversion automatically, but this means that the two functions are called with different COM objects. Because MODO doesn't recognize the second object as being the same as the first, the listener remains installed.

The solution is to pre-allocate the COM version of your object. You can then pass this to both functions and it will be seen as the same object.

  listener = MyListener()
  com_listener = lx.object.Unknown(listener)
 
  lServ.AddListener(com_listener)
  lServ.RemoveListener(com_listener)

Q: ???