In my last post I went over the structure of the Space Switch component I’ve been building. In the last line I asserted that I would get it to work without connecting the attribute by using callbacks.
I was wrong.
The reason I wanted to keep the toggle attribute disconnected from the space switch was that it creates an evaluation cluster. Unfortunately, I ran into a problem synchronizing the two attributes. It turns out that the Time Changed event is triggered before the graph is evaluated, so the value of the toggle attribute on the control might be stale.
So I decided to just connect the attribute and accept the small evaluation cluster.
The other reason I wanted to use callbacks was to make the control preserve position when the toggle attribute is changed in the channel box. The way I’ve usually seen this done is to have a UI or a marking menu call a function that does this. But I figured I could make it happen with a callback or two.
The main callback here is the Attribute Changed callback. It gets called when an attribute is changed by the user, either by changing it in the UI, or setting the attribute by using MEL.
Unfortunately, this callback is triggered after the graph is evaluated, so I need to “cache” the position of the control.
EDIT: Someone pointed out to me that the Attribute Changed callback is immediate. While this is true, it was still happening after the changed value propagated through the graph, meaning that I couldn’t query the position of the control before the change affected it.
My approach was fairly straightforward – I created a duplicate set of nodes that calculate the offset, and a duplicate of the control under it with its transform attributes driven by the control. The duplicate nodes were driven by a duplicate attribute that cached the value of the toggle attribute.
There are only two hard problems in computer science – cache invalidation, naming things, and off by one errors. Since I was caching an attribute, I needed to make sure the cache was valid. So every time the frame changed, I need to validate the cache.
Unfortunately, the Time Changed event is triggered before the graph is evaluated. But, it let me know that I need to refresh the cached value. My first solution was a one shot callback the next time the scene was idle. That didn’t play well with parallel evaluation. After reading through all the callbacks available in Maya, I realized I could listen for when the toggle attribute was dirtied. So I set up a flag that was set when Time Changed. When the toggle attribute was dirtied, I checked the flag. If the flag was set, I refreshed the cache.
At this point, the toggle behaved as expected – when you switched spaces in the channel box, the control stayed where it was. But the duplicate set of nodes meant that the component was twice as heavy as it needed to be.
Looking at the graph, I realized that I didn’t need the duplicate set of nodes. The value of the toggle attribute corresponds to the index of the message array attribute the parent spaces are connected to. This meant I could walk the graph from the control and calculate the worldMatrix of the control from previous value of the toggle attribute.
The final setup takes four callbacks – Time Changed, Playback Stopped, Node Plug Dirtied, and Attribute Changed. The time changed callback marks the cache as needing validation. The playback stopped callback validates the cache (time changed is not called when playback / scrubbing stops). Node Plug Dirtied validates the cache when the toggle attribute is dirtied. And as discussed above, the Attribute Changed attribute preserves the position of the control when the user changes the toggle attribute.
In order to keep track of the callbacks, and prevent the same control from having its callbacks being created multiple times, I’m using a non-storable array-like integer attribute. Since the attribute is not writable, it’s values will not be saved. To install the callbacks, I’ll have a script node in the scene that runs a command at scene open.