Skip to content

setdress

compare_representations(current_repre, current_parents, new_repre, new_parents)

Check if the old representation given can be updated

Due to limitations of the update_container function we cannot allow differences in the following data:

  • Representation name (extension)
  • Folder id
  • Product id

If any of those data values differs, the function will raise an RuntimeError

Parameters:

Name Type Description Default
current_repre dict[str, Any]

Current representation entity.

required
current_parents RepresentationParents

Current representation parents.

required
new_repre dict[str, Any]

New representation entity.

required
new_parents RepresentationParents

New representation parents.

required

Returns:

Name Type Description
bool

False if the representation is not invalid else True

Source code in client/ayon_maya/api/setdress.py
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def compare_representations(
    current_repre, current_parents, new_repre, new_parents
):
    """Check if the old representation given can be updated

    Due to limitations of the `update_container` function we cannot allow
    differences in the following data:

    * Representation name (extension)
    * Folder id
    * Product id

    If any of those data values differs, the function will raise an
    RuntimeError

    Args:
        current_repre (dict[str, Any]): Current representation entity.
        current_parents (RepresentationParents): Current
            representation parents.
        new_repre (dict[str, Any]): New representation entity.
        new_parents (RepresentationParents): New representation parents.

    Returns:
        bool: False if the representation is not invalid else True

    """
    if current_repre["name"] != new_repre["name"]:
        log.error("Cannot switch extensions")
        return False

    # TODO add better validation e.g. based on parent ids
    if current_parents.folder["id"] != new_parents.folder["id"]:
        log.error("Changing folders between updates is not supported.")
        return False

    if current_parents.product["id"] != new_parents.product["id"]:
        log.error("Changing products between updates is not supported.")
        return False

    return True

get_contained_containers(container)

Get the AYON containers in this container

Parameters:

Name Type Description Default
container dict

The container dict.

required

Returns:

Name Type Description
list

A list of member container dictionaries.

Source code in client/ayon_maya/api/setdress.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def get_contained_containers(container):
    """Get the AYON containers in this container

    Args:
        container (dict): The container dict.

    Returns:
        list: A list of member container dictionaries.

    """

    from .pipeline import parse_container

    # Get AYON containers in this package setdress container
    containers = []
    members = cmds.sets(container['objectName'], query=True)
    for node in cmds.ls(members, type="objectSet"):
        member_container = parse_container(node)
        if not member_container:
            # Skip invalid container (missing partial metadata)
            continue
        containers.append(member_container)

    return containers

load_package(filepath, name, namespace=None)

Load a package that was gathered elsewhere.

A package is a group of published instances, possibly with additional data in a hierarchy.

Source code in client/ayon_maya/api/setdress.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
160
161
162
163
def load_package(filepath, name, namespace=None):
    """Load a package that was gathered elsewhere.

    A package is a group of published instances, possibly with additional data
    in a hierarchy.

    """

    if namespace is None:
        # Define a unique namespace for the package
        namespace = os.path.basename(filepath).split(".")[0]
        unique_namespace(namespace)
    assert isinstance(namespace, str)

    # Load the setdress package data
    with open(filepath, "r") as fp:
        data = json.load(fp)

    # Load the setdress alembic hierarchy
    #   We import this into the namespace in which we'll load the package's
    #   instances into afterwards.
    alembic = filepath.replace(".json", ".abc")
    hierarchy = cmds.file(alembic,
                          reference=True,
                          namespace=namespace,
                          returnNewNodes=True,
                          groupReference=True,
                          groupName="{}:{}".format(namespace, name),
                          typ="Alembic")

    # Get the top root node (the reference group)
    root = "{}:{}".format(namespace, name)

    containers = []
    all_loaders = discover_loader_plugins()
    for representation_id, instances in data.items():

        # Find the compatible loaders
        loaders = loaders_from_representation(
            all_loaders, representation_id
        )

        for instance in instances:
            container = _add(instance=instance,
                             representation_id=representation_id,
                             loaders=loaders,
                             namespace=namespace,
                             root=root)
            containers.append(container)

    # TODO: Do we want to cripple? Or do we want to add a 'parent' parameter?
    # Cripple the original AYON containers so they don't show up in the
    # manager
    # for container in containers:
    #     cmds.setAttr("%s.id" % container,
    #                  "setdress.container",
    #                  type="string")

    # TODO: Lock all loaded nodes
    #   This is to ensure the hierarchy remains unaltered by the artists
    # for node in nodes:
    #      cmds.lockNode(node, lock=True)

    return containers + hierarchy

namespaced(namespace, new=True)

Work inside namespace during context

Parameters:

Name Type Description Default
new bool

When enabled this will rename the namespace to a unique namespace if the input namespace already exists.

True

Yields:

Name Type Description
str

The namespace that is used during the context

Source code in client/ayon_maya/api/setdress.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@contextlib.contextmanager
def namespaced(namespace, new=True):
    """Work inside namespace during context

    Args:
        new (bool): When enabled this will rename the namespace to a unique
            namespace if the input namespace already exists.

    Yields:
        str: The namespace that is used during the context

    """
    original = cmds.namespaceInfo(cur=True)
    if new:
        namespace = unique_namespace(namespace)
        cmds.namespace(add=namespace)

    try:
        cmds.namespace(set=namespace)
        yield namespace
    finally:
        cmds.namespace(set=original)

to_namespace(node, namespace)

Return node name as if it's inside the namespace.

Parameters:

Name Type Description Default
node str

Node name

required
namespace str

Namespace

required

Returns:

Name Type Description
str

The node in the namespace.

Source code in client/ayon_maya/api/setdress.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def to_namespace(node, namespace):
    """Return node name as if it's inside the namespace.

    Args:
        node (str): Node name
        namespace (str): Namespace

    Returns:
        str: The node in the namespace.

    """
    namespace_prefix = "|{}:".format(namespace)
    node = namespace_prefix.join(node.split("|"))
    return node

update_package(set_container, context)

Update any matrix changes in the scene based on the new data

Parameters:

Name Type Description Default
set_container dict

container data from ls()

required
context dict

the representation document from the database

required

Returns:

Type Description

None

Source code in client/ayon_maya/api/setdress.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def update_package(set_container, context):
    """Update any matrix changes in the scene based on the new data

    Args:
        set_container (dict): container data from `ls()`
        context (dict): the representation document from the database

    Returns:
        None

    """

    # Load the original package data
    project_name = context["project"]["name"]
    repre_entity = context["representation"]
    current_representation = ayon_api.get_representation_by_id(
        project_name, set_container["representation"]
    )

    current_file = get_representation_path(current_representation)
    assert current_file.endswith(".json")
    with open(current_file, "r") as fp:
        current_data = json.load(fp)

    # Load the new package data
    new_file = get_representation_path(repre_entity)
    assert new_file.endswith(".json")
    with open(new_file, "r") as fp:
        new_data = json.load(fp)

    # Update scene content
    containers = get_contained_containers(set_container)
    update_scene(set_container, containers, current_data, new_data, new_file)

    # TODO: This should be handled by the pipeline itself
    cmds.setAttr(set_container['objectName'] + ".representation",
                 context["representation"]["id"], type="string")

update_package_version(container, version)

Update package by version number

Parameters:

Name Type Description Default
container dict

container data of the container node

required
version int

the new version number of the package

required

Returns:

Type Description

None

Source code in client/ayon_maya/api/setdress.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
def update_package_version(container, version):
    """
    Update package by version number

    Args:
        container (dict): container data of the container node
        version (int): the new version number of the package

    Returns:
        None

    """

    # Versioning (from `core.maya.pipeline`)
    project_name = get_current_project_name()
    repre_id = container["representation"]
    current_representation = ayon_api.get_representation_by_id(
        project_name, repre_id
    )

    assert current_representation is not None, "This is a bug"

    (
        version_entity,
        product_entity,
        folder_entity,
        project_entity
    ) = ayon_api.get_representation_parents(project_name, repre_id)

    if version == -1:
        new_version = ayon_api.get_last_version_by_product_id(
            project_name, product_entity["id"]
        )
    else:
        new_version = ayon_api.get_version_by_name(
            project_name, version, product_entity["id"]
        )

    if new_version is None:
        raise ValueError("Version not found: {}".format(version))

    # Get the new representation (new file)
    new_representation = ayon_api.get_representation_by_name(
        project_name, current_representation["name"], new_version["id"]
    )
    # TODO there is 'get_representation_context' to get the context which
    #   could be possible to use here
    new_context = {
        "project": project_entity,
        "folder": folder_entity,
        "product": product_entity,
        "version": version_entity,
        "representation": new_representation,
    }
    update_package(container, new_context)

update_scene(set_container, containers, current_data, new_data, new_file)

Updates the hierarchy, assets and their matrix

Updates the following within the scene
  • Setdress hierarchy alembic
  • Matrix
  • Parenting
  • Representations

It removes any assets which are not present in the new build data

Parameters:

Name Type Description Default
set_container dict

the setdress container of the scene

required
containers list

the list of containers under the setdress container

required
current_data dict

the current build data of the setdress

required
new_data dict

the new build data of the setdres

required

Returns:

Name Type Description
processed_containers list

all new and updated containers

Source code in client/ayon_maya/api/setdress.py
366
367
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
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
474
475
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
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def update_scene(set_container, containers, current_data, new_data, new_file):
    """Updates the hierarchy, assets and their matrix

    Updates the following within the scene:
        * Setdress hierarchy alembic
        * Matrix
        * Parenting
        * Representations

    It removes any assets which are not present in the new build data

    Args:
        set_container (dict): the setdress container of the scene
        containers (list): the list of containers under the setdress container
        current_data (dict): the current build data of the setdress
        new_data (dict): the new build data of the setdres

    Returns:
        processed_containers (list): all new and updated containers

    """

    set_namespace = set_container['namespace']
    project_name = get_current_project_name()

    # Update the setdress hierarchy alembic
    set_root = get_container_transforms(set_container, root=True)
    set_hierarchy_root = cmds.listRelatives(set_root, fullPath=True)[0]
    set_hierarchy_reference = cmds.referenceQuery(set_hierarchy_root,
                                                  referenceNode=True)
    new_alembic = new_file.replace(".json", ".abc")
    assert os.path.exists(new_alembic), "%s does not exist." % new_alembic
    with unlocked(cmds.listRelatives(set_root, ad=True, fullPath=True)):
        cmds.file(new_alembic,
                  loadReference=set_hierarchy_reference,
                  type="Alembic")

    identity = DEFAULT_MATRIX[:]

    processed_namespaces = set()
    processed_containers = list()

    new_lookup = _instances_by_namespace(new_data)
    old_lookup = _instances_by_namespace(current_data)
    repre_ids = set()
    containers_for_repre_compare = []
    for container in containers:
        container_ns = container['namespace']

        # Consider it processed here, even it it fails we want to store that
        # the namespace was already available.
        processed_namespaces.add(container_ns)
        processed_containers.append(container['objectName'])

        if container_ns not in new_lookup:
            # Remove this container because it's not in the new data
            log.warning("Removing content: %s", container_ns)
            remove_container(container)
            continue

        root = get_container_transforms(container, root=True)
        if not root:
            log.error("Can't find root for %s", container['objectName'])
            continue

        old_instance = old_lookup.get(container_ns, {})
        new_instance = new_lookup[container_ns]

        # Update the matrix
        # check matrix against old_data matrix to find local overrides
        current_matrix = cmds.xform(root,
                                    query=True,
                                    matrix=True,
                                    objectSpace=True)

        original_matrix = old_instance.get("matrix", identity)
        has_matrix_override = not matrix_equals(current_matrix,
                                                original_matrix)

        if has_matrix_override:
            log.warning("Matrix override preserved on %s", container_ns)
        else:
            new_matrix = new_instance.get("matrix", identity)
            cmds.xform(root, matrix=new_matrix, objectSpace=True)

        # Update the parenting
        if old_instance.get("parent", None) != new_instance["parent"]:

            parent = to_namespace(new_instance['parent'], set_namespace)
            if not cmds.objExists(parent):
                log.error("Can't find parent %s", parent)
                continue

            # Set the new parent
            cmds.lockNode(root, lock=False)
            root = cmds.parent(root, parent, relative=True)
            cmds.lockNode(root, lock=True)

        # Update the representation
        representation_current = container['representation']
        representation_old = old_instance['representation']
        representation_new = new_instance['representation']
        has_representation_override = (representation_current !=
                                       representation_old)

        if representation_new == representation_current:
            continue

        if has_representation_override:
            log.warning("Your scene had local representation "
                        "overrides within the set. New "
                        "representations not loaded for %s.",
                        container_ns)
            continue

        # We check it against the current 'loader' in the scene instead
        # of the original data of the package that was loaded because
        # an Artist might have made scene local overrides
        if new_instance['loader'] != container['loader']:
            log.warning("Loader is switched - local edits will be "
                        "lost. Removing: %s",
                        container_ns)

            # Remove this from the "has been processed" list so it's
            # considered as new element and added afterwards.
            processed_containers.pop()
            processed_namespaces.remove(container_ns)
            remove_container(container)
            continue

        # Check whether the conversion can be done by the Loader.
        # They *must* use the same folder, product and Loader for
        # `update_container` to make sense.
        repre_ids.add(representation_current)
        repre_ids.add(representation_new)

        containers_for_repre_compare.append(
            (container, representation_current, representation_new)
        )

    repre_entities_by_id = {
        repre_entity["id"]: repre_entity
        for repre_entity in ayon_api.get_representations(
            project_name, representation_ids=repre_ids
        )
    }
    repre_parents_by_id = ayon_api.get_representations_parents(
        project_name, repre_ids
    )
    for (
        container,
        repre_current_id,
        repre_new_id
    ) in containers_for_repre_compare:
        current_repre = repre_entities_by_id[repre_current_id]
        current_parents = repre_parents_by_id[repre_current_id]
        new_repre = repre_entities_by_id[repre_new_id]
        new_parents = repre_parents_by_id[repre_new_id]

        is_valid = compare_representations(
            current_repre, current_parents, new_repre, new_parents
        )
        if not is_valid:
            log.error("Skipping: %s. See log for details.",
                      container["namespace"])
            continue

        new_version = new_parents.version["version"]
        update_container(container, version=new_version)

    # Add new assets
    all_loaders = discover_loader_plugins()
    for representation_id, instances in new_data.items():

        # Find the compatible loaders
        loaders = loaders_from_representation(
            all_loaders, representation_id
        )
        for instance in instances:

            # Already processed in update functionality
            if instance['namespace'] in processed_namespaces:
                continue

            container = _add(instance=instance,
                             representation_id=representation_id,
                             loaders=loaders,
                             namespace=set_container['namespace'],
                             root=set_root)

            # Add to the setdress container
            cmds.sets(container,
                      addElement=set_container['objectName'])

            processed_containers.append(container)

    return processed_containers