TagData plugin undo issues
-
When I need to use a class attribute as a cache in a Tag plugin, if I do an undo, the entire Tag plugin reinitializes causing my cached data to be lost!
When I use global variables to cache data, it will conflict if there are multiple tags in the scene!
How should I cache data in a tag without it being initialized on undo?
class MyBase(object): def __init__(self): self.mycache = None class MyTag(plugins.TagData): def Init(self, node, isCloneInit): self.MB = MyBase()
-
Hello @moghurt,
Thank you for reaching out to us. You cannot prevent nodes being reinitialized, this is a key-aspect of the Cinema 4D API. You have three options here:
- Store data in the data container of the node. This is the intended and recommended way, as it will also work over serialization and deserialization boundaries.
- Store data in a global hash map where nodes are the keys, this will not work over serialization and deserialization boundaries (by default).
- Implement the whole custom data serialization and deserialization chain of NodeData, i.e.,
NodeData.Read()
,NodeData.Write()
, andNodeData.CopyTo()
. This will work over serialization and deserialization boundaries, but is quite a bit of work. It is basically the same as 1. (on an abstract level), just more complicated and with more control.
The advantage of (1) and (3) is that they work over serialization and deserialization boundaries, i.e., your data will be saved and restored with a scene file. The disadvantage of (1) and (3) is that you are bound to what can be serialized into a
BaseContainer
, i.e., when you want to serialize non-atomic data, you must decompose your type into them in a custom serialization and deserialization routine. The advantage of (2) is that you can store arbitrary data, but the disadvantage is that it will not be saved with the scene file (you can however realize copying data which was the focus of your question).Option two is the least costly option for your case. But it would be better to store data in the node itself. Writing something that transforms an instance of your
MyData
in and out of aBaseContainer
is not that hard, the real problem is then that you also have to synchronize the data between the data container of the node. This is doable but adds another layer of complexity.On an even larger scope I would recommend abandoning the idea of storing data of your
NodeData
plugin in a separate entity. If you really want an abstraction layer, you can still write functions or a separate class which do the data manipulation for you. But the data should be directly stored in the data container of the node itself. By removing this 'in-between' state of data temporarily being stored inMyData
before finally being written into the data container of the node, you would remove the necessity of synchronization.Last but not least, you could just store things as a class attribute (as suggested by your posting). But I think you just misspoke here, and meant instance attribute, as the rest of your posting suggests that you want to store data per node and not globally for that one plugin class.
Cheers,
FerdinandPS: You could technically also just implement
NodeData.CopyTo
to move data from one instance to another when nodes are copied. We have the warning/rule that you should always implementRead
,Write
, andCopyTo
all together. At your own risk, you could ignore this here, it should be without consequences in this case. By not implementingRead
andWrite
you would loose serialization and deserialization, but inCopyTo
you can literally copy from one instance ofMyPlugin
to another, i.e., you would have to pack your data into aBaseContainer
/HyperFile
.Code
Example for case (2):
import c4d # Teh custom data to store. class MyData: def __init__(self): self.data = "Hello World" # This is a global hash map where we store data per node. Hashing nodes themselves is a 2023.2 feature, # before that you would have to do it manually via the MAXON_CREATOR_ID UUID of a node. MY_TAG_PLUGIN_DATA: dict[c4d.BaseTag, MyData] = {} class MyTag(plugins.TagData): """The plugin which holds instances of MyData per node. """ def Init(self, node: c4d.BaseTag, isCloneInit: bool) -> bool: """Load the data into the node. """ self.InitAttr(node, float, c4d.MY_FLOAT_PARAMETER) self.InitAttr(node, int, c4d.MY_INT_PARAMETER) if not isCloneInit: node[c4d.MY_DTYPE_REAL_PARAMETER] = 200.0 node[c4d.MY_DTYPE_LONG_PARAMETER] = 5 # We load here the data from the global hash map. The underlying knowledge is that when we # insert or poll a hash map with our node as a key, we call with that GeListNode.__hash__ # which hashes the node over its MAXON_CREATOR_ID UUID. It is more or less the same as if # we would do this: # # uuid: bytes = bytes(node.FindUniqueID(c4d.MAXON_CREATOR_ID) or None) # data: "MyData" = MY_TAG_PLUGIN_DATA.get(uuid, None) # # And because the UUID of a node is persistent over reallocations, we can use it as a key # to identify the nodes which have been reintialized. UUIDs are even persistent over # serialization and deserialization, so one can also use this to store data over these # boundaries when there is a separate mechanism to serialize and deserialize # MY_TAG_PLUGIN_DATA. But it is usually easier to then just store the data in the node # itself. data: "MyData" = MY_TAG_PLUGIN_DATA.get(node, None) if data is None: MY_TAG_PLUGIN_DATA[node] = MyData() data = MY_TAG_PLUGIN_DATA[node] self._data: "MyData" = data return True
-
@ferdinand
Hi ferdinand,
Thank you for your response; it has been very helpful!I have implemented different instances for different nodes as suggested. However, I am encountering an issue when undoing actions. My plugin's cached data, which is stored in instance attributes, gets reinitialized upon undo. I would prefer to use a data container to manage this cached data, but I have no experience with data containers.
Could you provide a simple demo code or direct me to an existing related post?
Thank you!
-
Hey @moghurt,
I am not quite sure what you mean by 'different instances for different nodes'? What I described under point 1? In any case, you must share your code with us, as we will otherwise not be able to help you.
Cheers,
Ferdinand -
Hi @ferdinand ,
As per your suggestion I have stored the data in containers and implemented caching, I'm very sorry that I may not have described it clearly enough but the problem is now solved!Thanks again for your help, cheers!
-
No need to be sorry, a good to hear that you found your solution!