Skip to content

lib

Library functions for Cinema4d.

active_document()

Get the active Cinema4d document.

Returns:

Type Description

c4d.documents.BaseDocument: The active document.

Source code in client/ayon_cinema4d/api/lib.py
74
75
76
77
78
79
80
81
def active_document():
    """Get the active Cinema4d document.

    Returns:
        c4d.documents.BaseDocument: The active document.
    """

    return c4d.documents.GetActiveDocument()

add_objects_to_container(container, nodes)

Add the nodes to the container.

A container in Cinema4d is a selection object. We have to get the so called InExcludeData and add the objects to it.

Parameters:

Name Type Description Default
container

The Avalon container to add the nodes to

required
nodes list

The nodes to add to the container

required
Source code in client/ayon_cinema4d/api/lib.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def add_objects_to_container(container, nodes):
    """Add the nodes to the container.

    A container in Cinema4d is a selection object. We have to get the so called
    InExcludeData and add the objects to it.

    Args:
        container: The Avalon container to add the nodes to
        nodes (list): The nodes to add to the container
    """
    in_exclude_data = container[c4d.SELECTIONOBJECT_LIST]
    for node in nodes:
        in_exclude_data.InsertObject(node, 1)
    container[c4d.SELECTIONOBJECT_LIST] = in_exclude_data
    c4d.EventAdd()

collect_animation_defs(create_context, fps=False)

Get the basic animation attribute definitions for the publisher.

Parameters:

Name Type Description Default
create_context CreateContext

The context of publisher will be used to define the defaults for the attributes to use the current context's entity frame range as default values.

required
step bool

Whether to include step attribute definition.

required
fps bool

Whether to include fps attribute definition.

False

Returns:

Type Description

List[NumberDef]: List of number attribute definitions.

Source code in client/ayon_cinema4d/api/lib.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def collect_animation_defs(create_context, fps=False):
    """Get the basic animation attribute definitions for the publisher.

    Arguments:
        create_context (CreateContext): The context of publisher will be
            used to define the defaults for the attributes to use the current
            context's entity frame range as default values.
        step (bool): Whether to include `step` attribute definition.
        fps (bool): Whether to include `fps` attribute definition.

    Returns:
        List[NumberDef]: List of number attribute definitions.

    """

    # use task entity attributes to set defaults based on current context
    task_entity = create_context.get_current_task_entity()
    attrib: dict = task_entity["attrib"]
    frame_start: int = attrib["frameStart"]
    frame_end: int = attrib["frameEnd"]
    handle_start: int = attrib["handleStart"]
    handle_end: int = attrib["handleEnd"]

    # build attributes
    defs = [
        NumberDef("frameStart",
                  label="Frame Start",
                  default=frame_start,
                  decimals=0),
        NumberDef("frameEnd",
                  label="Frame End",
                  default=frame_end,
                  decimals=0),
        NumberDef("handleStart",
                  label="Handle Start",
                  tooltip="Frames added before frame start to use as handles.",
                  default=handle_start,
                  decimals=0),
        NumberDef("handleEnd",
                  label="Handle End",
                  tooltip="Frames added after frame end to use as handles.",
                  default=handle_end,
                  decimals=0),
    ]

    if fps:
        doc = active_document()
        current_fps = doc.GetFps()
        fps_def = NumberDef(
            "fps", label="FPS", default=current_fps, decimals=5
        )
        defs.append(fps_def)

    return defs

get_all_children(obj)

Returns all children of an object, including grandchildren.

Source code in client/ayon_cinema4d/api/lib.py
363
364
365
def get_all_children(obj):
    """Returns all children of an object, including grandchildren."""
    return list(iter_all_children(obj))

get_materials_from_objects(objects)

Get the materials assigned to the objects.

Parameters:

Name Type Description Default
objects List[BaseObject]

Objects to get materials for.

required

Returns:

Type Description

List[c4d.BaseMaterial]: List of assigned materials.

Source code in client/ayon_cinema4d/api/lib.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def get_materials_from_objects(objects):
    """Get the materials assigned to the objects.

    Arguments:
        objects (List[c4d.BaseObject]): Objects to get materials for.

    Returns:
        List[c4d.BaseMaterial]: List of assigned materials.
    """

    materials = []
    for obj in objects:
        material_tags = [
            tag for tag in obj.GetTags() if tag.GetTypeName() == "Material"
        ]
        for material_tag in material_tags:
            material = material_tag.GetMaterial()
            if material:
                materials.append(material)

    return materials

get_objects_from_container(container, existing_only=True)

Get the objects from the container.

A container in Cinema4d is a selection object. We have to get the so called InExcludeData, get the object count and then get the objects at the indices.

Parameters:

Name Type Description Default
container BaseObject

The object containing selections.

required

Returns:

Name Type Description
generator

The objects in the selection object.

Source code in client/ayon_cinema4d/api/lib.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def get_objects_from_container(container, existing_only=True):
    """Get the objects from the container.

    A container in Cinema4d is a selection object. We have to get the so called
    InExcludeData, get the object count and then get the objects at the indices.

    Arguments:
        container (c4d.BaseObject): The object containing selections.

    Returns:
        generator: The objects in the selection object.
    """
    doc: c4d.documents.BaseDocument = container.GetMain()
    assert isinstance(doc, c4d.documents.BaseDocument)
    in_exclude_data = container[c4d.SELECTIONOBJECT_LIST]

    # If the container is not a selection object list yield no child objects
    if not in_exclude_data:
        return

    object_count = in_exclude_data.GetObjectCount()
    for i in range(object_count):
        obj = in_exclude_data.ObjectFromIndex(doc, i)
        if existing_only and not obj:
            continue

        yield obj

get_unique_namespace(folder_name, prefix=None, suffix=None, doc=None)

Get a unique namespace for a newly loaded asset.

Go through all loaded assets and if a loaded asset with the same name in encountered, go 1 version higher. So for example if you load 'foo' and there is already a 'foo_01', set the namespace to 'foo_02'.

You can optionally set a prefix or suffix.

Parameters:

Name Type Description Default
folder_name str

The name of the folder.

required
prefix optional str

An optional prefix for the namespace.

None
suffix optional str

An optional suffix for the namespace.

None
doc optional c4d.documents.BaseDocument

Optional Cinema4d document to work on. Default is the active document.

None

Returns:

Name Type Description
str

The unique namespace.

Source code in client/ayon_cinema4d/api/lib.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def get_unique_namespace(folder_name, prefix=None, suffix=None, doc=None):
    """Get a unique namespace for a newly loaded asset.

    Go through all loaded assets and if a loaded asset with the same name in
    encountered, go 1 version higher. So for example if you load 'foo'
    and there is already a 'foo_01', set the namespace to 'foo_02'.

    You can optionally set a prefix or suffix.

    Arguments:
        folder_name (str): The name of the folder.
        prefix (optional str): An optional prefix for the namespace.
        suffix (optional str): An optional suffix for the namespace.
        doc (optional c4d.documents.BaseDocument): Optional Cinema4d document
            to work on. Default is the active document.

    Returns:
        str: The unique namespace.
    """
    doc = doc or active_document()
    prefix = prefix or ""
    suffix = suffix or ""
    iteration = 1
    unique = "{prefix}{asset_name}_{iteration:02d}{suffix}".format(
        prefix=prefix,
        asset_name=folder_name,
        iteration=iteration,
        suffix=suffix,
    )

    while doc.SearchObject(unique):
        iteration += 1
        unique = "{prefix}{asset_name}_{iteration:02d}{suffix}".format(
            prefix=prefix,
            asset_name=folder_name,
            iteration=iteration,
            suffix=suffix,
        )

    return unique

imprint(node, data, group=None)

Write data to node as userDefined attributes

Parameters:

Name Type Description Default
node BaseObject

The selection object

required
data dict

Dictionary of key/value pairs

required
Source code in client/ayon_cinema4d/api/lib.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def imprint(node, data, group=None):
    """Write `data` to `node` as userDefined attributes

    Arguments:
        node (c4d.BaseObject): The selection object
        data (dict): Dictionary of key/value pairs
    """

    existing_user_data = node.GetUserDataContainer()
    existing_to_id = {}
    for description_id, base_container in existing_user_data:
        key = base_container[c4d.DESC_NAME]
        existing_to_id[key] = description_id

    # If `group` is specified, find the group to add new attributes to.
    group_id = None
    if group:
        # Search the group first, if it does not exist, create it.
        for description_id, base_container in existing_user_data:
            name = base_container[c4d.DESC_NAME]
            if name == group and description_id[1].dtype == c4d.DTYPE_GROUP:
                group_id = description_id
                break
        else:
            # Create the group
            group_bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_GROUP)
            group_bc[c4d.DESC_NAME] = group
            group_bc[c4d.DESC_SHORT_NAME] = group
            group_bc[c4d.DESC_TITLEBAR] = True
            group_bc[c4d.DESC_GUIOPEN] = False
            group_id = node.AddUserData(group_bc)

    for key, value in data.items():

        if callable(value):
            # Support values evaluated at imprint
            value = value()

        if isinstance(value, bool):
            add_type = c4d.DTYPE_BOOL
        elif isinstance(value, str):
            add_type = c4d.DTYPE_STRING
        elif isinstance(value, int):
            add_type = c4d.DTYPE_LONG
        elif isinstance(value, float):
            add_type = c4d.DTYPE_REAL
        elif isinstance(value, (dict, list)):
            add_type = c4d.DTYPE_STRING
            value = f"{JSON_PREFIX}{json.dumps(value)}"
        else:
            raise TypeError(
                f"Unsupported type for {key}: {value} ({type(value)})")

        if key in existing_to_id:
            # Set existing
            element = existing_to_id[key]
        else:
            # Create new
            base_container = c4d.GetCustomDataTypeDefault(add_type)
            base_container[c4d.DESC_NAME] = key
            base_container[c4d.DESC_SHORT_NAME] = key
            base_container[c4d.DESC_ANIMATE] = c4d.DESC_ANIMATE_OFF
            if group_id:
                base_container[c4d.DESC_PARENTGROUP] = group_id

            element = node.AddUserData(base_container)

        node[element] = value

    c4d.EventAdd()

iter_all_children(obj)

Yield all children of an object, including grandchildren.

Source code in client/ayon_cinema4d/api/lib.py
354
355
356
357
358
359
360
def iter_all_children(obj):
    """Yield all children of an object, including grandchildren."""
    stack = obj.GetChildren()
    while stack:
        child_obj = stack.pop()
        stack.extend(child_obj.GetChildren())
        yield child_obj

maintained_selection()

Maintain selection during context.

Source code in client/ayon_cinema4d/api/lib.py
84
85
86
87
88
89
90
91
92
93
@contextlib.contextmanager
def maintained_selection():
    """Maintain selection during context."""

    doc = active_document()
    previous_selection = doc.GetSelection()
    try:
        yield
    finally:
        set_selection(doc, previous_selection)

obj_user_data_to_dict(obj)

Construct a simple dictionary from the user data.

Convert the user data to a dictionary so it's easier to work with.

Returns:

Type Description
dict

dict[str, Any]: User data of object..

Source code in client/ayon_cinema4d/api/lib.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def obj_user_data_to_dict(obj) -> dict:
    """Construct a simple dictionary from the user data.

    Convert the user data to a dictionary so it's easier to work with.

    Returns:
        dict[str, Any]: User data of object..

    """
    if not obj.GetUserDataContainer():
        return None

    user_data = {}

    for description_id, base_container in obj.GetUserDataContainer():
        key = base_container[c4d.DESC_NAME]

        try:
            value = obj[description_id]
        except AttributeError:
            # Fix #23: Silently ignore values that are not wrapped to Python
            #  because we know user data we are interested in isn't any of
            #  those anyway. Avoids object unknown in Python error.
            continue

        user_data[key] = value

    return user_data

read(node)

Return user-defined attributes from node

Source code in client/ayon_cinema4d/api/lib.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def read(node) -> dict:
    """Return user-defined attributes from `node`"""

    data = obj_user_data_to_dict(node)

    # data can be None, if so just return it
    if data is None:
        return {}

    data = {
        key: value
        for key, value in data.items()
        # Ignore hidden/internal data
        if not key.startswith("_")
        # Ignore values that are None (e.g. groups in user data)
        and value is not None
    }

    for key, value in data.items():
        if isinstance(value, str) and value.startswith(JSON_PREFIX):
            data[key] = json.loads(value[len(JSON_PREFIX):])

    return data

set_frame_range_from_entity(task_entity, doc=None)

Set scene FPS adn resolution from task entity

Source code in client/ayon_cinema4d/api/lib.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
def set_frame_range_from_entity(task_entity, doc=None):
    """Set scene FPS adn resolution from task entity"""
    if doc is None:
        doc = active_document()
    attrib = task_entity["attrib"]

    # get handles values
    handle_start = int(attrib["handleStart"])
    handle_end = int(attrib["handleEnd"])

    f_fps = float(attrib["fps"])
    i_fps = int(math.ceil(attrib["fps"]))
    frame_start = int(attrib["frameStart"]) - handle_start
    frame_end = int(attrib["frameEnd"]) + handle_end
    bt_frame_start = c4d.BaseTime(frame_start, i_fps)
    bt_frame_end = c4d.BaseTime(frame_end, i_fps)

    # set document fps
    doc.SetFps(i_fps)

    # set document frame range
    doc.SetMinTime(bt_frame_start)
    doc.SetMaxTime(bt_frame_end)
    doc.SetLoopMinTime(bt_frame_start)
    doc.SetLoopMaxTime(bt_frame_end)

    rd = doc.GetFirstRenderData()

    while rd:
        # set render fps
        rd[c4d.RDATA_FRAMERATE] = f_fps
        # set render frame range
        rd[c4d.RDATA_FRAMEFROM] = bt_frame_start
        rd[c4d.RDATA_FRAMETO] = bt_frame_end
        rd = rd.GetNext()

    c4d.EventAdd()

set_resolution_from_entity(task_entity, doc=None)

Set render resolution from task entity

Source code in client/ayon_cinema4d/api/lib.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def set_resolution_from_entity(task_entity, doc=None):
    """Set render resolution from task entity"""
    if doc is None:
        doc = active_document()

    attrib = task_entity["attrib"]
    width: int = int(attrib["resolutionWidth"])
    height: int = int(attrib["resolutionHeight"])
    pixel_aspect: float = attrib["pixelAspect"]

    @contextlib.contextmanager
    def _unlocked_ratio(render_data):
        """Temporarily unlock the ratio of the render resolution."""
        original = render_data[c4d.RDATA_LOCKRATIO]
        render_data[c4d.RDATA_LOCKRATIO] = False
        try:
            yield
        finally:
            render_data[c4d.RDATA_LOCKRATIO] = original

    rd = doc.GetFirstRenderData()
    while rd:
        # Fix #20: Set the virtual resolution with user interaction so Redshift
        # still triggers some additional checks on the attribute change.
        with _unlocked_ratio(rd):
            flag = c4d.DESCFLAGS_SET_USERINTERACTION
            rd.SetParameter(c4d.RDATA_XRES_VIRTUAL, width, flag)
            rd.SetParameter(c4d.RDATA_YRES_VIRTUAL, height, flag)

        # Set pixel aspect ratio
        rd[c4d.RDATA_PIXELASPECT] = pixel_aspect

        rd = rd.GetNext()
    c4d.EventAdd()

undo_chunk()

Open a undo chunk during context.

Source code in client/ayon_cinema4d/api/lib.py
109
110
111
112
113
114
115
116
117
@contextlib.contextmanager
def undo_chunk():
    """Open a undo chunk during context."""
    doc = active_document()
    try:
        doc.StartUndo()
        yield
    finally:
        doc.EndUndo()