ObjectData plugin. How to store class object data?
-
Hello!
I playing with GetDDescription function of ObjectData plugin and founded out that init() function of main ObjectData class executes alwais when change UI controls:class MyPluginData(c4d.plugins.ObjectData): """SomeObject Generator""" def __init__(self): print('Init') self.parameters = dict() # alwais redefines instead to be declared once
Due this issue i can't store my parameters in a dict and GetDDescription function continuesly rewrite it. You can see It happens with this SDK example plugin.
So how to store class variables alwais stable? -
Hello @mikeudin,
Thank you for reaching out to us. The behavior you encounter is caused by the Asset API when it is creating and freeing dummy nodes. With
S26
, we added this warning to NodeData.Init:The parameters of a node must be initialized in this method. NodeData::Init is being called frequently due to nodes being reallocated in the background by Cinema 4D. One should therefore be careful with carrying out computationally complex tasks in this function.
We are aware that this is an ongoing issue for third parties, and with the next major release, the signature of
NodeData::Init
will change to (will also be reflected in Python):virtual Bool Init (GeListNode * node, Bool isCloneInit);
I.e., there will then be the
isCloneInit
argument indicating such dummy initalizations. We recently talked here about the subject of dummy allocations in more detail.- As of
2023.2
it is not possible to distinguish dummy inits from 'real' inits (without getting creative as I did in the thread). - Dummy nodes being allocated does not mean that the real node has been deallocated.
- To
object.__init__
applies the same as toNodeData.Init
, I would also avoid using__init__
when possible inNodeData
for clarity reasons.
Your Problem at Hand
It is a bit ambiguous what your exact problem is.
- First of all, you should always try to store information in the data container of a node rather than on the node implementation instance, because you avoid a lot of problems in doing so, as data is then automatically serialized and deserialized with the node.
- If there is no way on earth to press your data into a
BaseContainer
form, you must overwriteNodeData.Read
,.Write
, and .CopyTo. You must always implement all three, but the third one is here the interesting one, as itsdest
argument will be theNodeData
instance of the destination node, so you simply write your.parameters
directly from the source to the target. For disk serialization you still have to implementRead/Write
. - There are also patterns such as caching data over UUIDs or parameter sets to cross reallocation boundaries or parameter sets. It depends a bit on what you want to do.
I have sketched out multiple techniques in the example tag plugin shown below.
Cheers,
FerdinandPlugin: pc14682.zip
Result for instantiating the tag and interacting with the tag (clicking paramaters) a couple of times:
ExampleTagData.Init: Created the data '[3, 7, 4, 6, 9]'. // The actual tag ExampleTagData.Init: Created the data '[9, 8, 6]'. // Dummy tag ExampleTagData.Execute: 3, 7, 4, 6, 9 ExampleTagData.Init: Restored the data '[3, 7, 4, 6, 9]'. ExampleTagData.CopyTo: Copied data from `<__main__.ExampleTagData object at 0x0000024554DDF710>` to <__main__.ExampleTagData object at 0x0000024557AD8150>. ExampleTagData.Init: Created the data '[0, 6, 7]'. // Dummy tag ExampleTagData.Execute: 3, 7, 4, 6, 9 ExampleTagData.Init: Restored the data '[3, 7, 4, 6, 9]'. ExampleTagData.CopyTo: Copied data from `<__main__.ExampleTagData object at 0x0000024554DDF710>` to <__main__.ExampleTagData object at 0x000002455304E550>. ExampleTagData.Init: Created the data '[4, 5, 2]'. // Dummy tag ExampleTagData.Execute: 3, 7, 4, 6, 9 ExampleTagData.Init: Created the data '[4, 7, 2, 1]'. // Dummy tag ExampleTagData.Execute: 3, 7, 4, 6, 9
Code:
"""Implements a tag which handles manually stored data. This demonstrates three patterns: * The builtin NodeData.Read/Write/CopyTo to store custom data over scene loading and node copying boundaries. * Caching data within a running Cinema 4D instance to share data over reallocation boundaries or similar conditions. * Just store things in the data container. """ import c4d import random class ExampleTagData(c4d.plugins.TagData): """Implements a tag which handles manually stored data. """ # The plugin ID for the hook. ID_PLUGIN: int = 1061096 # The class bound cache of parameters hashed over node UUIDs. The node UUID is MAXON_CREATOR_ID, # i.e., it expresses "sameness" of nodes over (mac_id, time, secret_sauce). # # We could also hash things in other ways, for example the state of a set of parameters, the # data container, or if we had tea or coffee for breakfast. It depends on which nodes you would # consider "the same" in the context of what you want to cache. UUID_PARAMETER_CACHE: dict[bytes, list[int]] = {} @classmethod def Register(cls) -> None: """Registers the plugin hook. """ if not c4d.plugins.RegisterTagPlugin( id=cls.ID_PLUGIN, str="Example Tag", info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE, g=cls, description="texample", icon=c4d.bitmaps.InitResourceBitmap(c4d.Tdisplay)): print(f"Warning: Failed to register '{cls}' tag plugin.") @staticmethod def GetNodeUuid(node: c4d.GeListNode) -> bytes: """Identifies a node over reallocation and scene reloading boundaries. The returned UUID will express what a user would consider one node. When a user creates a document, material, shader, object, tag, etc., it will get such UUID assigned and it will stay the same even over reallocation and scene reloading boundaries. """ data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID) if not isinstance(data, memoryview): raise RuntimeError("Could not access node UUID.") return bytes(data or None) def Init(self, node: c4d.GeListNode) -> bool: """Called to initialize a tag instance. """ self.InitAttr(node, bool, c4d.ID_MYTAG_SOMEPARAM) node[c4d.ID_MYTAG_SOMEPARAM] = False # Hash the node over its UUID to lookup if we already stored data under this hash. With the # `2023.2` API it is not possible skip the data init of dummy nodes, in the next major API # release it will be. uuid: bytes = ExampleTagData.GetNodeUuid(node) data: list | None = ExampleTagData.UUID_PARAMETER_CACHE.get(uuid, None) if data is None: data = [random.randint(0, 9) for i in range(random.randint(3, 5))] ExampleTagData.UUID_PARAMETER_CACHE[uuid] = data print(f"ExampleTagData.Init: Created the data '{data}'.") else: print(f"ExampleTagData.Init: Restored the data '{data}'.") self._data: list[int] = data # The safest route is to store all data in the data container of the node. The parameter ID # ID_MYTAG_CUSTOMDATA is just an enum-value defined in the header file of the plugin as a # "slot" for custom data. It is neither reflected in the res nor string file and therefore # does not appear in the GUI of the tag. bc: c4d.BaseContainer = c4d.BaseContainer(c4d.ID_MYTAG_CUSTOMDATA) for i, v in enumerate(data): bc[i] = v self.InitAttr(node, c4d.BaseContainer, c4d.ID_MYTAG_CUSTOMDATA) node[c4d.ID_MYTAG_CUSTOMDATA] = bc return True def CopyTo(self, dest: "ExampleTagData", snode: c4d.GeListNode, dnode: c4d.GeListNode, flags: int, trn: c4d.AliasTrans) -> bool: """Called by Cinema 4D when a node is copied to let the implementation copy data not stored in the data container of the node. """ dest._data = self._data print(f"ExampleTagData.CopyTo: Copied data from `{self}` to {dest}.") return True def Write(self, node: c4d.GeListNode, hf: c4d.storage.HyperFile) -> bool: """Called by Cinema 4D when an object is saved with a document to manually serialize data not stored in its data container. This method defines the serialization form which must be deserialized in NodeData.Read. """ # Write the data in the form [count, item_1, ..., item_count] count: int = len(self._data) if not hf.WriteInt32(count): raise IOError(f"Could not serialize {self.__class.__name__} node.") success: list[bool] = [hf.WriteInt32(v) for v in self._data] if not all(success): raise IOError(f"Could not serialize {self.__class.__name__} node.") print(f"ExampleTagData.Read: Serialized `{self._data}` for {node}.") return True def Read(self, node: c4d.GeListNode, hf: c4d.storage.HyperFile, level: int) -> bool: """Called by Cinema 4D when an object is loaded from a document to manually deserialize data not stored in its data container. What must be done here is defined by the serialization pattern chosen in NodeData:Write. """ # Get the count written into the first bin and then read #count bins for the actual values. count: int = hf.ReadInt32() if not count: raise IOError(f"Could not deserialize {self.__class.__name__} node.") data: list[int] = [hf.ReadInt32() for _ in range(count)] if None in data: raise IOError(f"Could not deserialize {self.__class.__name__} node.") print(f"ExampleTagData.Read: Deserialized `{data}` for {node}.") self._data = data 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. Not used in this case. """ print(f"ExampleTagData.Execute: " f"{', '.join([str(v) for _, v in tag[c4d.ID_MYTAG_CUSTOMDATA]])}") return c4d.EXECUTIONRESULT_OK if __name__ == "__main__": ExampleTagData.Register()
- As of
-
@ferdinand Thak you for such good explanation!
I tried to solve this with storing it in data container and figure out that the problem appears when calling "Reset To Default" command (from context menu or by right-click on arrows). Seems that instead of data from node container GetDDescription and GetDParameter uses another. -
Hey @mikeudin,
that the problem appears when calling "Reset To Default" command (from context menu or by right-click on arrows)
What does constitute as 'the problem' here for you? But:
- You should initialize the data parameter with
.InitAttr
. I forgot to do that in my code example myself and now have added it. But that should not be the cause of your problem, whatever it is. - As shown above and in the other thread, Cinema reinitializes nodes that users would consider "the same". A hook is reinitialized for the same
node
when that node has been reallocated. When some data is not recomputable, or too expensive to, you must the check inInit
if the data does already exist.
# The actual tag in the scene is allocated and initialized. Init: mem: 0X7F95D530E3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00' # An Asset API temp tag is being initialized and then freed right away. Init: mem: 0X7F95D5318500, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00' Free: mem: 0X7F95D5316640, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00' # The actual tag in the scene is reinitialized. Init: mem: 0X7F95D5314FC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00' # An Asset API temp tag is being initialized and then freed right away. Init: mem: 0X7F95D52E9E40, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00' Free: mem: 0X7F95D52E30C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00' # The actual tag in the scene is reinitialized. Init: mem: 0X7F95D52FB3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00' # An Asset API temp tag is being initialized and then freed right away. Init: mem: 0X7F95D53096C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00' Free: mem: 0X7F95D530A180, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00' # The actual tag in the scene is reinitialized. Init: mem: 0X7F95D5309F00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00' # An Asset API temp tag is being initialized and then freed right away. Init: mem: 0X7F95D52F9D00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00' Free: mem: 0X7F95D52FD980, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00' # The actual tag in the scene is reinitialized. Init: mem: 0X7F95D52FD940, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00' # An Asset API temp tag is being initialized and then freed right away. Init: mem: 0X7F95D5317A80, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00' Free: mem: 0X7F95D5313EC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00'
Other than that, I cannot say much, without understanding what is exactly going wrong. We will also need executable code, so that we can debug things ourselves as this is not a trivial topic.
Cheers,
Ferdinand - You should initialize the data parameter with
-
-