Solved Difficulties to dedect the right Message Data

Hi ,

I have a small Object Plugin which is reading data from a Measure&Contruct Object.

So a small example:
The plugin has a Baselist Link where you can insert a Measure&Construct-Object.
And it is reading the positions of point1, point2 and point3 from this object.

Then internally it creates splines and objects depending on this positions and so forth.

I set the cache optimization to True with self.SetOptimizeCache(True) in my init() method.
When the user now changes the measurement and distances of the Measure Object. The plugin instance does not update. Obvious because it is not part of the description.

Question:
How can I dedect or catch the right Message Data from the Measure Object to force my plugin to react on changes and to update?

So first I tried to catch a global message from the viewport which i readed out with print() and wrote the last points-position to a member-variable and if the user changed the distance of the Measure-Object he catched a message and compares the actual positions with the previos positions and if they differ I sent a node.Message(c4d.MSG_CHANGE). And wright the new position to the variable.
But this just worked for origin point of the Measure Object but not for point2 and point3.

Otherwise the user has to press "A" Key on the Keyboard to refresh the Editor View or I implement a Refresh Button.
But it's tiresome for users to keep hitting refresh or A all the time.

Thanks and best regards
Thomas

Cheers
Tom

Hey @ThomasB,

Thank you for reaching out to us. I have split your question into two sections:

Detecting Change Counts

One part of your question is how to determine how often a scene element has changed. Change counters, or how Cinema 4D calls them, dirty states, are built into the base class of all scene elements, c4d.C4DAtom. So, when you have a node myNode (a BaseObject, BaseShader, etc.), you would retrieve its current data container change count with cnt: int = myNode.GetDirty(c4d. DIRTYFLAGS_DATA). cnt would denote how often the data container of myNode has changed since it has been instantiated, i.e., or how often the parameter values of myNode changed. There are also other categories of dirtiness, I would recommend having a look at the docs.

Detecting Change Events

The other part of your question is to detect when a change event occurred. And while core messages can be a solution (by manually making objects dirty which should update), we want to avoid using such an overly complex solution when we can avoid that [1].

First, you should turn off SetOptimizeCache as it is just Python-exclusive convenance method which is targeted at beginners. Cinema 4D will now call your GetVirtualObjects quite often, and your plugin "will work" but you are also calculating the cache of your plugin more often than necessary. The rounded_tube example highlights the basic implementation of determining the dirtiness of your node yourself:

def GetVirtualObjects(self, op: c4d.BaseObject, hh: "HierarchyHelp") -> c4d.BaseObject:
    """
    """
    # Determine if #op, the node that is representing this plugin hook instance, is dirty,
    # i.e., has changed and therefore must be rebuilt.
    dirty: bool = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA)
    if not dirty: 
        return op.GetCache(hh) # The node is not dirty we simply return its cache.
    
    # Compute and return a new cache.
    return c4d.BaseObject(c4d.Ocube)

Usually, doing this will be enough, as Cinema 4D will call GetVirtualObjects quite often, but you can get event starved. For example, Cinema 4D will not try to update the cache of your object when the user pans with the camera around it. Modifying a BasaeList2D which has been linked in the description of your object will invoke a cache building attempt out of the box.

Your Concrete Case

You did not provide any code here, so I can only speculate, but in essence, it will look something like this:

import c4d

class MyPlugin (c4d.plugins.ObjectData):

    def __init__(self) -> None:
        """
        """
        # We initialize two fields to track the linked node via its UUID and the last known (data)
        # dirty count of that linked node. We could also overwrite SetDParameter to track changes
        # to our link parameter, then we would not have to track the UUID.
        self._linkDirtyCountCache: int = c4d.NOTOK
        self._linkUuidCache: bytes | None = None
        super().__init__()

    def GetVirtualObjects(self, op: c4d.BaseObject, hh: "HierarchyHelp") -> c4d.BaseObject:
        """
        """
        # Try to get the linked object or bail when there is None.
        link: c4d.BaseList2D | None = op.GetParameter(c4d.DescID(c4d.ID_MYPLUGIN_LINK), 
                                                    c4d.DESCFLAGS_GET_NONE)
        if link is None:
            return c4d.BaseObject(c4d.Onull)
        
        # Determine if the node itself is dirty. This will probably already suffice alone, because a 
        # link field should make its host data dirty once the linked node changed.
        selfDirty: bool = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA)

        # Or we compute the dirtiness of #link ourselves. There are two ways how we can do this, for 
        # once also with IsDirty (which can fail when there are also other interacting entities)
        linkDirty: bool = link.IsDirty(c4d.DIRTYFLAGS_DATA)

        # ... or by manually managing the dirty counter of the linked object.
        dirtyCnt: int = link.GetDirty(c4d.DIRTYFLAGS_DATA)
        uuid: bytes = bytes(link.FindUniqueID(c4d.MAXON_CREATOR_ID))

        # First check if the linked object is still the same as the last time we built the cache.
        if uuid != self._linkUuidCache:
            linkDirty = True
            self._linkUuidCache = uuid
            self._linkDirtyCache = dirtyCnt
        # Now compare the dirty count ...
        elif dirtyCnt > self._linkDirtyCountCache:
            linkDirty = True
            self._linkDirtyCache = dirtyCnt

        # Make use of the computed dirty states.
        if not any((selfDirty, linkDirty)): 
            return op.GetCache(hh)
        
        # Compute and return a new cache.
        return c4d.BaseObject(c4d.Ocube)

Cheers,
Ferdinand

[1] The node message stream you were trying to use is useless in this case because node messages are sealed, i.e., you cannot peek into the node message stream of your measure object.

MAXON SDK Specialist
developers.maxon.net

Hey @ThomasB,

Thank you for reaching out to us. I have split your question into two sections:

Detecting Change Counts

One part of your question is how to determine how often a scene element has changed. Change counters, or how Cinema 4D calls them, dirty states, are built into the base class of all scene elements, c4d.C4DAtom. So, when you have a node myNode (a BaseObject, BaseShader, etc.), you would retrieve its current data container change count with cnt: int = myNode.GetDirty(c4d. DIRTYFLAGS_DATA). cnt would denote how often the data container of myNode has changed since it has been instantiated, i.e., or how often the parameter values of myNode changed. There are also other categories of dirtiness, I would recommend having a look at the docs.

Detecting Change Events

The other part of your question is to detect when a change event occurred. And while core messages can be a solution (by manually making objects dirty which should update), we want to avoid using such an overly complex solution when we can avoid that [1].

First, you should turn off SetOptimizeCache as it is just Python-exclusive convenance method which is targeted at beginners. Cinema 4D will now call your GetVirtualObjects quite often, and your plugin "will work" but you are also calculating the cache of your plugin more often than necessary. The rounded_tube example highlights the basic implementation of determining the dirtiness of your node yourself:

def GetVirtualObjects(self, op: c4d.BaseObject, hh: "HierarchyHelp") -> c4d.BaseObject:
    """
    """
    # Determine if #op, the node that is representing this plugin hook instance, is dirty,
    # i.e., has changed and therefore must be rebuilt.
    dirty: bool = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA)
    if not dirty: 
        return op.GetCache(hh) # The node is not dirty we simply return its cache.
    
    # Compute and return a new cache.
    return c4d.BaseObject(c4d.Ocube)

Usually, doing this will be enough, as Cinema 4D will call GetVirtualObjects quite often, but you can get event starved. For example, Cinema 4D will not try to update the cache of your object when the user pans with the camera around it. Modifying a BasaeList2D which has been linked in the description of your object will invoke a cache building attempt out of the box.

Your Concrete Case

You did not provide any code here, so I can only speculate, but in essence, it will look something like this:

import c4d

class MyPlugin (c4d.plugins.ObjectData):

    def __init__(self) -> None:
        """
        """
        # We initialize two fields to track the linked node via its UUID and the last known (data)
        # dirty count of that linked node. We could also overwrite SetDParameter to track changes
        # to our link parameter, then we would not have to track the UUID.
        self._linkDirtyCountCache: int = c4d.NOTOK
        self._linkUuidCache: bytes | None = None
        super().__init__()

    def GetVirtualObjects(self, op: c4d.BaseObject, hh: "HierarchyHelp") -> c4d.BaseObject:
        """
        """
        # Try to get the linked object or bail when there is None.
        link: c4d.BaseList2D | None = op.GetParameter(c4d.DescID(c4d.ID_MYPLUGIN_LINK), 
                                                    c4d.DESCFLAGS_GET_NONE)
        if link is None:
            return c4d.BaseObject(c4d.Onull)
        
        # Determine if the node itself is dirty. This will probably already suffice alone, because a 
        # link field should make its host data dirty once the linked node changed.
        selfDirty: bool = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA)

        # Or we compute the dirtiness of #link ourselves. There are two ways how we can do this, for 
        # once also with IsDirty (which can fail when there are also other interacting entities)
        linkDirty: bool = link.IsDirty(c4d.DIRTYFLAGS_DATA)

        # ... or by manually managing the dirty counter of the linked object.
        dirtyCnt: int = link.GetDirty(c4d.DIRTYFLAGS_DATA)
        uuid: bytes = bytes(link.FindUniqueID(c4d.MAXON_CREATOR_ID))

        # First check if the linked object is still the same as the last time we built the cache.
        if uuid != self._linkUuidCache:
            linkDirty = True
            self._linkUuidCache = uuid
            self._linkDirtyCache = dirtyCnt
        # Now compare the dirty count ...
        elif dirtyCnt > self._linkDirtyCountCache:
            linkDirty = True
            self._linkDirtyCache = dirtyCnt

        # Make use of the computed dirty states.
        if not any((selfDirty, linkDirty)): 
            return op.GetCache(hh)
        
        # Compute and return a new cache.
        return c4d.BaseObject(c4d.Ocube)

Cheers,
Ferdinand

[1] The node message stream you were trying to use is useless in this case because node messages are sealed, i.e., you cannot peek into the node message stream of your measure object.

MAXON SDK Specialist
developers.maxon.net

@ferdinand
Many thanks for this detailed example, I'll try it out

P.S.
Oh my gosh, it's working perfectly. I decided to go the easiest way, just to check the dirtyness of the linked object.

linkDirty: bool = link.IsDirty(c4d.DIRTYFLAGS_DATA)

Many many thanks

Thomas

Cheers
Tom