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
    • Login

    Calling an existing user data preset onto a python tag.

    Cinema 4D SDK
    python r25
    2
    7
    1.3k
    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.
    • Z
      Z.Prich
      last edited by ferdinand

      Hey folks! Zach here - new to this community as well as Python scripting in general.

      I was trying to help someone out with an issue and it's a bit beyond me. But has basically spurred me to finally get into some Python scripting (once I get past this ha).

      Ok so here's the situation, this friend is using a script - a spline to nulls script - that is contained within a Python tag in C4D. Said Tag also has some custom user data on it that the Python references. Thus this means that, as far as we can tell, that in order to use this script, you have to import the .c4d file (which is how the plugin came to him) and then just ctrl drag the tag onto a new spline.

      But we'd like to have this script contained as a single executable user script.

      So, if I'm thinking correctly, we need to find a way to load this user data onto the tag, then execute the main portion of the script.

      I went into the original c4d plugin file and copied the necessary User Data into it's own preset.

      So, my specific question is - is there a way to call a User Data preset into existences before executing the second half of the script? It feels like that way we'd be able to get the whole thing into a self contained user script instead of this current workflow.

      I've seen the documentation on how to create user data within Python, but not how to just load an existing user data preset.

      And of course I know there are other ways to accomplish the spline to nulls idea, but I'm just using this as a problem solving exercise.

      Thanks!

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @Z.Prich
        last edited by ferdinand

        Hello @z-prich,

        welcome to the Plugin Café and the Cinema 4D development community. For future posts I would recommend reading our Forum Guidelines as they line out the procedures used in this forum. I removed the issue and project tool tags from your posting, as the posting is not about them, and marked it as a question. And no worries, you did well for your first posting, the procedures can be a bit involved.

        Is there a way to call a User Data preset into existence before executing the second half of the script? It feels like that way we'd be able to get the whole thing into a self-contained user script instead of this current workflow.

        I understand what you are talking about, but to be sure that there is no misunderstanding, I will be precise first. The term preset has a defined meaning in Cinema 4D and its APIs. A preset describes a set of values that can be loaded in for a defined set of parameters. You can, for example, have a preset for the Cube object, which loads in a specific set of values for the cube object. You can have the same for user data. You could for example have the user data a: str, b: int, and c: float on some kind of node, save the preset a="Hello world!", b=42, and c=3.1415 for them and then load that preset into anything that has a the user data parameters a: str, b: int, and c: float (with the same ids/order as the original one). This functionality is provided by the Attribute Manger and the Asset Browser and does not require any API access, the underlying API is the Asset API which currently is not exposed to Python. The screencast below demonstrates the workflow.

        cube.gif

        But from my understanding, you mean with preset here, that a specific signature of user data parameters is being loaded, e.g., the signature a: str, b: int, and c: float we talked about in the paragraph above. There are three ways you can tackle this problem:

        1. Simply tell the user to load in the file containing the object setup containing the user data and to copy or use that object. Which is a bit cumbersome to do.
        2. The natural solution to that would be writing a plugin. In your case of a Python Programming tag, the full-blown plugin counterpart would be a TagData plugin. The required code would be quite similar to the Python Programming tag code, the major work would lie in defining the interface as a resource and doing some boiler plate stuff like registering the plugin.
        3. The third option would be to automatically populate the user data. The problem here is that this is not an officially intended workflow and you must get a bit creative with when to build the data. The core of that workflow are BaseList2D.AddUserData, with which one can add user data, and the message method of the scripting object to find a point in time to do so. See the end of the posting for an example once posted here. But you should be aware that this option is sort of a hack.

        Cheers,
        Ferdinand

        The result of option three, in this case a Python Generator object which auto-populates its required user data:
        autopop.gif

        The code. Note that I wrote this code when I was a user myself, so it is not as extensively documented as example code, I only documented the greeble function then. I have added some doc-strings, but I did not document everything.

        """Demonstrates how to populate the user data setup of a scripting object
        from within the scripting object.
        
        The relevant code is in the function message().
        """
        
        import c4d
        import random
        
        from c4d.utils import SendModelingCommand
        
        def message(mid: int, data: any) -> None:
            """Called by Cinema 4D to convey even-like messages to the node.
            
            Args:
                mid (int): The id of the message event.
                data (any): The message data, depending on the message type. Not
                 used in this example.
            """
            # We must be on the main-thread for what we want to do.
            if not c4d.threading.GeIsMainThread():
                return
        
            # There is no dedicated message to build the user data of a node, so we
            # must piggy-back onto one for another purpose. c4d.MSG_GETREALCAMERADATA
            # is being called quite often in a Python Generator object and works well,
            # for other scripting object types you might have to choose other messages
            # to piggy-back onto, as not all messages are being sent to all nodes.
        
            # When there is a MSG_GETREALCAMERADATA event and the user data is still
            # empty, then populate it. 
            if mid == c4d.MSG_GETREALCAMERADATA and not op.GetUserDataContainer():
                """
                """
                # Build a user data parameter of type DTYPE_BASELISTLINK.
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK)
                bc[c4d.DESC_NAME] = "Object"
                # Add the parameter, the order is important here. Since this is the
                # first parameter added, #eid will be [c4d.ID_USERDATA, 1].
                eid = op.AddUserData(bc)
        
                # Build a user data parameter of type integer (long).
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
                bc[c4d.DESC_NAME] = "Subdivisions"
                bc[c4d.DESC_MIN] = 1
                bc[c4d.DESC_MAX] = 16
                bc[c4d.DESC_MINSLIDER] = 1
                bc[c4d.DESC_MAXSLIDER] = 8
                bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_LONGSLIDER
                # Add the parameter, the order is important here. Since this is the
                # second parameter added, #eid will be [c4d.ID_USERDATA, 2].
                eid = op.AddUserData(bc)
                op[eid] = 3
        
                # Build a user data parameter of type float(real).
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
                bc[c4d.DESC_NAME] = "Subdivision Probability"
                bc[c4d.DESC_MIN] = 0.
                bc[c4d.DESC_MAX] = 1.
                bc[c4d.DESC_MINSLIDER] = 0.
                bc[c4d.DESC_MAXSLIDER] = 1.
                bc[c4d.DESC_STEP] = .005
                bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
                bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
                # Add the parameter, the order is important here. Since this is the
                # third parameter added, #eid will be [c4d.ID_USERDATA, 3].
                eid = op.AddUserData(bc)
                op[eid] = 1.
        
                # ... just more of the same ...
        
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
                bc[c4d.DESC_NAME] = "Subdivision Probability Decay"
                bc[c4d.DESC_MIN] = 0.
                bc[c4d.DESC_MAX] = 1.
                bc[c4d.DESC_MINSLIDER] = 0.
                bc[c4d.DESC_MAXSLIDER] = 1.
                bc[c4d.DESC_STEP] = .005
                bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
                bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
                eid = op.AddUserData(bc)
                op[eid] = .75
        
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
                bc[c4d.DESC_NAME] = "Animation"
                bc[c4d.DESC_MIN] = 0.
                bc[c4d.DESC_STEP] = .01
                bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
                eid = op.AddUserData(bc)
                op[eid] = .0
        
                bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
                bc[c4d.DESC_NAME] = "Seed"
                bc[c4d.DESC_MIN] = 1
                eid = op.AddUserData(bc)
                op[eid] = 1
        
                # We need to update the GUI after we are done, which is why we must
                # be on the main thread and why we are doing this in message().
                c4d.EventAdd()
        
        """ The following code is irrelevant to the example in a literal sense, but
        included because one still must accommodate for example that the user 
        data might not be yet present when Cinema 4D calls main() for the first time(s).
        ------------------------------------------------------------------------------
        """
        
        def get_greeble(op, subdivisions, probability, probability_decay, time, seed):
            """Computes a greeble object for a polygon object input. Calls itself
             recursively for multiple subdivisions.
        
            The subdivision scheme is slight variation on the one midpoint subdivision
             scheme (used for example in Catmull-Clark SDS). The differences are that
             along the v axis two center points are being generated and the surrounding
             edge points are not centered on their edges. This will result in a
             segmented (not connected) mesh. The scheme in detail is as follows:
        
            a, b, c, d   - The points of the polygon (the vertex points)
            du, dv, dw   - The random offsets (.25 to .75) for the intermediate points
            p, q, r, s   - The intermediate points (the edge points)
            g, h         - The two center points
            A, B, C, D   - The resulting four new polygons
        
                           du
                       ┌───────┐
                       ↓       ↓
        
                      [d]───── r ───────[c]
                       │   D   ┆         │
               ┌───>   s┄┄┄┄┄┄┄g    C    │
               │       │       ┆         │
            dv │       │   A   h┄┄┄┄┄┄┄┄┄q   <───┐
               │       │       ┆    B    │       │ dw
               └───>  [a]───── p ───────[b]  <───┘
        
            Args:
                op (c4d.BaseObject): The input object to greeble.
                subdivisions (int): The number of subdivisions per polygon.
                probability (float): The chance that a subdivision occurs on a polygon. 
                probability_decay (float): The decay of probability per recursion.
                time (float): The time for computing the random offsets.
                seed (int): The seed for computing the random offsets.
        
            Returns:
                c4d.PolygonObject or None: The greeble or None if op was not a valid
                 input object.
            """
        
            # Get the caches
            if isinstance(op, c4d.BaseObject):
                deform_cache = op.GetDeformCache()
                cache = op.GetCache() if deform_cache is None else deform_cache
                op = cache if isinstance(cache, c4d.PolygonObject) else op
        
            if not isinstance(op, c4d.PolygonObject):
                return None
        
            # Data IO
            points = op.GetAllPoints()
            polygons = op.GetAllPolygons()
            new_points = []
            new_polygons = []
        
            def lerp(a, b, t): return a + (b-a) * t
            def noise(p): return c4d.utils.noise.Noise(p + time)
            seed = 1./seed
        
            # For every polygon in the input object
            for cpoly in polygons:
        
                # The points of the polygon
                a, b = points[cpoly.a], points[cpoly.b]
                c, d = points[cpoly.c], points[cpoly.d]
                # The mean of the points as the identity for the noise seed
                ip = (a + b + c + d) * .25
        
                # Skip a subdivision step and just copy the old polygon
                if random.random() > probability:
                    new_points += [a, b, c, d]
                    bid = len(new_points) - 1
                    A = c4d.CPolygon(bid - 3, bid - 2, bid - 1, bid - 0)
                    new_polygons.append(A)
                    continue
        
                # The random offsets
                du = noise(ip + c4d.Vector(seed, 0., 0.))
                dv = noise(ip + c4d.Vector(0., seed, 0.))
                dw = noise(ip + c4d.Vector(0., 0., seed))
                # The edge points
                p, r = lerp(a, b, du), lerp(c, d, 1. - du)
                q, s = lerp(b, c, dw), lerp(a, d, dv)
                # The center points
                g, h = lerp(p, r, dv), lerp(p, r, dw)
        
                # append the generated points to our output point list.
                # id offsets:  9, 8, 7, 6, 5, 4, 3, 2, 1, 0
                new_points += [a, b, c, d, p, q, r, s, g, h]
                # the base id for the point indices of the current polygon group
                bid = len(new_points) - 1
                """ [d]───── r ───────[c]
                     │   D   ┆         │
                     s┄┄┄┄┄┄┄g   C     │
                     │       ┆         │
                     │   A   h┄┄┄┄┄┄┄┄┄q
                     │       ┆   B     │
                    [a]───── p ───────[b] """
                #                      a,       p,       g,       s
                A = c4d.CPolygon(bid - 9, bid - 5, bid - 1, bid - 2)
                #                      p,       b,       q,       h
                B = c4d.CPolygon(bid - 5, bid - 8, bid - 4, bid - 0)
                #                      h,       q,       c,       r
                C = c4d.CPolygon(bid - 0, bid - 4, bid - 7, bid - 3)
                #                      s,       g,       r,       d
                D = c4d.CPolygon(bid - 2, bid - 1, bid - 3, bid - 6)
        
                new_polygons += [A, B, C, D]
        
            # Generate the output object
            res = c4d.PolygonObject(len(new_points), len(new_polygons))
            res.SetAllPoints(new_points)
            for index, cpoly in enumerate(new_polygons):
                res.SetPolygon(index, cpoly)
        
            # subdivide the result further
            if subdivisions > 1:
                res = get_greeble(op=res,
                                  subdivisions=subdivisions - 1,
                                  probability=probability * probability_decay,
                                  probability_decay=probability_decay,
                                  time=time,
                                  seed=seed)
            return res
        
        def get_output(op):
            """Computes the final output of the generator.
            """
            # Res was None because there is no user data yet.
            if op is None:
                return c4d.BaseObject(c4d.Onull)
        
        
            for i in range(1):
                poly_ids = list(range(op.GetPolygonCount()))
                random.shuffle(poly_ids)
        
                selection = c4d.BaseSelect()
                for j in range(10):
                    selection.Select(poly_ids.pop(0))
        
                op.GetPolygonS().DeselectAll()
                selection.CopyTo(op.GetPolygonS())
        
                res = SendModelingCommand(c4d.MCOMMAND_GENERATESELECTION, [op],
                                          c4d.MODELINGCOMMANDMODE_POLYGONSELECTION)
                tag = op.GetLastTag()
                tag.SetName(i)
            return op
        
        def main():
            """Called by Cinema 4D to populate the cache of the Python Generator 
            object. When message() has not been called yet, we must defer the 
            computation of the *real* output by 
            """
            res = None
        
            # The user data container is populated. I implemented this in a bit dicey
            # fashion, since I simply assume that if there is any user data, that it
            # is the correct one. It would be better to test if the ids 1 to 6 exist
            # and are indeed of the expected type.
            if op.GetUserDataContainer():
                # Get the user data
                obj = op[c4d.ID_USERDATA, 1]
                subdivisions = op[c4d.ID_USERDATA, 2]
                probability = op[c4d.ID_USERDATA, 3]
                probability_decay = op[c4d.ID_USERDATA, 4]
                time = op[c4d.ID_USERDATA, 5]
                seed = op[c4d.ID_USERDATA, 6]
        
                random.seed(seed)
        
                # Compute the greeble object
                res = get_greeble(op=obj,
                                  subdivisions=subdivisions,
                                  probability=probability,
                                  probability_decay=probability_decay,
                                  time=time,
                                  seed=seed)
        
            # Return a null object if get_greeble failed, else return the greeble
            return get_output(res)
        

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • Z
          Z.Prich
          last edited by

          Holy moly - fantastic post. Thanks for the effort! (and sorry about the incorrect tags, I'll definitely lurk a bit more and get the lay of the land ha).

          I'm going to try to ingest this over the week and see what I can make this weekend. Thanks again!

          1 Reply Last reply Reply Quote 0
          • Z
            Z.Prich
            last edited by

            Also, @ferdinand - this video might describe exactly what I'm looking for better.

            https://www.loom.com/share/f2ee91fac416435982a9a31af428b21a

            I realize I potentially didn't clarify what I meant when I said preset in reference to your definitions - so maybe that clears it up!

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

              Hello @Z-Prich,

              That is the Asset API at work (I did mention the API in my first posting in the first section), assuming you are referring to the ability of the User Data editor to save presets for its content. You can make use of that feature from the App, but you cannot invoke it programmatically in Python because the Asset API is not exposed and the parts that are going to be exposed in upcoming releases will likely not contain the relevant asset types for handling and unpacking this data. I am frankly not sure if this will be public at any point since we currently do not even expose the relevant component in C++.

              The closest you can get is the approach shown above, where you populate the interface programmatically at runtime. The cleanest approach is to write a TagData plugin. Which is not that hard to do.

              Cheers,
              Ferdinand

              MAXON SDK Specialist
              developers.maxon.net

              1 Reply Last reply Reply Quote 0
              • Z
                Z.Prich
                last edited by

                Thanks @ferdinand yeah that all makes sense. Just wanted to make sure I had provided a clearer explanation of what I was trying to do. Thanks again!

                ferdinandF 1 Reply Last reply Reply Quote 0
                • ferdinandF
                  ferdinand @Z.Prich
                  last edited by

                  Hello @z-prich,

                  without any further activity before Wednesday, the 16.03.2022, we will consider this topic as solved and remove the "unsolved" state of this topic.

                  Thank you for your understanding,
                  Ferdinand

                  MAXON SDK Specialist
                  developers.maxon.net

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