SOLVED Calling an existing user data preset onto a python tag.

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!

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!

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)

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!

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!

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

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!

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