diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 3eb2f5871d..eeb7d32d50 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,9 +1,11 @@ +from avalon.api import CreatorError from avalon.tvpaint import ( pipeline, lib, CommunicationWrapper ) from openpype.hosts.tvpaint.api import plugin +from openpype.lib import prepare_template_data class CreateRenderlayer(plugin.Creator): @@ -15,13 +17,31 @@ class CreateRenderlayer(plugin.Creator): defaults = ["Main"] rename_group = True + render_pass = "beauty" - subset_template = "{family}_{name}" rename_script_template = ( "tv_layercolor \"setcolor\"" " {clip_id} {group_id} {r} {g} {b} \"{name}\"" ) + dynamic_subset_keys = ["render_pass", "render_layer", "group"] + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateRenderlayer, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + # Use render pass name from creator's plugin + dynamic_data["render_pass"] = cls.render_pass + # Add variant to render layer + dynamic_data["render_layer"] = variant + # Change family for subset name fill + dynamic_data["family"] = "render" + + return dynamic_data + @classmethod def get_default_variant(cls): """Default value for variant in Creator tool. @@ -70,34 +90,44 @@ class CreateRenderlayer(plugin.Creator): # Raise if there is no selection if not group_ids: - raise AssertionError("Nothing is selected.") + raise CreatorError("Nothing is selected.") # This creator should run only on one group if len(group_ids) > 1: - raise AssertionError("More than one group is in selection.") + raise CreatorError("More than one group is in selection.") group_id = tuple(group_ids)[0] # If group id is `0` it is `default` group which is invalid if group_id == 0: - raise AssertionError( + raise CreatorError( "Selection is not in group. Can't mark selection as Beauty." ) self.log.debug(f"Selected group id is \"{group_id}\".") self.data["group_id"] = group_id - family = self.data["family"] - # Extract entered name - name = self.data["subset"][len(family):] - self.log.info(f"Extracted name from subset name \"{name}\".") - self.data["name"] = name + group_data = lib.groups_data() + group_name = None + for group in group_data: + if group["group_id"] == group_id: + group_name = group["name"] + break - # Change subset name by template - subset_name = self.subset_template.format(**{ - "family": self.family, - "name": name - }) - self.log.info(f"New subset name \"{subset_name}\".") + if group_name is None: + raise AssertionError( + "Couldn't find group by id \"{}\"".format(group_id) + ) + + subset_name_fill_data = { + "group": group_name + } + + family = self.family = self.data["family"] + + # Fill dynamic key 'group' + subset_name = self.data["subset"].format( + **prepare_template_data(subset_name_fill_data) + ) self.data["subset"] = subset_name # Check for instances of same group @@ -153,7 +183,7 @@ class CreateRenderlayer(plugin.Creator): # Rename TVPaint group (keep color same) # - groups can't contain spaces - new_group_name = name.replace(" ", "_") + new_group_name = self.data["variant"].replace(" ", "_") rename_script = self.rename_script_template.format( clip_id=selected_group["clip_id"], group_id=selected_group["group_id"], diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 58158685c7..ad06520210 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -1,9 +1,11 @@ +from avalon.api import CreatorError from avalon.tvpaint import ( pipeline, lib, CommunicationWrapper ) from openpype.hosts.tvpaint.api import plugin +from openpype.lib import prepare_template_data class CreateRenderPass(plugin.Creator): @@ -18,7 +20,19 @@ class CreateRenderPass(plugin.Creator): icon = "cube" defaults = ["Main"] - subset_template = "{family}_{render_layer}_{pass}" + dynamic_subset_keys = ["render_pass", "render_layer"] + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateRenderPass, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + dynamic_data["render_pass"] = variant + dynamic_data["family"] = "render" + + return dynamic_data @classmethod def get_default_variant(cls): @@ -66,11 +80,11 @@ class CreateRenderPass(plugin.Creator): # Raise if nothing is selected if not selected_layers: - raise AssertionError("Nothing is selected.") + raise CreatorError("Nothing is selected.") # Raise if layers from multiple groups are selected if len(group_ids) != 1: - raise AssertionError("More than one group is in selection.") + raise CreatorError("More than one group is in selection.") group_id = tuple(group_ids)[0] self.log.debug(f"Selected group id is \"{group_id}\".") @@ -87,34 +101,40 @@ class CreateRenderPass(plugin.Creator): # Beauty is required for this creator so raise if was not found if beauty_instance is None: - raise AssertionError("Beauty pass does not exist yet.") + raise CreatorError("Beauty pass does not exist yet.") - render_layer = beauty_instance["name"] + subset_name = self.data["subset"] + + subset_name_fill_data = {} + + # Backwards compatibility + # - beauty may be created with older creator where variant was not + # stored + if "variant" not in beauty_instance: + render_layer = beauty_instance["name"] + else: + render_layer = beauty_instance["variant"] + + subset_name_fill_data["render_layer"] = render_layer + + # Format dynamic keys in subset name + new_subset_name = subset_name.format( + **prepare_template_data(subset_name_fill_data) + ) + self.data["subset"] = new_subset_name + self.log.info(f"New subset name is \"{new_subset_name}\".") - # Extract entered name family = self.data["family"] - name = self.data["subset"] - # Is this right way how to get name? - name = name[len(family):] - self.log.info(f"Extracted name from subset name \"{name}\".") + variant = self.data["variant"] self.data["group_id"] = group_id - self.data["pass"] = name + self.data["pass"] = variant self.data["render_layer"] = render_layer # Collect selected layer ids to be stored into instance layer_names = [layer["name"] for layer in selected_layers] self.data["layer_names"] = layer_names - # Replace `beauty` in beauty's subset name with entered name - subset_name = self.subset_template.format(**{ - "family": family, - "render_layer": render_layer, - "pass": name - }) - self.data["subset"] = subset_name - self.log.info(f"New subset name is \"{subset_name}\".") - # Check if same instance already exists existing_instance = None existing_instance_idx = None @@ -122,7 +142,7 @@ class CreateRenderPass(plugin.Creator): if ( instance["family"] == family and instance["group_id"] == group_id - and instance["pass"] == name + and instance["pass"] == variant ): existing_instance = instance existing_instance_idx = idx @@ -131,7 +151,7 @@ class CreateRenderPass(plugin.Creator): if existing_instance is not None: self.log.info( f"Render pass instance for group id {group_id}" - f" and name \"{name}\" already exists, overriding." + f" and name \"{variant}\" already exists, overriding." ) instances[existing_instance_idx] = self.data else: diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 9b11f9fe80..4468bfae40 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -4,6 +4,8 @@ import copy import pyblish.api from avalon import io +from openpype.lib import get_subset_name + class CollectInstances(pyblish.api.ContextPlugin): label = "Collect Instances" @@ -62,9 +64,38 @@ class CollectInstances(pyblish.api.ContextPlugin): # Different instance creation based on family instance = None if family == "review": - # Change subset name + # Change subset name of review instance + + # Collect asset doc to get asset id + # - not sure if it's good idea to require asset id in + # get_subset_name? + asset_name = context.data["workfile_context"]["asset"] + asset_doc = io.find_one( + { + "type": "asset", + "name": asset_name + }, + {"_id": 1} + ) + asset_id = None + if asset_doc: + asset_id = asset_doc["_id"] + + # Project name from workfile context + project_name = context.data["workfile_context"]["project"] + # Host name from environemnt variable + host_name = os.environ["AVALON_APP"] + # Use empty variant value + variant = "" task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}".format(family, task_name.capitalize()) + new_subset_name = get_subset_name( + family, + variant, + task_name, + asset_id, + project_name, + host_name + ) instance_data["subset"] = new_subset_name instance = context.create_instance(**instance_data) @@ -119,19 +150,23 @@ class CollectInstances(pyblish.api.ContextPlugin): name = instance_data["name"] # Change label subset_name = instance_data["subset"] - instance_data["label"] = "{}_Beauty".format(name) - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_Beauty".format( - new_family, task_name.capitalize(), name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - subset_name, new_subset_name - )) + # Backwards compatibility + # - subset names were not stored as final subset names during creation + if "variant" not in instance_data: + instance_data["label"] = "{}_Beauty".format(name) + + # Change subset name + # Final family of an instance will be `render` + new_family = "render" + task_name = io.Session["AVALON_TASK"] + new_subset_name = "{}{}_{}_Beauty".format( + new_family, task_name.capitalize(), name + ) + instance_data["subset"] = new_subset_name + self.log.debug("Changed subset name \"{}\"->\"{}\"".format( + subset_name, new_subset_name + )) # Get all layers for the layer layers_data = context.data["layersData"] @@ -163,20 +198,23 @@ class CollectInstances(pyblish.api.ContextPlugin): ) # Change label render_layer = instance_data["render_layer"] - instance_data["label"] = "{}_{}".format(render_layer, pass_name) - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - old_subset_name = instance_data["subset"] - task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_{}".format( - new_family, task_name.capitalize(), render_layer, pass_name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - old_subset_name, new_subset_name - )) + # Backwards compatibility + # - subset names were not stored as final subset names during creation + if "variant" not in instance_data: + instance_data["label"] = "{}_{}".format(render_layer, pass_name) + # Change subset name + # Final family of an instance will be `render` + new_family = "render" + old_subset_name = instance_data["subset"] + task_name = io.Session["AVALON_TASK"] + new_subset_name = "{}{}_{}_{}".format( + new_family, task_name.capitalize(), render_layer, pass_name + ) + instance_data["subset"] = new_subset_name + self.log.debug("Changed subset name \"{}\"->\"{}\"".format( + old_subset_name, new_subset_name + )) layers_data = context.data["layersData"] layers_by_name = { diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index b059be90bf..b61fec895f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -3,6 +3,8 @@ import json import pyblish.api from avalon import io +from openpype.lib import get_subset_name + class CollectWorkfile(pyblish.api.ContextPlugin): label = "Collect Workfile" @@ -20,8 +22,38 @@ class CollectWorkfile(pyblish.api.ContextPlugin): basename, ext = os.path.splitext(filename) instance = context.create_instance(name=basename) + # Get subset name of workfile instance + # Collect asset doc to get asset id + # - not sure if it's good idea to require asset id in + # get_subset_name? + family = "workfile" + asset_name = context.data["workfile_context"]["asset"] + asset_doc = io.find_one( + { + "type": "asset", + "name": asset_name + }, + {"_id": 1} + ) + asset_id = None + if asset_doc: + asset_id = asset_doc["_id"] + + # Project name from workfile context + project_name = context.data["workfile_context"]["project"] + # Host name from environemnt variable + host_name = os.environ["AVALON_APP"] + # Use empty variant value + variant = "" task_name = io.Session["AVALON_TASK"] - subset_name = "workfile" + task_name.capitalize() + subset_name = get_subset_name( + family, + variant, + task_name, + asset_id, + project_name, + host_name + ) # Create Workfile instance instance.data.update({ diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index a5254af0da..1f2fb7a46e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -34,7 +34,8 @@ def get_subset_name( asset_id, project_name=None, host_name=None, - default_template=None + default_template=None, + dynamic_data=None ): if not family: return "" @@ -68,11 +69,16 @@ def get_subset_name( if not task_name and "{task" in template.lower(): raise TaskNotSetError() - fill_pairs = ( - ("variant", variant), - ("family", family), - ("task", task_name) - ) + fill_pairs = { + "variant": variant, + "family": family, + "task": task_name + } + if dynamic_data: + # Dynamic data may override default values + for key, value in dynamic_data.items(): + fill_pairs[key] = value + return template.format(**prepare_template_data(fill_pairs)) @@ -91,7 +97,8 @@ def prepare_template_data(fill_pairs): """ fill_data = {} - for key, value in fill_pairs: + regex = re.compile(r"[a-zA-Z0-9]") + for key, value in dict(fill_pairs).items(): # Handle cases when value is `None` (standalone publisher) if value is None: continue @@ -102,13 +109,18 @@ def prepare_template_data(fill_pairs): # Capitalize only first char of value # - conditions are because of possible index errors + # - regex is to skip symbols that are not chars or numbers + # - e.g. "{key}" which starts with curly bracket capitalized = "" - if value: - # Upper first character - capitalized += value[0].upper() - # Append rest of string if there is any - if len(value) > 1: - capitalized += value[1:] + for idx in range(len(value or "")): + char = value[idx] + if not regex.match(char): + capitalized += char + else: + capitalized += char.upper() + capitalized += value[idx + 1:] + break + fill_data[key.capitalize()] = capitalized return fill_data diff --git a/openpype/plugin.py b/openpype/plugin.py index 8ade0f3825..45c9a08209 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -16,13 +16,59 @@ class PypeCreatorMixin: Mixin class must be used as first in inheritance order to override methods. """ + dynamic_subset_keys = [] + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + """Return dynamic data for current Creator plugin. + + By default return keys from `dynamic_subset_keys` attribute as mapping + to keep formatted template unchanged. + + ``` + dynamic_subset_keys = ["my_key"] + --- + output = { + "my_key": "{my_key}" + } + ``` + + Dynamic keys may override default Creator keys (family, task, asset, + ...) but do it wisely if you need. + + All of keys will be converted into 3 variants unchanged, capitalized + and all upper letters. Because of that are all keys lowered. + + This method can be modified to prefill some values just keep in mind it + is class method. + + Returns: + dict: Fill data for subset name template. + """ + dynamic_data = {} + for key in cls.dynamic_subset_keys: + key = key.lower() + dynamic_data[key] = "{" + key + "}" + return dynamic_data @classmethod def get_subset_name( cls, variant, task_name, asset_id, project_name, host_name=None ): + dynamic_data = cls.get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + return get_subset_name( - cls.family, variant, task_name, asset_id, project_name, host_name + cls.family, + variant, + task_name, + asset_id, + project_name, + host_name, + dynamic_data=dynamic_data ) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 5f779fccfa..4351f18a60 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -215,6 +215,17 @@ "hosts": [], "tasks": [], "template": "{family}{Task}{Variant}" + }, + { + "families": [ + "renderLayer", + "renderPass" + ], + "hosts": [ + "tvpaint" + ], + "tasks": [], + "template": "{family}{Task}_{Render_layer}_{Render_pass}" } ] },