HowTo set keyframes for PosXYZ/RotXYZ/ScaleXYZ with a script

Hello folks!

I am a senior animator for the past 20 years now. I am coming from Softimage. The best Animation Tool ever 🙂

I have to switch to C4D now because at the moment I am working for a company. They work with C4D.

There is something I really need for my animation workflow:
I want to set a key on

  • Position X/Y/Z (XYZ separately)
  • Rotation X/Y/Z (XYZ separately)
  • Scale X/Y/Z (XYZ separately)

with hotkeys.

Due to the fact, this possibility is not given via the command manager (who knows why...), I need a Python script, which I can save in the PlugIn folder and assign a hotkey to this.

Sadly I am no coder and need some help. Already found some stuff, but it's not really helping me with this problem. There is an old post, but this doesn't work anymore.
https://plugincafe.maxon.net/topic/11552/set-keyframes-through-a-python-tag

Some more information:
0a621171-98af-4430-a599-52434114f831-image.png
The the script should just execute this simple single click I can do with the left mouse button.
For a single transform parameter. I just want to add a keyframe. If there is no track, one will be created, if there is one, a key will be added with the current value on the current frame of the scene.

It should work for all the selected objects in this moment. Not for the Children.

It would be so nice, if someone could help me with this. Just for PositionX. I think, I am able to adapt this to the other parameters.

Kind regards
Vannipo

Hello @vannipo,

Thank you for reaching out to us. First of all, I think you should have a look at the animation features of Cinema 4D, auto keying should be very close to what you want to do.

If you still want to create nine scripts for these 3*3 parameter channels, find a brief example below. Please also understand that we are usually not in the habit of writing scripts or plugins on demand and that you must write your code yourself.

Cheers,
Ferdinand

Code:

"""Sets the current relative position x-component of the selected object as a keyframe at the current
time.

Must be run as a Script Manager script.
"""
import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: typing.Optional[c4d.BaseObject] # The active object, can be None.

# The major work in writing key frames in the way you want to do it, lies in precisely defining the 
# DescID for these parameters. I wrote this example only for keying the relative position x-component,
# but below I hint at how the remaining eight parameters must be addressed.

# The description level for the relative position, rotation, and scale parameter, we are going to
# reuse these when defining IDs for each component of them.
DLV_REL_POS: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0)
DLV_REL_ROT: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0)
DLV_REL_SCL: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_SCALE, c4d.DTYPE_VECTOR, 0)

# The description levels for the x, y, and z components of a vector parameter.
DLV_VEC_X: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
DLV_VEC_Y: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
DLV_VEC_Z: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)

# Define the IDs for the x, y, and z relative position parameters.
DID_POS_X: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_X)
DID_POS_Y: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Y)
DID_POS_Z: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Z)

# Define the IDs for the x, y, and z relative rotation and scale parameters.
DID_ROT_X: c4d.DescID = c4d.DescID(DLV_REL_ROT, DLV_VEC_X)
# ...
DID_SCL_X: c4d.DescID = c4d.DescID(DLV_REL_SCL, DLV_VEC_X)
# ...

def main() -> None:
    """Runs the example.
    """
    # Stop the script when there is no object selected.
    if not op:
        return None

    # Find the track for the parameter we are interested in creating, here the track for x component
    # of the relative position, or create a new track when there is none.
    track: c4d.CTrack = op.FindCTrack(DID_POS_X)
    if track is None:
        track = c4d.CTrack(op, DID_POS_X)
        if not track:
            raise MemoryError(f"{track = }")
        op.InsertTrackSorted(track)

    # Get the current time and the key in #track for this time. CCurve.AddKey will return an existing
    # key if there is one, so we do not have to search for potentially existing keys first.
    t: c4d.BaseTime = doc.GetTime()
    curve: c4d.CCurve = track.GetCurve()
    keyData: dict = curve.AddKey(t)
    if keyData is None:
        raise RuntimeError("Could not add key frame.")

    # Set the value of the key to the current value of that parameter.
    key: c4d.CKey = keyData["key"]
    key.SetValue(curve, op[DID_POS_X])

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


if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Hello @ferdinand,

1st of all: Thank u very much for your support!
I thought, there is maybe something existing, what I have missed. I understand, that's not the place to ask for ready-to-use-scripts. Really sorry.

I know the autokeying feature. But this is exactly not something I am looking for, cause it sets keys only on existing curves and on curves, where I don't want a key to get added.
E.g. if an object has curves for X and Z and keys are not at the same frame and I move the object in X, it also gets a key on Z. And that's exactly what messes clean curves...

I will try out the script you posted.

Again, thank you very much!

Kind regards
Vannipo

@ferdinand
After some research I added

key.ChangeNBit(c4d.NBIT_CKEY_AUTO, c4d.NBITCONTROL_SET)
key.ChangeNBit(c4d.NBIT_CKEY_REMOVEOVERSHOOT, c4d.NBITCONTROL_SET)
key.ChangeNBit(c4d.NBIT_CKEY_CLAMP, c4d.NBITCONTROL_SET)
key.ChangeNBit(c4d.NBIT_CKEY_WEIGHTEDTANGENT, c4d.NBITCONTROL_SET)
key.ChangeNBit(c4d.NBIT_CKEY_AUTOWEIGHT, c4d.NBITCONTROL_SET)

after

key.SetValue(curve, op[DID_POS_X])

for getting smooth interpolation and default nice tangents.
Works very nice so far.
Thank you again, Sir!

This post is deleted!

Hello @vannipo,

for a non-coder you are already knee-deep in technical details 😉 If you want to set the key interpolation settings, changing the NBITs of a key is not categorically wrong, but a bit of a taking a sledgehammer for cracking a nut scenario.

The settings of a key are parameters, and they are primarily set with the bracket syntax in Python (there are other ways, but they are usually less important). So, when you want to set the number of subdivisions a cube object generator has on the x-axis, you do this cube[c4d.PRIM_CUBE_SUBX] = 20. The ID of the parameter, here c4d.PRIM_CUBE_SUBX, can usually be discovered by simply dragging the parameter into the console.

The thing you are operating here on, CKey instances, is however unfortunately a bit of an oddball. Because it is one of the two cases where a type is a GeListNode but not a BaseList2D which will then make it impossible to use this drag and drop workflow to discover parameter IDs. Here you must use our parameter description overviews for key types, in this case a value key, as this is what you are operating with here.

By doing this, you will not only learn a core technique within our classic API, parameter access, but also be able to modify all the other parameters a key has, and not only the one's which shine through as NBITs.

Find below a modified example.

Cheers,
Ferdinand

Result:
Screenshot 2023-01-10 at 17.48.30.png

Code:

"""Sets the current relative position x-component of the selected object as a keyframe at the current
time.

Must be run as a Script Manager script.
"""
import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: typing.Optional[c4d.BaseObject] # The active object, can be None.

# The major work in writing key frames in the way you want to do it, lies in precisely defining the 
# DescID for these parameters. I wrote this example only for keying the relative position x-component,
# but below I hint at how the remaining eight parameters must be addressed.

# The description level for the relative position, rotation, and scale parameter, we are going to
# reuse these when defining IDs for each component of them.
DLV_REL_POS: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0)
DLV_REL_ROT: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0)
DLV_REL_SCL: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_SCALE, c4d.DTYPE_VECTOR, 0)

# The description levels for the x, y, and z components of a vector parameter.
DLV_VEC_X: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
DLV_VEC_Y: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
DLV_VEC_Z: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)

# Define the IDs for the x, y, and z relative position parameters.
DID_POS_X: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_X)
DID_POS_Y: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Y)
DID_POS_Z: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Z)

# Define the IDs for the x, y, and z relative rotation and scale parameters.
DID_ROT_X: c4d.DescID = c4d.DescID(DLV_REL_ROT, DLV_VEC_X)
# ...
DID_SCL_X: c4d.DescID = c4d.DescID(DLV_REL_SCL, DLV_VEC_X)
# ...

def main() -> None:
    """Runs the example.
    """
    # Stop the script when there is no object selected.
    if not op:
        return None

    # Find the track for the parameter we are interested in creating, here the track for x component
    # of the relative position, or create a new track when there is none.
    track: c4d.CTrack = op.FindCTrack(DID_POS_X)
    if track is None:
        track = c4d.CTrack(op, DID_POS_X)
        if not track:
            raise MemoryError(f"{track = }")
        op.InsertTrackSorted(track)

    # Get the current time and the key in #track for this time. CCurve.AddKey will return an existing
    # key if there is one, so we do not have to search for potentially existing keys first.
    t: c4d.BaseTime = doc.GetTime()
    curve: c4d.CCurve = track.GetCurve()
    keyData: dict = curve.AddKey(t)
    if keyData is None:
        raise RuntimeError("Could not add key frame.")

    # Set the value of the key to the current value of that parameter.
    key: c4d.CKey = keyData["key"]
    key.SetValue(curve, op[DID_POS_X])

    # Set some of the key parameters. We could also use the more low-level and precise 
    # C4DAtom.Get/SetParameter calls instead of GeListNode.__get/setitem__ calls (the bracket 
    # syntax) used below. But that is more typing work and there is no need to be more precise here.

    # Enable the "Lock Time" tick box for the key.
    key[c4d.ID_CKEY_LOCK_T] = True
    # Set "Fixed Weighted" as the key preset.
    key[c4d.ID_CKEY_PRESET] = c4d.ID_CKEY_PRESET_FIXED_OVERSHOOTWEIGHTED
    # Enable clamping for the key, just as in the Attribute Manager, this will implicitly change
    # the preset mode to "Custom" but we will maintain the changes made to other parameters when
    # we set it to "Fixed Weighted" before.
    key[c4d.ID_CKEY_CLAMP] = True 

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


if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Hello @ferdinand ,

ah, ok. I understand. Yes, this looks a bit more handy setting those parameters this way.

Sorry, I have one more question:
How I can make this work for my current selection? No matter, how many objects I have selected...
Is it easy? Or do I have to create a "for-loop" and save my selection in the script before as an array?

Are there any references how to do that?

Hey,

How I can make this work for my current selection? No matter, how many objects I have selected...

The question is a little bit ambiguous because the current script will also work when multiple objects are selected, it will then take what Cinema 4D considers to be the primary selected object. I assume you mean that you want to run the script on all selected objects at once.

In this case you must manually retrieve all selected objects with BaseDocument.GetActiveObjects and iterate over them.

for op in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN):
    # Do something with the selected object #op
    ...

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

If I want to do something on the selected objects only (not the children), I use

for op in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE):
# Do something with the selected objects #op
...

Do I have to place it on top, before the definitions are done? Do I have to include everything or just where the key stuff starts?

Hey,

you must wrap most of the content of the main() function as you want to do stuff per object I assume. The only thing which can be done once is the EventAdd(). See below for an example. Please understand that my ability to help you to learn Python or programming in general, as for example loops, will be limited, as this is out of scope of support.

Cheers,
Ferdinand

"""Sets the current relative position x-component of the selected object as a keyframe at the current
time.

Must be run as a Script Manager script.
"""
import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: typing.Optional[c4d.BaseObject] # The active object, can be None.

# The major work in writing key frames in the way you want to do it, lies in precisely defining the 
# DescID for these parameters. I wrote this example only for keying the relative position x-component,
# but below I hint at how the remaining eight parameters must be addressed.

# The description level for the relative position, rotation, and scale parameter, we are going to
# reuse these when defining IDs for each component of them.
DLV_REL_POS: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0)
DLV_REL_ROT: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0)
DLV_REL_SCL: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_SCALE, c4d.DTYPE_VECTOR, 0)

# The description levels for the x, y, and z components of a vector parameter.
DLV_VEC_X: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
DLV_VEC_Y: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
DLV_VEC_Z: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)

# Define the IDs for the x, y, and z relative position parameters.
DID_POS_X: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_X)
DID_POS_Y: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Y)
DID_POS_Z: c4d.DescID = c4d.DescID(DLV_REL_POS, DLV_VEC_Z)

# Define the IDs for the x, y, and z relative rotation and scale parameters.
DID_ROT_X: c4d.DescID = c4d.DescID(DLV_REL_ROT, DLV_VEC_X)
# ...
DID_SCL_X: c4d.DescID = c4d.DescID(DLV_REL_SCL, DLV_VEC_X)
# ...

def main() -> None:
    """Runs the example.
    """
    # Iterate over all selected objects. It is better to not use #op as a loop variable to not 
    # shadow the module attribute of the same name. I only suggested that so that you could copy and
    # paste the code. I am using here #node instead, so #node is what was #op before in the other
    # script, an instance of a selected BaseObject. But here we not only operate on the primary one,
    # but all of them.
    for node in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN):
        # Find the track for the parameter we are interested in creating, here the track for x 
        # component of the relative position, or create a new track when there is none.
        track: c4d.CTrack = node.FindCTrack(DID_POS_X)
        if track is None:
            track = c4d.CTrack(node, DID_POS_X)
            if not track:
                raise MemoryError(f"{track = }")
            node.InsertTrackSorted(track)

        # Get the current time and the key in #track for this time. CCurve.AddKey will return an 
        # existing key if there is one, so we do not have to search for potentially existing keys 
        # first.
        t: c4d.BaseTime = doc.GetTime()
        curve: c4d.CCurve = track.GetCurve()
        keyData: dict = curve.AddKey(t)
        if keyData is None:
            raise RuntimeError("Could not add key frame.")

        # Set the value of the key to the current value of that parameter.
        key: c4d.CKey = keyData["key"]
        key.SetValue(curve, node[DID_POS_X])

        # Set some of the key parameters. We could also use the more low-level and precise 
        # C4DAtom.Get/SetParameter calls instead of GeListNode.__get/setitem__ calls (the bracket 
        # syntax) used below. But that is more typing work and there is no need to be more precise 
        # here.

        # Enable the "Lock Time" tick box for the key.
        key[c4d.ID_CKEY_LOCK_T] = True
        # Set "Fixed Weighted" as the key preset.
        key[c4d.ID_CKEY_PRESET] = c4d.ID_CKEY_PRESET_FIXED_OVERSHOOTWEIGHTED
        # Enable clamping for the key, just as in the Attribute Manager, this will implicitly change
        # the preset mode to "Custom" but we will maintain the changes made to other parameters when
        # we set it to "Fixed Weighted" before.
        key[c4d.ID_CKEY_CLAMP] = True 

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


if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Thank you for the example.

Yes, I know. Sorry again.
Maybe ask the developers why it's not possible to assign hotkeys for keying SRT/X/Y/Z separately in vanilla C4D. This is a basic feature.

I could show you how u could make C4D the best animation tool on earth in a 3 hours meeting. 🙂

Hey,

We in the SDK Group do not have the ability to deal with end user feature requests, it is only the end user support team which collects them. There might also be built-in animation features of Cinema 4D which do what you want to be done. We are primarily engineers and SDK experts here in the SDK Group, and not trainers and application experts. So, there is a good chance that user support knows tips and tricks I am not aware of, as I factually never use Cinema 4D to animate things in any substantial capacity.

You can reach end user support via our support portal.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hello @ferdinand,

This is the final script for PositionX.
I made it a bit shorter for making it more easy to adapt this to PosYZ/RotXYZ/SclXYZ.
And I added a possibility to undo the complete action (e.g. if there was already a key, which got overridden and the user wants to revert this). I am not sure, whether the undo commands are in the right place, but after some research I placed the AddUndo before the stuff begins and it works so far. For all selected objects.

Just wanted to post it, because maybe some else needs something like this.

"""Sets the current relative pos/rot/scl x/y/z-component of the selected object as a keyframe at the current
time.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: typing.Optional[c4d.BaseObject] # The active object, can be None.

# The description level for the relative position, rotation, and scale parameter, we are going to
# reuse these when defining IDs for each component of them.
# Possible:  ID_BASEOBJECT_REL_POSITION // ID_BASEOBJECT_REL_ROTATION // ID_BASEOBJECT_REL_SCALE
DLV_REL: c4d.DescLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0)

# The description levels for the x, y, and z components of a vector parameter.
# Possible: VECTOR_X // VECTOR_Y // VECTOR_Z
DLV_VEC: c4d.DescLevel = c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)

# Define the IDs for the x/y/z relative SRT parameters.
DID_SRT: c4d.DescID = c4d.DescID(DLV_REL, DLV_VEC)

PRINT_MESSAGE = "Added Key PosX"

def main() -> None:

    doc.StartUndo()
    for node in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE):
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
        # Find the track for the parameter we are interested in creating, here the track for x
        # component of the relative position, or create a new track when there is none.
        track: c4d.CTrack = node.FindCTrack(DID_SRT)
        if track is None:
            track = c4d.CTrack(node, DID_SRT)
            if not track:
                raise MemoryError(f"{track = }")
            node.InsertTrackSorted(track)

        # Get the current time and the key in #track for this time. CCurve.AddKey will return an
        # existing key if there is one, so we do not have to search for potentially existing keys
        # first.
        t: c4d.BaseTime = doc.GetTime()
        curve: c4d.CCurve = track.GetCurve()
        keyData: dict = curve.AddKey(t)
        if keyData is None:
            raise RuntimeError("Could not add key frame.")

        # Set the value of the key to the current value of that parameter.
        key: c4d.CKey = keyData["key"]
        key.SetValue(curve, node[DID_SRT])

        # Set different options for the key tangents
        key[c4d.ID_CKEY_PRESET] = c4d.ID_CKEY_PRESET_AUTO_OVERSHOOTWEIGHTED

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

    print (PRINT_MESSAGE)

if __name__ == "__main__":
    main()

Is there a documentation how to make a PlugIn of my 9 scripts?
I mean, atm I just saved them into ..\Maxon Cinema 4D 2023_0AF8E603\library\scripts

ae923af7-aded-4291-8f17-f46186067625-image.png

Maybe it's enough doing a nicely named subfolder there and to add the folder to search location. I don't know. I'll try it out. Never installed a plugin or had a look into a plugin structure yet.

Thank you very much again for your help, Sir!

Kind regards
Vannipo

Hello @vannipo ,
What you need may not a plugin, scripts is enough. In Command Manager, search your script's name, then assign the shortcut you like. Assign a shortcut key to each of your nine scripts, You can even drag the item out of the list tree view and turn it into a button.
Snipaste_2023-01-12_06-08-50.png

C4DJSON -- a useful module to load an dump c4d objects in python dict format (similar to standard python json module).
Examples on Notion.

Hey @vannipo,

Great to hear that you succeeded.

And just to be clear, we are here to answer your questions, that is part of the responsibility of the SDK group. I just have to sometimes moderate a bit the expectations of newcomers (and long timers) regarding what we can do (provide help along the way) and what not (provide full solutions, help with learning a language itself). So, there is no need to thank me every step of the way, we are happy to answer questions, as long as we see that users do not try to use us as a "write my script on demand"-service.

Regarding bringing your script to the next level, there are countless ways you could take, @iplai already pointed out the most straight forward one, assigning them all a hotkey or placing them in a palette (you can directly drag and drop the script cion from the script manager to do that). But I would assume that you are aware of that and rather want to consolidate your nine scripts into less scripts, i.e., bring down the number of hotkeys you require.

  • First of all, it is not possible to write a plugin or script that consolidates multiple hotkeys. So, you cannot have a plugin which reacts to all variations of CTRL + C, as for example CTRL + C + A, SHIFT + CTRL + C, etc., being pressed.
  • If you want a 'real' plugin, you could implement a CommandData plugin with a GeDialog as an interface and thereby implement your own little GUI which determines what happens when you invoke the plugin, press its assigned shortcut. The GUI could then have a dropdown where you can select the transformation mode ("position", "rotation", "or "scale") and a dropdown where one can select the component ("x", "y", "z") and when the user has set them to "position" and "z", every invocation of the plugin would set a "position.z" keyframe.
  • Another way to reduce the number of shortcuts would be to use a popup menu as a very minmal GUI. You could do this both in a plugin and a script. Find a small example below.

Cheers,
Ferdinand

Result
setkey.gif

Code

"""Sets a key for one of the relative position components for all selected objects.

The component which is being set is being determined with a popup menu. This example must be run 
from the script manager.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: typing.Optional[c4d.BaseObject] # The active object, can be None.

# The parameter IDs this script is targeting.
DID_POS_X: c4d.DescID = c4d.DescID(
    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0), 
    c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0))
DID_POS_Y: c4d.DescID = c4d.DescID(
    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0), 
    c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0))
DID_POS_Z: c4d.DescID = c4d.DescID(
    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0), 
    c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0))

# The popup menu container for selecting the component which should be written, x, y, or z. I am using here
# the string mini language to embed an icon in the name of an item, so 'X Channel&i12153&' means for 
# example, the label "X Channel" and the icon with the ID 12153. The mini language is documented under 
# ShowPopupDialog(). The icons I am using here are documented under:
#   https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d.bitmaps/RESOURCEIMAGE.html
POPUP_MENU: c4d.BaseContainer = c4d.BaseContainer()
POPUP_MENU.InsData(c4d.VECTOR_X, 'X Channel&i12153&')
POPUP_MENU.InsData(c4d.VECTOR_Y, 'Y Channel&i12154&')
POPUP_MENU.InsData(c4d.VECTOR_Z, 'Z Channel&i12155&')

def main() -> None:
    """Runs the example.
    """
    # Do not run the script at all when there is not object selected.
    selectedObjects: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
    if len(selectedObjects) < 1:
        return
    
    # Determine which parameter (ID) to target with a little popup menu.
    targetParameter: typing.Optional[c4d.DescID] = None
    result: int = c4d.gui.ShowPopupDialog(cd=None, bc=POPUP_MENU, x=c4d.MOUSEPOS, y=c4d.MOUSEPOS,
                                          flags=c4d.POPUP_CENTERHORIZ|c4d.POPUP_CENTERHORIZ)
    if result == c4d.VECTOR_X:
        targetParameter = DID_POS_X
    elif result == c4d.VECTOR_Y:
        targetParameter = DID_POS_Y
    elif result == c4d.VECTOR_Z:
        targetParameter = DID_POS_Z

    # The user aborted the popup menu.
    if targetParameter is None:
        return

    # Your code ...
    doc.StartUndo()
    for node in selectedObjects:
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
        track: c4d.CTrack = node.FindCTrack(targetParameter)
        if track is None:
            track = c4d.CTrack(node, targetParameter)
            if not track:
                raise MemoryError(f"{track = }")
            node.InsertTrackSorted(track)

        t: c4d.BaseTime = doc.GetTime()
        curve: c4d.CCurve = track.GetCurve()
        keyData: dict = curve.AddKey(t)
        if keyData is None:
            raise RuntimeError("Could not add key frame.")

        key: c4d.CKey = keyData["key"]
        key.SetValue(curve, node[targetParameter])
        key[c4d.ID_CKEY_PRESET] = c4d.ID_CKEY_PRESET_AUTO_OVERSHOOTWEIGHTED

    doc.EndUndo()
    c4d.EventAdd()

if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand @iplai ,

Yes, sure, I already assigned hotkeys to my 9 scripts and made a folded palette and rendered 9 super sweet icons 🙂
I was very disappointed as I found out, C4D does not support Numpad keys. So, the whole numpad keys are not useable. I mean, I can use them, but then I have to find new hotkeys for the commands I already use on the usual keys 1-9. Very sad 😞

The most important point for an animator is to key the desired channel as fast as possible. I just got used to use the numpad keys 1-3 for position, 4-6 for rotation, 7-9 for scaling in other 3D tools. They perfectly ordered for those commands.

But, the pop up idea is very interesting and I will have a look. Concerning the GUI... yes, I already saw something like this. I think the GUI builder is inside C4D, isn't it? I am not sure, atm.

Btw, is it possible to set a "real name" for a script? atm, everywhere it just shows the filename.

1de9c4f0-7e2d-4b15-9952-775b08a39bf7-image.png

Cheers,
Vannipo

Hey @vannipo,

GUI builder is inside C4D, isn't it? I am not sure, atm.

There is no WYSIWYG GUI builder for dialogs in Cinema 4D anymore. A long time ago existed the classic API Resource Editor, but it is not published anymore by us. The Resource Editor you can find via CTRL + C in a modern Cinema 4D instance is an editor for maxon API resources, a different and newer GUI paradigm in Cinema 4D.

When you want to implement a dialog, you will have to either write a resource file manually or use the methods of GeDialog to add elements at runtime, you could have a look at this posting where I recently lined out some basics. The Python GUI Manual is quite superficial at the moment, but we have some simple examples on GitHub. For learning dialog resource markup, I would recommend the Dialog Resource Manual.

The most important point for an animator is to key the desired channel as fast as possible. [...] But, the pop-up idea is very interesting, and I will have a look [...]

Yeah, I understood that you were after a very streamlined setup. When you want to make zero compromises, nine shortcuts are probably the best solution. The solution I proposed with the popup menu is a compromise of the number of shortcut keys to allocate and the speed with which keys can be generated. The advantage is here that the popup menu will always open under your mouse cursor no matter where it is, which will minimize mouse travel distances. But when you overpopulate the menu with all nine entries (you can also add separators if you want to), selecting the right item will probably become slow. So, if you want a good compromise, you could use three scripts (translate/rotate/scale) which each provide a popup for x, y, or z.

Btw, is it possible to set a "real name" for a script? atm, everywhere it just shows the filename.

You can supply a different name from its filename by adding file docstring containing Name-en-US: XXXX where XXX is the name of the plugin. This can also be done for description that will be displayed when you hover the command with Description-en-US: XXX. If you need to support other language please refer to Plugin Structure Manual which specify the different language code. Finally you can find an example within GitHub - script_custom_name_description.py

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net