diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py new file mode 100644 index 0000000000..0b027c02ea --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -0,0 +1,211 @@ +from ayon_api import ( + get_folder_by_name, + get_folder_by_path, + get_folders, +) +from maya import cmds # noqa: F401 + +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_assets +from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef, EnumDef, TextDef +from openpype.pipeline import ( + Creator, + get_current_asset_name, + get_current_project_name, +) +from openpype.pipeline.create import CreatorError + + +class CreateMultishotLayout(plugin.MayaCreator): + """Create a multi-shot layout in the Maya scene. + + This creator will create a Camera Sequencer in the Maya scene based on + the shots found under the specified folder. The shots will be added to + the sequencer in the order of their clipIn and clipOut values. For each + shot a Layout will be created. + + """ + identifier = "io.openpype.creators.maya.multishotlayout" + label = "Multi-shot Layout" + family = "layout" + icon = "project-diagram" + + def get_pre_create_attr_defs(self): + # Present artist with a list of parents of the current context + # to choose from. This will be used to get the shots under the + # selected folder to create the Camera Sequencer. + + """ + Todo: `get_folder_by_name` should be switched to `get_folder_by_path` + once the fork to pure AYON is done. + + Warning: this will not work for projects where the asset name + is not unique across the project until the switch mentioned + above is done. + """ + + current_folder = get_folder_by_name( + project_name=get_current_project_name(), + folder_name=get_current_asset_name(), + ) + + current_path_parts = current_folder["path"].split("/") + + # populate the list with parents of the current folder + # this will create menu items like: + # [ + # { + # "value": "", + # "label": "project (shots directly under the project)" + # }, { + # "value": "shots/shot_01", "label": "shot_01 (current)" + # }, { + # "value": "shots", "label": "shots" + # } + # ] + + # add the project as the first item + items_with_label = [ + { + "label": f"{self.project_name} " + "(shots directly under the project)", + "value": "" + } + ] + + # go through the current folder path and add each part to the list, + # but mark the current folder. + for part_idx, part in enumerate(current_path_parts): + label = part + if label == current_folder["name"]: + label = f"{label} (current)" + + value = "/".join(current_path_parts[:part_idx + 1]) + + items_with_label.append({"label": label, "value": value}) + + return [ + EnumDef("shotParent", + default=current_folder["name"], + label="Shot Parent Folder", + items=items_with_label, + ), + BoolDef("groupLoadedAssets", + label="Group Loaded Assets", + tooltip="Enable this when you want to publish group of " + "loaded asset", + default=False), + TextDef("taskName", + label="Associated Task Name", + tooltip=("Task name to be associated " + "with the created Layout"), + default="layout"), + ] + + def create(self, subset_name, instance_data, pre_create_data): + shots = list( + self.get_related_shots(folder_path=pre_create_data["shotParent"]) + ) + if not shots: + # There are no shot folders under the specified folder. + # We are raising an error here but in the future we might + # want to create a new shot folders by publishing the layouts + # and shot defined in the sequencer. Sort of editorial publish + # in side of Maya. + raise CreatorError(( + "No shots found under the specified " + f"folder: {pre_create_data['shotParent']}.")) + + # Get layout creator + layout_creator_id = "io.openpype.creators.maya.layout" + layout_creator: Creator = self.create_context.creators.get( + layout_creator_id) + if not layout_creator: + raise CreatorError( + f"Creator {layout_creator_id} not found.") + + # Get OpenPype style asset documents for the shots + op_asset_docs = get_assets( + self.project_name, [s["id"] for s in shots]) + asset_docs_by_id = {doc["_id"]: doc for doc in op_asset_docs} + for shot in shots: + # we are setting shot name to be displayed in the sequencer to + # `shot name (shot label)` if the label is set, otherwise just + # `shot name`. So far, labels are used only when the name is set + # with characters that are not allowed in the shot name. + if not shot["active"]: + continue + + # get task for shot + asset_doc = asset_docs_by_id[shot["id"]] + + tasks = asset_doc.get("data").get("tasks").keys() + layout_task = None + if pre_create_data["taskName"] in tasks: + layout_task = pre_create_data["taskName"] + + shot_name = f"{shot['name']}%s" % ( + f" ({shot['label']})" if shot["label"] else "") + cmds.shot(sequenceStartTime=shot["attrib"]["clipIn"], + sequenceEndTime=shot["attrib"]["clipOut"], + shotName=shot_name) + + # Create layout instance by the layout creator + + instance_data = { + "asset": shot["name"], + "variant": layout_creator.get_default_variant() + } + if layout_task: + instance_data["task"] = layout_task + + layout_creator.create( + subset_name=layout_creator.get_subset_name( + layout_creator.get_default_variant(), + self.create_context.get_current_task_name(), + asset_doc, + self.project_name), + instance_data=instance_data, + pre_create_data={ + "groupLoadedAssets": pre_create_data["groupLoadedAssets"] + } + ) + + def get_related_shots(self, folder_path: str): + """Get all shots related to the current asset. + + Get all folders of type Shot under specified folder. + + Args: + folder_path (str): Path of the folder. + + Returns: + list: List of dicts with folder data. + + """ + # if folder_path is None, project is selected as a root + # and its name is used as a parent id + parent_id = self.project_name + if folder_path: + current_folder = get_folder_by_path( + project_name=self.project_name, + folder_path=folder_path, + ) + parent_id = current_folder["id"] + + # get all child folders of the current one + return get_folders( + project_name=self.project_name, + parent_ids=[parent_id], + fields=[ + "attrib.clipIn", "attrib.clipOut", + "attrib.frameStart", "attrib.frameEnd", + "name", "label", "path", "folderType", "id" + ] + ) + + +# blast this creator if Ayon server is not enabled +if not AYON_SERVER_ENABLED: + del CreateMultishotLayout diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 11e2b8a36c..84e873589d 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -1,6 +1,7 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +from ayon_server.settings import task_types_enum class CreateLookModel(BaseSettingsModel): @@ -120,6 +121,16 @@ class CreateVrayProxyModel(BaseSettingsModel): default_factory=list, title="Default Products") +class CreateMultishotLayout(BasicCreatorModel): + shotParent: str = Field(title="Shot Parent Folder") + groupLoadedAssets: bool = Field(title="Group Loaded Assets") + task_type: list[str] = Field( + title="Task types", + enum_resolver=task_types_enum + ) + task_name: str = Field(title="Task name (regex)") + + class CreatorsModel(BaseSettingsModel): CreateLook: CreateLookModel = Field( default_factory=CreateLookModel,