Hi @mocoloco,
I still don't get why I have to write c4d.mAXIS_X
instead of mAXIS_X
You do not have to but it is convenient. An ID ist just a number and integer symbols like c4d.ID_USERDATA
are just aliases for such number. 700
and ID_USERDATA
do the same thing. When you provide a resource file, here one for your tag, Cinema 4D automatically exposes all resource symbols in the c4d
namespace. You can also just redefine that symbol yourself, it is all the same. But it is extra work 
# These are all the same.
bobIsYourUncle: int = 1550
mMoverTag[c4d.mAXIS_X] = 1.0
mMoverTag[1550] = 1.0
mMoverTag[bobIsYourUncle] = 1.0
No more error, but no CTrack created, at list it does not appears in the Dope Sheet; so I should still makes something wrong somewhere. Does my pointing is now correct or still wrong for my DescID?
Your DescID
is correct, the problem is that you never add the track to your tag, you never call tag.InsertTrackSorted(xTrack)
. But you should not do this in TagData.Execute
, as lined out below.
First, as I was stuck on getting what I tried on Execute()
, which was the place where I would like to set the CTrack
, I indeed tried to get it set in the mMoverTagData.Init()
- which I agree, is maybe not the right way to do.
I never intended to imply that you should use TagData.Execute
. You cannot add or remove elements from a scene as for example objects, tags, tracks or points in this method. Because you are outside of the main thread in Execute
and bound to the threading restrictions. Init
would be sort of okay, but a node has not yet been attached to a document when it is called, which probably would complicate things.
But I think I understand what you want to do generally. Although I still do not understand why you want to create a track on your mAXIS_X
parameter itself, which then leads to weird feedback loops when the parameter is used to both define the values of a track but is also driven by it.
The other problem is that what you want to do in general, modify a scene from a NodeData plugin is not the greatest design pattern. Doing this will often lead to trouble in Cinema 4D. It is okay to do this in the form of a button and when the user clicks the button, something is added or removed. But in the dynamic fashion you want to do it, this is not a good path. If you want to dynamically generate animations based on a set of parameters, you should implement a CommandData plugin instead. |
I have provided nonetheless an example for both driving an x-position animation of an object hosted by a plugin tag with tracks, as well as driving the same animation procedurally. It is only the latter which is intended to be done with tags. If you want to to do this, you must offload your scene modifications to NodeData.Message
, as this method is commonly called on the main thread. But you must still check for being there.
Cheers,
Ferdinand
Plugin: pc14315.zip
Result: I am first driving the animation of the cube procedurally and then in the second half with tracks. In both cases the x-axis position of the object hosting the tag is driven by the parameters in the tag.

Code:
"""Implements a tag which drives the position of a host object procedurally or via tracks.
The tag implements two modes, a "Procedural" and a "Track" mode. In procedural mode, the tag does
what tags are supposed to do, it drives procedurally parameters of the scene, here the x-axis
position of its host object. In track mode, it generates the same animation for its host object,
but uses a key framed animation instead which is generated every time the user interacts with the
GUI.
Note:
It is not recommended to use NodeData plugins to add or remove elements (e.g., objects,
materials, tags, shaders, tracks, keys, points, etc.) from a scene in the manner shown here.
It is okay to do this based on non-animateable parameters, e.g., a button. But as this is based
on a set of animateable parameters #P, one can run into problems. This can happen when the
document is rendered and #P is also animated. The only safe way to add and remove elements are
therefore buttons in NodeData plugins.
Respected must be in any case the threading restrictions of TagData.Execute, it is never allowed
to add or remove scene elements from there. This solution will not crash, but is bound to the
main thread which can cause the plugin to (intentionally) stop operating in the mentioned
scenario.
"""
import c4d
class mMoverTagData(c4d.plugins.TagData):
"""Implements a tag which drives the position of a host object procedurally or via tracks.
"""
# The plugin ID and a precise definition of the relative x-axis position parameter of an object.
ID_PLUGIN: int = 1059303
DID_OBJECT_REL_POSITION_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))
def Init(self, node: c4d.GeListNode) -> bool:
"""Called to initialize a mMover tag instance.
Args:
node: The BaseTag instance representing this plugin object.
"""
self.InitAttr(node, int, c4d.ID_MMOVER_MODE)
self.InitAttr(node, c4d.BaseTime, c4d.ID_MMOVER_ANIMATION_MIN)
self.InitAttr(node, c4d.BaseTime, c4d.ID_MMOVER_ANIMATION_MAX)
self.InitAttr(node, float, c4d.ID_MMOVER_AXIS_X_MIN)
self.InitAttr(node, float, c4d.ID_MMOVER_AXIS_X_MAX)
node[c4d.ID_MMOVER_MODE] = c4d.ID_MMOVER_MODE_PROCEDURAL
node[c4d.ID_MMOVER_ANIMATION_MIN] = c4d.BaseTime(0)
node[c4d.ID_MMOVER_ANIMATION_MAX] = c4d.BaseTime(1)
node[c4d.ID_MMOVER_AXIS_X_MIN] = 0.
node[c4d.ID_MMOVER_AXIS_X_MAX] = 1000.
return True
def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
"""Called when expressions are evaluated to let a tag modify a scene.
It is important to understand that tags are also bound by threading restrictions in this
method, we are here NOT on the main thread. I.e., we cannot add or remove nodes from the
scene as for example tracks. All we can do, is change parameters or the values of other
data. We can for example change the point positions of a point object, but we cannot add
or remove points.
Args:
tag: The BaseTag instance representing this plugin object.
op: The BaseObject hosting the tag.
doc: The document both the tag and object are attached to.
"""
# Bail when the tag is not in procedural mode.
if tag[c4d.ID_MMOVER_MODE] != c4d.ID_MMOVER_MODE_PROCEDURAL:
return c4d.EXECUTIONRESULT_OK
# Set the x axis position of the tag host for the current time.
mg: c4d.Matrix = op.GetMg()
v: float = mMoverTagData._GetMoveOffset(tag, doc.GetTime())
mg.off = c4d.Vector(v, mg.off.y, mg.off.z)
op.SetMg(mg)
return c4d.EXECUTIONRESULT_OK
def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool:
"""Called to signal events to the tag.
Used in this case to react to parameter changes, inducing track modifications on the host
object of the tag when the tag is in "Track" mode.
Args:
node: The BaseTag instance representing this plugin object.
mid: The ID of the raised event.
data: The data associated with the event.
"""
# Bail when the tag is not attached to a document or an object as we will need both.
doc: c4d.documents.BaseDocument = node.GetDocument()
obj: c4d.BaseObject = node.GetObject()
if None in (doc, obj):
return True
# Bail when this is not an event for a parameter value being changed.
if mid != c4d.MSG_DESCRIPTION_POSTSETPARAMETER:
return True
# Get the DescID of the parameter which has changed and get the ID of the first desc level.
pid: int = data["descid"][0].id
# Bail when the mode of the tag is "Procedural" and it was not the mode which has changed.
isProceduralMode: bool = node[c4d.ID_MMOVER_MODE] == c4d.ID_MMOVER_MODE_PROCEDURAL
if pid != c4d.ID_MMOVER_MODE and isProceduralMode:
return True
# Bail when this is not being called on the main thread, as all modifications we intend to
# do below require use being on the main thread (adding or removing tracks or keys).
if not c4d.threading.GeIsMainThreadAndNoDrawThread():
print ("Warning: Failed to execute 'mMover' tag logic due to threading restrictions.")
return True
# The mode switched from "Track" to "Procedural", we attempt to remove an existing track.
if pid == c4d.ID_MMOVER_MODE and isProceduralMode:
mMoverTagData._RemoveTrack(obj)
# The user either turned on track mode or changed one of the parameters while being in
# track mode, we add or modify the track.
else:
mMoverTagData._AddOrModifyTrack(obj, node)
return True
# --- Custom Methods ---------------------------------------------------------------------------
@staticmethod
def _GetMoveOffset(tag: c4d.BaseTag, t: c4d.BaseTime) -> float:
"""Computes a new x-axis offset based on the data in #tag and the given time #t.
This is a custom method of this plugin and not part of the TagData interface.
Args:
tag: The tag to compute the offset for.
t: The time at which to compute the offset.
"""
# Bail when the tag is not attached to a document.
doc: c4d.documents.BaseDocument = tag.GetDocument()
if not doc:
raise RuntimeError(f"{tag} is not attached to a document.")
# Get the min, max, and current frame for the animation described by the tag data.
fps: int = doc.GetFps()
minFrame: int = tag[c4d.ID_MMOVER_ANIMATION_MIN].GetFrame(fps)
maxFrame: int = tag[c4d.ID_MMOVER_ANIMATION_MAX].GetFrame(fps)
currentFrame: int = t.GetFrame(fps)
# Get the min and max offset values and compute with them and the time values the offset for
# the given time #t.
minValue: float = tag[c4d.ID_MMOVER_AXIS_X_MIN]
maxValue: float = tag[c4d.ID_MMOVER_AXIS_X_MAX]
v: float = c4d.utils.RangeMap(currentFrame, minFrame, maxFrame, minValue, maxValue, True)
return v
@staticmethod
def _AddOrModifyTrack(obj: c4d.BaseObject, tag: c4d.BaseTag) -> None:
"""Adds or modifies the track for a relative x-position animation on #obj.
This is a custom method of this plugin and not part of the TagData interface.
Args:
obj: The object to add or modify the track for.
tag: The mMover tag carrying the data.
"""
# Attempt to retrieve an existing track or create a new one.
track: c4d.CTrack = obj.FindCTrack(mMoverTagData.DID_OBJECT_REL_POSITION_X)
if track is None:
track = c4d.CTrack(obj, mMoverTagData.DID_OBJECT_REL_POSITION_X)
obj.InsertTrackSorted(track)
# Flush the keys of the curve associated with the track in case we are dealing with an
# already existing track.
curve: c4d.CCurve = track.GetCurve()
curve.FlushKeys()
# Get the time and animation values and write them into two keys.
minTime: c4d.BaseTime = tag[c4d.ID_MMOVER_ANIMATION_MIN]
maxTime: c4d.BaseTime = tag[c4d.ID_MMOVER_ANIMATION_MAX]
minValue: float = mMoverTagData._GetMoveOffset(tag, minTime)
maxValue: float = mMoverTagData._GetMoveOffset(tag, maxTime)
for time, value in ((minTime, minValue), (maxTime, maxValue)):
key: c4d.CKey = curve.AddKey(time)["key"]
if key:
key.SetValue(curve, value)
@staticmethod
def _RemoveTrack(obj: c4d.BaseObject) -> None:
"""Removes the track for a relative x-position animation on #obj.
This is a custom method of this plugin and not part of the TagData interface.
Args:
obj: The object to attempt to remove the track from.
"""
track: c4d.CTrack = obj.FindCTrack(mMoverTagData.DID_OBJECT_REL_POSITION_X)
if track:
track.Remove()
def RegisterPlugins() -> None:
"""Registers the plugins contained in this file.
"""
if not c4d.plugins.RegisterTagPlugin(
id=mMoverTagData.ID_PLUGIN,
str="mMover Tag",
info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE,
g=mMoverTagData, description="tmmover",
icon=c4d.bitmaps.InitResourceBitmap(c4d.ID_MODELING_MOVE)):
print("Warning: Failed to register 'mMover' tag plugin.")
if __name__ == "__main__":
RegisterPlugins()