Merge pull request #1662 from pypeclub/bugfix/tvpaint_custom_subset_name

TVPaint custom subset template
This commit is contained in:
Jakub Trllo 2021-06-11 10:17:02 +02:00 committed by GitHub
commit ee883ece89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 80 deletions

View file

@ -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"],

View file

@ -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:

View file

@ -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 = {

View file

@ -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({

View file

@ -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

View file

@ -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
)

View file

@ -215,6 +215,17 @@
"hosts": [],
"tasks": [],
"template": "{family}{Task}{Variant}"
},
{
"families": [
"renderLayer",
"renderPass"
],
"hosts": [
"tvpaint"
],
"tasks": [],
"template": "{family}{Task}_{Render_layer}_{Render_pass}"
}
]
},