SOLVED What's really happening inside a CKey?

Hello once again;

I have expanded some scripted automatisms for CTrack, CCurve and CKey from pure float values to other types, and it turned out to be somewhat irritating. The class CKey provides two different methods to set/get values: SetValue and SetGeData. Documentation tells me that the former is for floats, the latter for "any", as Python does not have a GeData class.

Although... the doc now gets cryptic. SetGeData precisely says it sets "the data" of the key - not "the value". The C++ doc tells me to use SetValue only for floats and "Use GetParameter() to read non-real values" (with an example). SetGeData in the C++ docs says "Sets the data of the key" as well; it's so consistent that I begin to suspect "value" and "data" of a key are actually two different things.

As SetValue works fine for floats, I try SetGeData for a DTYPE_LONG track anyway (PRIM_CUBE_SUBX). Doesn't work as expected. In the Timeline, the result is always 0, and the animation widget in the Attribute Manager shows up as a yellow circle as if that frame doesn't have any key.

On the other hand, for a DTYPE_BOOL track, SetGeData works fine.

After some searching, I get an answer on this very forum: I must take the track category into account. CTRACK_CATEGORY_VALUE requires GetValue, and CTRACK_CATEGORY_DATA needs GetGeData. And CTRACK_CATEGORY_PLUGIN, well, I still have not the slightest idea, but I suppose GetParameter may be the thing here?

I'm doing the following test script:
(this works only for a selected cube primitive and creates keys for the Fillet boolean parameter and the X segments value)

import c4d
from c4d import gui


def SetKey (currentTime, obj, parameterDesc, value):
    track = obj.FindCTrack(parameterDesc)
    print ("To set:", value, type(value))

    if track == None:
        # create track for this parameter
        track = c4d.CTrack(obj, parameterDesc)
        obj.InsertTrackSorted(track)
    curve = track.GetCurve() # always present
    cat = track.GetTrackCategory()
    if cat == c4d.CTRACK_CATEGORY_PLUGIN: 
        print ("Track is Plugin")
        return
    elif cat == c4d.CTRACK_CATEGORY_VALUE: 
        print ("Track is Value")
    elif cat == c4d.CTRACK_CATEGORY_DATA:
        print ("Track is Data")
    else:
        print ("Track is unknown")
        return 

    def SetKey_Value(key, value):
        if cat == c4d.CTRACK_CATEGORY_VALUE:
            origData = key.GetValue()
            print ("Orig:", origData, type(origData))
            key.SetValue(curve, value)
            postData = key.GetValue()
            print ("Post:", postData, type(postData))
        elif cat == c4d.CTRACK_CATEGORY_DATA:
            origData = key.GetGeData()
            print ("Orig:", origData, type(origData))
            key.SetGeData(curve, value)
            postData = key.GetGeData()
            print ("Post:", postData, type(postData))
        
    currentKey = curve.FindKey(currentTime)
    if currentKey:
        key = currentKey['key'] # key reference to change
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, key)
        SetKey_Value(key, value)
        curve.SetKeyDirty()
        # Key attributes found under C++ API doc: ckvalue.h File Reference
    else:
        defkey, dub = doc.GetDefaultKey() # new key to insert
        key = defkey.GetClone() # needed?
        SetKey_Value(key, value)
        key.SetTime(curve, currentTime)
        # key.SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE)
        curve.InsertKey(key, True)
        doc.AddUndo(c4d.UNDOTYPE_NEW, key)


def main():
    if op == None : return

    descSubX = c4d.DescID(c4d.DescLevel(c4d.PRIM_CUBE_SUBX, c4d.DTYPE_LONG, 0))
    descFillet = c4d.DescID(c4d.DescLevel(c4d.PRIM_CUBE_DOFILLET, c4d.DTYPE_BOOL, 0))

    doc.StartUndo()
    val = op[c4d.PRIM_CUBE_SUBX]
    SetKey (doc.GetTime(), op, descSubX, val)
    val = op[c4d.PRIM_CUBE_DOFILLET]
    SetKey (doc.GetTime(), op, descFillet, val)
    doc.EndUndo()
    c4d.EventAdd()


if __name__=='__main__':
    main()

This works fine, and tells me in the console output that actually a DTYPE_LONG DescID has created a track of category CTRACK_CATEGORY_VALUE, and my integer value that I'm trying to pass to the key is converted into a float. And the DTYPE_BOOL DescID has created a track of category CTRACK_CATEGORY_DATA, and my Boolean value that I'm trying to pass to the key is converted into an int.

While I now have a function that seems to work for two track categories (I haven't even started on the "plugin" category), this is not what I would have expected from the documentation. I later discover the track category thing in the C++ CKey manual as well, but there it only again mentions float values.

Questions:

  1. Is my solution for DTYPE_LONG even correct? It works, but the solution is pretty much not what I gather from the API docs.
  2. So a CKey stores an int as float, and a bool as int, and... what's the mapping here?
  3. Are the Set/GetValueLeft/Right functions similarly dispositioned? The Python docs say "Just for keys with float values." but right now I suspect that they will also work for DTYPE_LONG tracks as the according keys seem to work with floats internally. (Also, the Timeline shows me tangents.)
  4. Checking the track categories, I find that a PLA track (as the C++ CKey API doc shows as an example) is a Plugin track. So, am I right with my theory that Plugin tracks need GetParameter, just like Value tracks need GetValue and Data tracks need GetGeData? The doc says "Use GetParameter() to read non-real values." in the same place, but that is not taking GetGeData in account at all? (I'm not even going to mention that GetParameter also uses GeData...)

hi,

1-2
As you said, if the track is CTRACK_CATEGORY_VALUE the value of the key will be stored as a float.
if the track is CTRACK_CATEGORY_DATA, the data will be stored in a GeData, so the data can be anything that GeData can store.
CTRACK_CATEGORY_PLUGIN will be for the plugins that create track (and neither a float nor a GeData)

3- left and right values are stored as float. the track type isn't checked. (by the way, the function accept a float as parameter)
4-
well, our comments are not that clear about the key storing data and pla.
8b0f4671-3fa2-4b72-84aa-84e7fc8ba8a5-image.png

For plugins track, you need to use Set/GetParameter because cinema4D doesn't know how to store that parameter (just like a custom datatype).

Cheers,
Manuel

The internal behavior seems to be even more convoluted... I just tried writing out the Track Category to the console, and an integer / DTYPE_LONG value does not even lead to the same category necessarily.

If you have an integer with a simple numeric control in the AM, like a cube's subdivisions, you get a value track (and the value itself is stored as float internally).

If you have an integer with a restricted GUI, like a Cycle or Radio Button control, you get a data track, although it's DescID is also a DTYPE_LONG.

I conclude that the track gets its category - for which there is no setter function - from extra information about the attribute, which must be stored in the animated object (because it cannot be part of the DescID). I tried that for a few animation tracks that I created through the GUI (not by script) on a Platonic, and get:

Track: Blubberlutsch (user data, numeric field)
-- DescID: ((700, 5, 0), (1, 15, 0))
-- Value track
Track: Quibbelplaff (user data, radio buttons)
-- DescID: ((700, 5, 0), (2, 15, 0))
-- Data track
   Note how this is almost the same DescID as Blubberlutsch (except for the ID), yet the track category comes out as data, not value!
Track: Position . Y
-- DescID: ((903, 23, 5155), (1001, 19, 23))
-- Value track
Track: Enabled
-- DescID: (906, 400006001, 5155)
-- Data track
Track: Display Color
-- DescID: (907, 15, 5155)
-- Data track
Track: Radius
-- DescID: (1120, 19, 5161)
-- Value track
Track: Segments
-- DescID: (1121, 15, 5161)
-- Value track
Track: Type
-- DescID: (1122, 15, 5161)
-- Data track

In any way, the documentation is at least misleading.

I am not sure if you're right about Set/GetParameter. The signature for these functions in C++ both contain a GeData & t_data, so the values for this track are read and written through a GeData object either way. As GeData can hold a CustomDataType, the key's function Set/GetGeData should be able to handle that as well.
The question seems to be what happens in Python in that case, as there is no GeData. So, I used the PLA track to cobble together an ultimate example:

import c4d


def main():
    if op == None: return
    for track in op.GetCTracks():
        name = track.GetName()
        desc = track.GetDescriptionID()
        print ("Track:", name)
        print ("-- DescID:", desc)
        cat = track.GetTrackCategory()
        if cat == c4d.CTRACK_CATEGORY_VALUE:
            print ("-- Value track")
        if cat == c4d.CTRACK_CATEGORY_DATA:
            print ("-- Data track")
        if cat == c4d.CTRACK_CATEGORY_PLUGIN:
            print ("-- Plugin track")
        if name == "PLA":
            curve = track.GetCurve()
            currentKey = curve.FindKey(doc.GetTime())
            key = currentKey['key']
            bc = key.GetGeData()
            print (bc)
            print (len(bc))
            for index, value in bc:
                print(f"id: {index}, val: {value}")
            
if __name__=='__main__':
    main()

Took me additional iterations to find that GetGeData returns a BaseContainer, but now I know that this BaseContainer contains an element which is a c4d.PLAData object.

So, at least in this case it is not necessary to access the key through GetParameter. I suspect that the line
Data of special tracks can be read and set using C4DAtom::GetParameter() and C4DAtom::SetParameter().
from the C++ CKey manual does not refer to the data content of the keys, but to the track's data itself.

E.g. the Sound track does not have keys but stores its data in the CTrack object, and since there is no special derived class for a sound track, there cannot be specialized functions to retrieve that data, so if you want the sound's filename, you have to use GetParameter.
Which can, under Python, be shortened to the square bracket notation as this is only a shorthand for GetParameter anyway.

Correct me if I'm wrong...

Hi,

cKey::SetDParameter is called with a GeData.

This GeData is set depending on the type of the description gadget. This is done in C4DAtom::SetParameter like so

const BaseContainer *bc = desc.GetParameter(id, temp, ar);
...
switch (bc->GetId())
case DTYPE_LONG:
     // special case if it's a cycle
     // clamp the value to min or max if needed.
     // data is a Int32
case DTYPE_REAL:
      // clamp the value to min or max if needed
     // data is a Float
// both color and vector are the same
case DTYPE_COLOR:
case DTYPE_VECTOR:
     // special case as a vector is a bit special
     // data is a Vector
case DTYPE_TIME:
     // data is a BaseTime
case DTYPE_BOOL:
     // data is a Int32
case DTYPE_GROUP:
case DTYPE_SEPARATOR:
        break;
default:
// try to find a plugin for that data

now on CKey::SetDParameter, as you can see depending on the track type, SetValue or SetGeData Is called.

switch (track type)
       case CTRACK_CATEGORY_VALUE:
		SetValue(seq, t_data.GetFloat());
		flags |= DESCFLAGS_SET::PARAM_SET;
		break;
	case CTRACK_CATEGORY_DATA:
		SetGeData(seq, t_data);
		flags |= DESCFLAGS_SET::PARAM_SET;
		break;
	case CTRACK_CATEGORY_PLUGIN:
		CriticalStop();
		break;

I'm not sure what's the goal here. We all know that our documentation isn't ultra-precise in all points, and it's not its goal.
Specially on things that can be old like track and animation.

I could spend more time and do lots of more test to see when and when not this function is called or not but as you know c4d's code is huge and we can't dissect all part of the code like this. (i wish we could)

I am missing something here?

Cheers,
Manuel

@m_magalhaes said in What's really happening inside a CKey?:

I'm not sure what's the goal here. We all know that our documentation isn't ultra-precise in all points, and it's not its goal.
Specially on things that can be old like track and animation.
I could spend more time and do lots of more test to see when and when not this function is called or not but as you know c4d's code is huge and we can't dissect all part of the code like this. (i wish we could)
I am missing something here?

Thanks for answering; no, I don't think you're missing anything. I was just trying to explain the topic in my Python course, based on a ton of code I have written in the past, and I came to notice that tracks of type integer don't behave as I expected them.

That is probably my fault as I have only really used float tracks in the past (mostly MoCap stuff) so the issue never presented itself before. It's also a bit more ambiguous in Python than in C++ since Python does not have the GeData class but uses "any" type.

I believe I have written and checked enough code now to understand the handling for non-float tracks, so I am going to close the topic.

(It would be interesting to follow up some more about your second code sample, as it gets to a CriticalStop on a Plugin track, while I definitely can use GetGeData on the key of a PLA track which is a Plugin track... but I suppose this is too much internal Cinema code to analyze, and I cannot follow the call stack here all the way down to SetDParameter. Maybe some other time...)

Thanks again.

@cairyn said in What's really happening inside a CKey?:

(It would be interesting to follow up some more about your second code sample, as it gets to a CriticalStop on a Plugin track, while I definitely can use GetGeData on the key of a PLA track which is a Plugin track... but I suppose this is too much internal Cinema code to analyze, and I cannot follow the call stack here all the way down to SetDParameter. Maybe some other time...)

This is in CKey::SetDParameter and that is probably never called for a plugin track. That is why it's doing nothing. But to be sure, i would need to create (or use our sdk example) plugin track.

I add a task to our task pool but we have so much to document before that...

Cheers,
Manuel

Hello @Cairyn,

I have just updated the Python docs regarding the class CKey. These changes will be reflected in an upcoming release of the Python SDK. One minor thing for you and to avoid confusion for future readers of this thread: The information brought forwards here that keys of type DTYPE_BOOL must be written with CKey.SetValue() is incorrect. .SetValue() must only be used for writing DTYPE_REAL and DTYPE_LONG parameters, i.e., float and int values in Python terms. DYTPE_BOOL key values must be written with CKey.SetGeData().

Cheers,
Ferdinand

import c4d

def main():
    """Example for writing the 'use color' track of a material.

    The function expects the first material in the document to have a 
    'use-color' track, i.e., an animation for the little check box for the
    color channel. It will write into the first key of that track.
    """
    # Get the first material of the document and its first track.
    mat = doc.GetFirstMaterial()
    if mat is None:
        raise RuntimeError("No material found.")
    track = mat.GetFirstCTrack()
    if track is None:
        raise RuntimeError("Material has no tracks.")

    # Bail when this first track is not for the boolean "use color" parameter 
    # of the material.
    if track.GetDescriptionID()[0] != c4d.MATERIAL_USE_COLOR:
        raise RuntimeError("'Use-Color' track not found")

    # Get the curve of the track and its first key. 
    curve = track.GetCurve(c4d.CCURVE_CURVE)
    if curve.GetKeyCount() < 1:
        raise RuntimeError("Curve has no keys.")
    key = curve.GetKey(0)

    # Print the current value of the boolean key and then set it to `True`.
    print (key.GetGeData())
    key.SetGeData(curve, True)

    # Push an update event to Cinema 4D.
    c4d.EventAdd()

# Execute main()
if __name__=='__main__':
    main()

@ferdinand said in What's really happening inside a CKey?:

I have just updated the Python docs regarding the class CKey. These changes will be reflected in an upcoming release of the Python SDK.

Thank you!

One minor thing for you and to avoid confusion for future readers of this thread: The information brought forwards here that keys of type DTYPE_BOOL must be written with CKey.SetValue() is incorrect.

Hmm, I just tried finding where that was claimed, can't see it right now. Anyway, if you go by the track's CATEGORY, you can easily make the distinction between SetValue and SetGeData. That would work even in ambiguous cases.

Hi @Cairyn,

I do not really know where this has been claimed, but there was a note in our internal systems which summarized this thread as that the data types DTYPE_REAL, DTYPE_LONG, and DTYPE_BOOL should be written with .SetValue(). For which our documentation currently makes the slightly misleading claim that is should be used for float values. Which I believe was at least partially the starting point of this thread.

I just wanted to make sure that no unclarities remain for future readers, DTYPE_BOOL keys/tracks should be written with SetGeData() and .SetValue() can be used for DTYPE_REAL and DTYPE_LONG type keys.

Cheers,
Ferdinand

DTYPE_LONG can be used with SetGeData if the track category is CTRACK_CATEGORY_DATA, see the third post in this thread. This is the only ambiguous datatype at the moment, and as other datatypes wouldn't make sense with SetValue I think it will remain the only one. But I do claim that the category of the track determines whether you need to use SetValue or SetGeData. (That is almost the same in effect... but in case of DTYPE_LONG not quite.)