From c72539d74162464da2c263393a5f44b387dfb2a2 Mon Sep 17 00:00:00 2001 From: Jacky Date: Thu, 23 Feb 2023 10:20:18 +0800 Subject: [PATCH] Support C4D Load and Manage --- avalon/cinema4d/lib.py | 62 ++++++++++++++++++- avalon/cinema4d/pipeline.py | 120 +++++++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 4 deletions(-) diff --git a/avalon/cinema4d/lib.py b/avalon/cinema4d/lib.py index 12a0d5ceb..cfe435ea5 100644 --- a/avalon/cinema4d/lib.py +++ b/avalon/cinema4d/lib.py @@ -5,6 +5,60 @@ AVALON_TAB = "avalon" +def unique_namespace(namespace, format="%02d", prefix="", suffix=""): + """Return unique namespace + + Similar to :func:`unique_name` but evaluating namespaces + as opposed to object names. + + Arguments: + namespace (str): Name of namespace to consider + format (str, optional): Formatting of the given iteration number + suffix (str, optional): Only consider namespaces with this suffix. + + """ + + iteration = 1 + unique = prefix + (namespace + format % iteration) + suffix + + # The `existing` set does not just contain the namespaces but *all* nodes + # within "current namespace". We need all because the namespace could + # also clash with a node name. To be truly unique and valid one needs to + # check against all. + doc = c4d.documents.GetActiveDocument() + iterator = doc.GetFirstObject() + existing = [node[c4d.ID_CA_XREF_NAMESPACE] + for node in iterate_nodes(iterator, node_type="XRef")] + while unique in existing: + iteration += 1 + unique = prefix + (namespace + format % iteration) + suffix + + return unique + + +def iterate_nodes(iterator, node_type=None): + while iterator: + if node_type and iterator.GetTypeName() != node_type: + pass + else: + yield iterator + for node in iterate_nodes(iterator.GetDown(), node_type=node_type): + yield node + iterator = iterator.GetNext() + + +def iterate_children(node, node_type=None): + iterator = node.GetDown() + while iterator: + if node_type and iterator.GetTypeName() != node_type: + pass + else: + yield iterator + for node in iterate_children(iterator, node_type=node_type): + yield node + iterator = iterator.GetNext() + + def read(obj): """Return user-defined attributes from `obj`""" @@ -38,12 +92,14 @@ def imprint(obj, data): data (dict): Dictionary of key/value pairs """ - exists_keys = [] + exists_keys = {} for cid, bc in obj.GetUserDataContainer(): - exists_keys.append(bc[c4d.DESC_NAME]) + exists_keys[bc[c4d.DESC_NAME]] = cid for key, value in data.items(): - if key in exists_keys: + if key in exists_keys.keys(): + cid = exists_keys[key] + obj[cid] = value continue if callable(value): diff --git a/avalon/cinema4d/pipeline.py b/avalon/cinema4d/pipeline.py index d3b6c80a4..958113177 100644 --- a/avalon/cinema4d/pipeline.py +++ b/avalon/cinema4d/pipeline.py @@ -11,9 +11,13 @@ from ..lib import logger, find_submodule from .. import api +from ..pipeline import AVALON_CONTAINER_ID + self = sys.modules[__name__] self._menu = None +AVALON_CONTAINERS = "AVALON_CONTAINERS" + class AvalonContextLabel(c4d.plugins.CommandData): PLUGIN_ID = 999000001 @@ -202,10 +206,123 @@ def _uninstall_menu(): """ +def containerise(name, + namespace, + nodes, + context, + loader=None, + suffix="CON"): + """Bundle `nodes` into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + nodes (list): Long names of nodes to containerise + context (dict): Asset information + loader (str, optional): Name of loader used to produce this container. + suffix (str, optional): Suffix of container, defaults to `_CON`. + + Returns: + container (str): Name of container assembly + + """ + doc = c4d.documents.GetActiveDocument() + + container = c4d.BaseObject(c4d.Oselection) + doc.InsertObject(container) + container[c4d.ID_BASELIST_NAME] = "%s_%s_%s" % (namespace, name, suffix) + + data = { + "schema": "avalon-core:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace, + "loader": str(loader), + "representation": str(context["representation"]["_id"]) + } + + lib.imprint(container, data) + + main_container = doc.SearchObject(AVALON_CONTAINERS) + if not main_container: + main_container = c4d.BaseObject(c4d.Oselection) + doc.InsertObject(main_container) + main_container[c4d.ID_BASELIST_NAME] = AVALON_CONTAINERS + + # Hide main_container in Object Manager + main_container.ChangeNBit(c4d.NBIT_OHIDE, c4d.NBITCONTROL_TOGGLE) + + selection_list = main_container[c4d.SELECTIONOBJECT_LIST] + selection_list.InsertObject(container, 1) + main_container[c4d.SELECTIONOBJECT_LIST] = selection_list + + # Hide container in Object Manager + container.ChangeNBit(c4d.NBIT_OHIDE, c4d.NBITCONTROL_TOGGLE) + + return container + + +def parse_container(container): + """Return the container node's full container data. + + Args: + container (node): A container node. + + Returns: + dict: The container schema data for this container node. + + """ + data = lib.read(container) + + # Backwards compatibility pre-schemas for containers + data["schema"] = data.get("schema", "avalon-core:container-1.0") + + # Append transient data + data["objectName"] = container[c4d.ID_BASELIST_NAME] + + return data + + +def _ls(): + """Yields Avalon container node names. + + Used by `ls()` to retrieve the nodes and then query the full container's + data. + + Yields: + str: Avalon container node name (Selection Object) + + """ + + ids = {AVALON_CONTAINER_ID, + # Backwards compatibility + "pyblish.mindbender.container"} + + # Iterate over all 'Selection Object' nodes in the scene to detect + # whether they have the avalon container ".id" attribute. + doc = c4d.documents.GetActiveDocument() + iterator = doc.GetFirstObject() + for node in lib.iterate_nodes(iterator, node_type="Selection"): + + for cid, bc in node.GetUserDataContainer(): + if bc[c4d.DESC_NAME] == "id": + value = node[cid] + if value in ids: + yield node + break + + def ls(): """Yields containers from active Cinema 4D scene """ - return [] + containers = _ls() + + for container in containers: + data = parse_container(container) + yield data class Creator(api.Creator): @@ -215,6 +332,7 @@ def process(self): doc = c4d.documents.GetActiveDocument() instance = c4d.BaseObject(c4d.Oselection) instance[c4d.ID_BASELIST_NAME] = self.name + instance.ChangeNBit(c4d.NBIT_NO_DELETE, c4d.NBITCONTROL_TOGGLE) lib.imprint(instance, self.data) if (self.options or {}).get("useSelection"):