Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Register
    • Login

    TagData plugin undo issues

    Cinema 4D SDK
    python windows
    2
    6
    708
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • moghurtM
      moghurt
      last edited by

      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()
      
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @moghurt
        last edited by ferdinand

        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:

        1. 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.
        2. Store data in a global hash map where nodes are the keys, this will not work over serialization and deserialization boundaries (by default).
        3. Implement the whole custom data serialization and deserialization chain of NodeData, i.e., NodeData.Read(), NodeData.Write(), and NodeData.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 a BaseContainer 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 in MyData 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,
        Ferdinand

        PS: 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 implement Read, Write, and CopyTo all together. At your own risk, you could ignore this here, it should be without consequences in this case. By not implementing Read and Write you would loose serialization and deserialization, but in CopyTo you can literally copy from one instance of MyPlugin to another, i.e., you would have to pack your data into a BaseContainer/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

        MAXON SDK Specialist
        developers.maxon.net

        moghurtM 1 Reply Last reply Reply Quote 0
        • moghurtM
          moghurt @ferdinand
          last edited by

          @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!

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @moghurt
            last edited by ferdinand

            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

            MAXON SDK Specialist
            developers.maxon.net

            moghurtM 1 Reply Last reply Reply Quote 0
            • moghurtM
              moghurt @ferdinand
              last edited by

              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!

              1 Reply Last reply Reply Quote 0
              • ferdinandF
                ferdinand
                last edited by

                No need to be sorry, a good to hear that you found your solution!

                MAXON SDK Specialist
                developers.maxon.net

                1 Reply Last reply Reply Quote 0
                • First post
                  Last post