From 5692d570b760f7c1de00a320b84f564918b431e5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:08:36 +0200 Subject: [PATCH 01/42] #680 - Toggle Ftrack upload in StandalonePublisher Removed default_family in collect_context --- .../plugins/publish/collect_ftrack_family.py | 68 +++++++++++++------ .../defaults/project_settings/ftrack.json | 31 +++++++++ .../project_settings/standalonepublisher.json | 21 ++++-- .../schema_project_ftrack.json | 53 +++++++++++++++ 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e6daed9a33..85e9c4ab9e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -1,29 +1,57 @@ +""" +Requires: + none + +Provides: + instance -> families ([]) +""" +import os import pyblish.api +from openpype.lib.plugin_tools import filter_profiles -class CollectFtrackFamilies(pyblish.api.InstancePlugin): - """Collect family for ftrack publishing - - Add ftrack family to those instance that should be published to ftrack +class CollectFtrackFamily(pyblish.api.InstancePlugin): """ + Adds explicitly 'ftrack' to families to upload instance to FTrack. - order = pyblish.api.CollectorOrder + 0.3 - label = 'Add ftrack family' - families = ["model", - "setdress", - "model", - "animation", - "look", - "rig", - "camera" - ] - hosts = ["maya"] + Uses selection by combination of hosts/families/tasks names via + profiles resolution. + + Triggered everywhere, checks instance against configured + """ + label = "Collect Ftrack Family" + order = pyblish.api.CollectorOrder + 0.4999 + + profiles = None def process(self, instance): + if self.profiles: + anatomy_data = instance.context.data["anatomyData"] + task_name = anatomy_data.get("task", + os.environ["AVALON_TASK"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - # make ftrack publishable - if instance.data.get('families'): - instance.data['families'].append('ftrack') - else: - instance.data['families'] = ['ftrack'] + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile: + families = instance.data.get("families") + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + self.log.debug("Removing ftrack family if present") + if families and "ftrack" in families: + instance.data["families"].pop("ftrack") + + self.log.debug("instance.data:: {}".format(instance.data)) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index b964ce07c3..26361a091a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -200,6 +200,37 @@ } }, "publish": { + "CollectFtrackFamily": { + "enabled": true, + "profiles": [ + { + "families": [ + "render", + "image" + ], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": true + }, + { + "families": [ + "model", + "setdress", + "animation", + "look", + "rig", + "camera" + ], + "tasks": [], + "hosts": [ + "maya" + ], + "add_ftrack_family": true + } + ] + }, "IntegrateFtrackNote": { "enabled": true, "note_with_intent_template": "{intent}: {comment}", diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 979f5285d3..8e833de7e0 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -105,16 +105,23 @@ "label": "Render", "family": "render", "icon": "image", - "defaults": ["Animation", "Lighting", "Lookdev", "Compositing"], + "defaults": [ + "Animation", + "Lighting", + "Lookdev", + "Compositing" + ], "help": "Rendered images or video files" }, "create_mov_batch": { - "name": "mov_batch", - "label": "Batch Mov", - "family": "render_mov_batch", - "icon": "image", - "defaults": ["Main"], - "help": "Process multiple Mov files and publish them for layout and comp." + "name": "mov_batch", + "label": "Batch Mov", + "family": "render_mov_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Process multiple Mov files and publish them for layout and comp." }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index aae2bb2539..af85ccb254 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -604,6 +604,59 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFtrackFamily", + "label": "Collect Ftrack Family", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "collapsible": true, + "key": "profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From dad17b9184be8d96c9ca39b9028d7f3e32675ce9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:27:08 +0200 Subject: [PATCH 02/42] #680 - fixed where to check for Task definition Modified default profile for StandalonePublisher --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 4 ++-- openpype/settings/defaults/project_settings/ftrack.json | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 85e9c4ab9e..a66d4217b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -28,8 +28,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): def process(self, instance): if self.profiles: anatomy_data = instance.context.data["anatomyData"] - task_name = anatomy_data.get("task", - os.environ["AVALON_TASK"]) + task_name = instance.data("task", + instance.context.data["task"]) host_name = anatomy_data.get("app", os.environ["AVALON_APP"]) family = instance.data["family"] diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 26361a091a..a8e121d7c3 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,10 +204,7 @@ "enabled": true, "profiles": [ { - "families": [ - "render", - "image" - ], + "families": [], "tasks": [], "hosts": [ "standalonepublisher" From 137097631d3f0189c380b94f4e853fb67746c810 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:30:37 +0200 Subject: [PATCH 03/42] #680 - added warning if no profiles configured, changed if order --- .../plugins/publish/collect_ftrack_family.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66d4217b5..e910970290 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -26,32 +26,30 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - if self.profiles: - anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) - host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) - family = instance.data["family"] + if not self.profiles: + self.log.warning("No profiles present for adding Ftrack family") + return - filtering_criteria = { - "hosts": host_name, - "families": family, - "tasks": task_name - } - profile = filter_profiles(self.profiles, filtering_criteria) + anatomy_data = instance.context.data["anatomyData"] + task_name = instance.data("task", + instance.context.data["task"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - if profile: - families = instance.data.get("families") - if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] - else: - self.log.debug("Removing ftrack family if present") - if families and "ftrack" in families: - instance.data["families"].pop("ftrack") + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile and profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + families = instance.data.get("families") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] self.log.debug("instance.data:: {}".format(instance.data)) From ad59c70a985fb9818866ee6302645d0eae18c2c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:31:07 +0200 Subject: [PATCH 04/42] #680 - removed implicit assignment for SP --- .../standalonepublisher/plugins/publish/collect_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 43ab13cd79..d4855da140 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = ["ftrack"] + default_families = [] def process(self, context): # get json paths from os and load them From 5ea17137495657c7f32c73359c890c252841ab92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 12:00:00 +0200 Subject: [PATCH 05/42] #680 - changed logging Added explicit removing of ftrack family if configured to remove --- .../plugins/publish/collect_ftrack_family.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e910970290..a66e6db29f 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -44,12 +44,19 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): } profile = filter_profiles(self.profiles, filtering_criteria) - if profile and profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + if profile: families = instance.data.get("families") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") - self.log.debug("instance.data:: {}".format(instance.data)) + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + if families and "ftrack" in families: + self.log.debug("Explicitly removing 'ftrack'") + instance.data["families"].remove("ftrack") + + self.log.debug("Resulting families '{}' for '{}'".format( + instance.data["families"], instance.data["family"])) From c105eedbc7503f0ef8475ef26b0c68c53a023241 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 15:23:49 +0200 Subject: [PATCH 06/42] #680 - fixed logging, task selection --- .../plugins/publish/collect_ftrack_family.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66e6db29f..13ae40fd44 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -6,7 +6,9 @@ Provides: instance -> families ([]) """ import os + import pyblish.api +import avalon.api from openpype.lib.plugin_tools import filter_profiles @@ -31,10 +33,11 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): return anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) + task_name = instance.data.get("task", + instance.context.data.get("task", + avalon.api.Session["AVALON_TASK"])) host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) + avalon.api.Session["AVALON_APP"]) family = instance.data["family"] filtering_criteria = { @@ -47,7 +50,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + self.log.debug("Adding ftrack family for '{}'". + format(instance.data.get("family"))) if families and "ftrack" not in families: instance.data["families"].append("ftrack") @@ -57,6 +61,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if families and "ftrack" in families: self.log.debug("Explicitly removing 'ftrack'") instance.data["families"].remove("ftrack") - - self.log.debug("Resulting families '{}' for '{}'".format( - instance.data["families"], instance.data["family"])) + else: + self.log.debug("Instance '{}' doesn't match any profile".format( + instance.data.get("family"))) From 2b202dd7599fcc1aafe337923040a345043f274f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:25:02 +0200 Subject: [PATCH 07/42] #680 - added remove "matchmove" to settings Removed unneeded plugin --- .../plugins/publish/collect_matchmove.py | 29 ------------------- .../defaults/project_settings/ftrack.json | 8 +++++ 2 files changed, 8 insertions(+), 29 deletions(-) delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py deleted file mode 100644 index 5d9e8ddfb4..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Requires: - Nothing - -Provides: - Instance -""" - -import pyblish.api -import logging - - -log = logging.getLogger("collector") - - -class CollectMatchmovePublish(pyblish.api.InstancePlugin): - """ - Collector with only one reason for its existence - remove 'ftrack' - family implicitly added by Standalone Publisher - """ - - label = "Collect Matchmove - SA Publish" - order = pyblish.api.CollectorOrder - families = ["matchmove"] - hosts = ["standalonepublisher"] - - def process(self, instance): - if "ftrack" in instance.data["families"]: - instance.data["families"].remove("ftrack") diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index a8e121d7c3..f83b6cef3a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -211,6 +211,14 @@ ], "add_ftrack_family": true }, + { + "families": ["matchmove"], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": false + }, { "families": [ "model", From 4f0e30b46debfa44f2f4b8816248a0f9bca15f82 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:26:09 +0200 Subject: [PATCH 08/42] #680 - removed default_families as now there are profiles in Setting doing that --- .../standalonepublisher/plugins/publish/collect_context.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index d4855da140..6913e0836d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = [] def process(self, context): # get json paths from os and load them @@ -213,10 +212,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): subset = in_data["subset"] # If instance data already contain families then use it instance_families = in_data.get("families") or [] - # Make sure default families are in instance - for default_family in self.default_families or []: - if default_family not in instance_families: - instance_families.append(default_family) instance = context.create_instance(subset) instance.data.update( From 29271dc0c777234a7ad3952fef50ed42a6d3ea2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Jun 2021 12:19:11 +0200 Subject: [PATCH 09/42] #680 - added additional filtering on 'families' instead of main 'family' --- .../plugins/publish/collect_ftrack_family.py | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 13ae40fd44..8dbcb0ab2c 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -5,8 +5,6 @@ Requires: Provides: instance -> families ([]) """ -import os - import pyblish.api import avalon.api @@ -20,10 +18,14 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): Uses selection by combination of hosts/families/tasks names via profiles resolution. - Triggered everywhere, checks instance against configured + Triggered everywhere, checks instance against configured. + + Checks advanced filtering which works on 'families' not on main + 'family', as some variants dynamically resolves addition of ftrack + based on 'families' (editorial drives it by presence of 'review') """ label = "Collect Ftrack Family" - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.4998 profiles = None @@ -34,8 +36,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", - instance.context.data.get("task", - avalon.api.Session["AVALON_TASK"])) + avalon.api.Session["AVALON_TASK"]) host_name = anatomy_data.get("app", avalon.api.Session["AVALON_APP"]) family = instance.data["family"] @@ -49,7 +50,17 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") - if profile["add_ftrack_family"]: + add_ftrack_family = profile["add_ftrack_family"] + + additional_filters = profile.get("additional_filters") + if additional_filters: + add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( + additional_filters, + families, + add_ftrack_family + ) + + if add_ftrack_family: self.log.debug("Adding ftrack family for '{}'". format(instance.data.get("family"))) @@ -57,10 +68,41 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] - else: - if families and "ftrack" in families: - self.log.debug("Explicitly removing 'ftrack'") - instance.data["families"].remove("ftrack") else: self.log.debug("Instance '{}' doesn't match any profile".format( instance.data.get("family"))) + + def _get_add_ftrack_f_from_addit_filters(self, + additional_filters, + families, + add_ftrack_family): + """ + Compares additional filters - working on instance's families. + + Triggered for more detailed filtering when main family matches, + but content of 'families' actually matter. + (For example 'review' in 'families' should result in adding to + Ftrack) + + Args: + additional_filters (dict) - from Setting + families (list) - subfamilies + add_ftrack_family (bool) - add ftrack to families if True + """ + override_filter = None + override_filter_value = -1 + for additional_filter in additional_filters: + filter_families = set(additional_filter["families"]) + valid = filter_families <= families # issubset + if not valid: + continue + + value = len(filter_families) + if value > override_filter_value: + override_filter = additional_filter + override_filter_value = value + + if override_filter: + add_ftrack_family = override_filter["add_ftrack_family"] + + return add_ftrack_family From 68e9102f7263e70c50c7f7a959ae48ba5c16280e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 17:04:45 +0200 Subject: [PATCH 10/42] #636 - PS created loader to load reference frame from 'render' sequence Loader cannot do containerization for now as updates would be too difficult. (Customer doesn't mind.) --- .../plugins/load/load_image_from_sequence.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py new file mode 100644 index 0000000000..1409065d43 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -0,0 +1,90 @@ +import os +from avalon import photoshop +from avalon.vendor import qargparse + +from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader + +stub = photoshop.stub() + + +class ImageFromSequenceLoader(ImageLoader): + """ Load specifing image from sequence + + Used only as quick load of reference file from a sequence. + + Plain ImageLoader picks first frame from sequence. + + Loads only existing files - currently not possible to limit loaders + to single select - multiselect. If user selects multiple repres, list + for all of them is provided, but selection is only single file. + This loader will be triggered multiple times, but selected name will + match only to proper path. + + Loader doesnt do containerization as there is currently no data model + of 'frame of rendered files' (only rendered sequence), update would be + difficult. + """ + + families = ["render"] + representations = ["*"] + options = [] + + def load(self, context, name=None, namespace=None, data=None): + if data.get("frame"): + self.fname = os.path.join(os.path.dirname(self.fname), + data["frame"]) + if not os.path.exists(self.fname): + return + + layer_name = self._get_unique_layer_name(context["asset"]["name"], + name) + with photoshop.maintained_selection(): + layer = stub.import_smart_object(self.fname, layer_name) + + self[:] = [layer] + namespace = namespace or layer_name + + return namespace + + @classmethod + def get_options(cls, repre_contexts): + """ + Returns list of files for selected 'repre_contexts'. + + It returns only files with same extension as in context as it is + expected that context points to sequence of frames. + + Returns: + (list) of qargparse.Choice + """ + files = [] + for context in repre_contexts: + fname = ImageLoader.filepath_from_context(context) + _, file_extension = os.path.splitext(fname) + + for file_name in os.listdir(os.path.dirname(fname)): + if not file_name.endswith(file_extension): + continue + files.append(file_name) + + # return selection only if there is something + if not files or len(files) <= 1: + return [] + + return [ + qargparse.Choice( + "frame", + label="Select specific file", + items=files, + default=0, + help="Which frame should be loaded?" + ) + ] + + def update(self, container, representation): + """No update possible, not containerized.""" + pass + + def remove(self, container): + """No update possible, not containerized.""" + pass From 852a2f35aa9352e3d9b3efe8b95d96c5697b7aa1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 18:30:15 +0200 Subject: [PATCH 11/42] #636 - un subclassed loader from sequence, created lib for reusable methods Loader from sequence didnt was dropping connection to PS after first successful import --- openpype/hosts/photoshop/plugins/lib.py | 26 +++++++++++++++ .../photoshop/plugins/load/load_image.py | 33 ++++--------------- .../plugins/load/load_image_from_sequence.py | 15 ++++++--- 3 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/lib.py diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py new file mode 100644 index 0000000000..74aff06114 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/lib.py @@ -0,0 +1,26 @@ +import re + + +def get_unique_layer_name(layers, asset_name, subset_name): + """ + Gets all layer names and if 'asset_name_subset_name' is present, it + increases suffix by 1 (eg. creates unique layer name - for Loader) + Args: + layers (list) of dict with layers info (name, id etc.) + asset_name (string): + subset_name (string): + + Returns: + (string): name_00X (without version) + """ + name = "{}_{}".format(asset_name, subset_name) + names = {} + for layer in layers: + layer_name = re.sub(r'_\d{3}$', '', layer.name) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 44cc96c96f..d043323768 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -1,7 +1,9 @@ -from avalon import api, photoshop -import os import re +from avalon import api, photoshop + +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name + stub = photoshop.stub() @@ -15,8 +17,9 @@ class ImageLoader(api.Loader): representations = ["*"] def load(self, context, name=None, namespace=None, data=None): - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -69,25 +72,3 @@ class ImageLoader(api.Loader): def switch(self, container, representation): self.update(container, representation) - - def _get_unique_layer_name(self, asset_name, subset_name): - """ - Gets all layer names and if 'name' is present in them, increases - suffix by 1 (eg. creates unique layer name - for Loader) - Args: - name (string): in format asset_subset - - Returns: - (string): name_00X (without version) - """ - name = "{}_{}".format(asset_name, subset_name) - names = {} - for layer in stub.get_layers(): - layer_name = re.sub(r'_\d{3}$', '', layer.name) - if layer_name in names.keys(): - names[layer_name] = names[layer_name] + 1 - else: - names[layer_name] = 1 - occurrences = names.get(name, 0) - - return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 1409065d43..09525d2791 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,13 +1,15 @@ import os + +from avalon import api from avalon import photoshop from avalon.vendor import qargparse -from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() -class ImageFromSequenceLoader(ImageLoader): +class ImageFromSequenceLoader(api.Loader): """ Load specifing image from sequence Used only as quick load of reference file from a sequence. @@ -36,8 +38,11 @@ class ImageFromSequenceLoader(ImageLoader): if not os.path.exists(self.fname): return - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + stub = photoshop.stub() + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) + with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -59,7 +64,7 @@ class ImageFromSequenceLoader(ImageLoader): """ files = [] for context in repre_contexts: - fname = ImageLoader.filepath_from_context(context) + fname = ImageFromSequenceLoader.filepath_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): From 8813c7cc062742ee27a75726d08bc07b2c2413ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 13:11:39 +0200 Subject: [PATCH 12/42] #680 - added advanced_filtering to schema --- .../schema_project_ftrack.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index af85ccb254..a94ebc8888 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -651,6 +651,29 @@ "key": "add_ftrack_family", "label": "Add Ftrack Family", "type": "boolean" + }, + { + "type": "list", + "collapsible": true, + "key": "advanced_filtering", + "label": "Advanced adding if additional families present", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Additional Families", + "type": "list", + "object_type": "text" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } } ] } From 186bd21edc47af44745b6fedecbc143dba39dce6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:16:44 +0200 Subject: [PATCH 13/42] #680 - added documentation --- .../assets/ftrack/ftrack-collect-advanced.png | Bin 0 -> 16622 bytes .../assets/ftrack/ftrack-collect-main.png | Bin 0 -> 40169 bytes website/docs/module_ftrack.md | 28 +++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/ftrack/ftrack-collect-advanced.png create mode 100644 website/docs/assets/ftrack/ftrack-collect-main.png diff --git a/website/docs/assets/ftrack/ftrack-collect-advanced.png b/website/docs/assets/ftrack/ftrack-collect-advanced.png new file mode 100644 index 0000000000000000000000000000000000000000..5249685c86d21461589b6556a5453becf0d742ae GIT binary patch literal 16622 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 C%`C70 literal 0 HcmV?d00001 diff --git a/website/docs/assets/ftrack/ftrack-collect-main.png b/website/docs/assets/ftrack/ftrack-collect-main.png new file mode 100644 index 0000000000000000000000000000000000000000..7c75cd6269e8862845f392dea0b98852b4dd4bff GIT binary patch literal 40169 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 Ch7%9~ literal 0 HcmV?d00001 diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index bd0dbaef4f..c3b9fd6bc2 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -196,7 +196,7 @@ Is used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is c ### Sync status from Task to Parent -List of parent boject types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) +List of parent object types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) ### Sync status from Version to Task @@ -214,3 +214,29 @@ This is usefull for example if first version publish doesn't contain any actual ### Update status on next task Change status on next task by task types order when task status state changed to "Done". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored. + +## Publish plugins + +### Collect Ftrack Family + +Reviews uploads to Ftrack could be configured by combination of hosts, families and task names. +(Currently implemented only in Standalone Publisher, Maya.) + +#### Profiles + +Profiles are used to select when to add Ftrack family to the instance. One or multiple profiles could be configured, Families, Task names (regex available), Host names combination is needed. + +Eg. If I want review created and uploaded to Ftrack for render published from Maya , setting is: + +Host names: 'Maya' +Families: 'render' +Add Ftrack Family: enabled + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-main.png) + +#### Advanced adding if additional families present + +In special cases adding 'ftrack' based on main family ('Families' set higher) is not enough. +(For example upload to Ftrack for 'plate' main family should only happen if 'review' is contained in instance 'families', not added in other cases. ) + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png) \ No newline at end of file From ba5d6a17d23439d98094792075f65e7c29345897 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:17:06 +0200 Subject: [PATCH 14/42] Fixed wrong label --- website/docs/admin_hosts_aftereffects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_aftereffects.md b/website/docs/admin_hosts_aftereffects.md index dc43820465..e3a09058da 100644 --- a/website/docs/admin_hosts_aftereffects.md +++ b/website/docs/admin_hosts_aftereffects.md @@ -14,7 +14,7 @@ All of them are Project based, eg. each project could have different configurati Location: Settings > Project > AfterEffects -![Harmony Project Settings](assets/admin_hosts_aftereffects_settings.png) +![AfterEffects Project Settings](assets/admin_hosts_aftereffects_settings.png) ## Publish plugins From 582f228d0a7fd1d0748bf7a7862b55611e069b32 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 10:54:22 +0200 Subject: [PATCH 15/42] #680 - small fixes --- .../ftrack/plugins/publish/collect_ftrack_family.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 8dbcb0ab2c..b505a429b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -34,11 +34,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): self.log.warning("No profiles present for adding Ftrack family") return - anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", avalon.api.Session["AVALON_TASK"]) - host_name = anatomy_data.get("app", - avalon.api.Session["AVALON_APP"]) + host_name = avalon.api.Session["AVALON_APP"] family = instance.data["family"] filtering_criteria = { @@ -46,7 +44,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): "families": family, "tasks": task_name } - profile = filter_profiles(self.profiles, filtering_criteria) + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) if profile: families = instance.data.get("families") @@ -93,7 +92,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): override_filter_value = -1 for additional_filter in additional_filters: filter_families = set(additional_filter["families"]) - valid = filter_families <= families # issubset + valid = filter_families <= set(families) # issubset if not valid: continue From 10f5c76973ba97929ea01a10c12cb01ce3324930 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 11:07:07 +0200 Subject: [PATCH 16/42] #680 - allow configuration for editorial --- .../plugins/publish/collect_instances.py | 4 +-- .../defaults/project_settings/ftrack.json | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py index eb04217136..d753a3d9bb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py @@ -16,12 +16,12 @@ class CollectInstances(pyblish.api.InstancePlugin): subsets = { "referenceMain": { "family": "review", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".mp4"] }, "audioMain": { "family": "audio", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".wav"], }, "shotMain": { diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f83b6cef3a..f43900c76f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,22 +204,30 @@ "enabled": true, "profiles": [ { + "hosts": [ + "standalonepublisher" + ], "families": [], "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] + }, + { "hosts": [ "standalonepublisher" ], - "add_ftrack_family": true - }, - { - "families": ["matchmove"], + "families": [ + "matchmove", + "shot" + ], "tasks": [], - "hosts": [ - "standalonepublisher" - ], - "add_ftrack_family": false + "add_ftrack_family": false, + "advanced_filtering": [] }, { + "hosts": [ + "maya" + ], "families": [ "model", "setdress", @@ -229,10 +237,8 @@ "camera" ], "tasks": [], - "hosts": [ - "maya" - ], - "add_ftrack_family": true + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From aea7d538095a794e44ce50d6e9074b7d6715e2f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 17:36:51 +0200 Subject: [PATCH 17/42] #636 - use new api function --- .../hosts/photoshop/plugins/load/load_image_from_sequence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 09525d2791..8704627b12 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -2,8 +2,10 @@ import os from avalon import api from avalon import photoshop +from avalon.pipeline import get_representation_path_from_context from avalon.vendor import qargparse +from openpype.lib import Anatomy from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() @@ -64,7 +66,7 @@ class ImageFromSequenceLoader(api.Loader): """ files = [] for context in repre_contexts: - fname = ImageFromSequenceLoader.filepath_from_context(context) + fname = get_representation_path_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): @@ -93,3 +95,4 @@ class ImageFromSequenceLoader(api.Loader): def remove(self, container): """No update possible, not containerized.""" pass + From bf284caccf998352222722af616287e4fd411c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 20:03:31 +0200 Subject: [PATCH 18/42] make sure bg compositing is happening only if alpha is available --- .../plugins/publish/extract_sequence.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 007b5c41f1..8588a5b07c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -293,10 +293,15 @@ class ExtractSequence(pyblish.api.Extractor): thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") if first_frame_filepath and os.path.exists(first_frame_filepath): + # Composite background only on rgba images + # - just making sure source_img = Image.open(first_frame_filepath) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -392,9 +397,14 @@ class ExtractSequence(pyblish.api.Extractor): if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): source_img = Image.open(thumbnail_src_filepath) thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + # Composite background only on rgba images + # - just making sure + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From 4b707d4cb2b4781bb14325b9051a24496d29a5cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 12:14:26 +0200 Subject: [PATCH 19/42] Settings: ftrack family for editorial standalonepublishing --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f43900c76f..03ecf024a6 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -224,6 +224,26 @@ "add_ftrack_family": false, "advanced_filtering": [] }, + { + "hosts": [ + "standalonepublisher" + ], + "families": [ + "review", + "plate" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [ + { + "families": [ + "clip", + "review" + ], + "add_ftrack_family": true + } + ] + }, { "hosts": [ "maya" From c807bef0cb1c028c15d67d08c2576faf406d8171 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:07 +0200 Subject: [PATCH 20/42] added `hosts-enum` entity for settings --- openpype/settings/entities/__init__.py | 2 + openpype/settings/entities/enum_entity.py | 52 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index f64ca1e98d..94eb819f2b 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -101,6 +101,7 @@ from .color_entity import ColorEntity from .enum_entity import ( BaseEnumEntity, EnumEntity, + HostsEnumEntity, AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, @@ -153,6 +154,7 @@ __all__ = ( "BaseEnumEntity", "EnumEntity", + "HostsEnumEntity", "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 0b0575a255..add5c0298c 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -101,6 +101,58 @@ class EnumEntity(BaseEnumEntity): super(EnumEntity, self).schema_validations() +class HostsEnumEntity(BaseEnumEntity): + schema_types = ["hosts-enum"] + + def _item_initalization(self): + self.multiselection = self.schema_data.get("multiselection", True) + self.use_empty_value = self.schema_data.get( + "use_empty_value", not self.multiselection + ) + self.empty_label = ( + self.schema_data.get("empty_label") or "< without host >" + ) + + self.enum_items = [ + {"aftereffects": "aftereffects"}, + {"blender": "blender"}, + {"celaction": "celaction"}, + {"fusion": "fusion"}, + {"harmony": "harmony"}, + {"hiero": "hiero"}, + {"houdini": "houdini"}, + {"maya": "maya"}, + {"nuke": "nuke"}, + {"photoshop": "photoshop"}, + {"resolve": "resolve"}, + {"tvpaint": "tvpaint"}, + {"unreal": "unreal"} + ] + + if self.use_empty_value: + self.enum_items.insert(0, {"": self.empty_label}) + + valid_keys = set() + for item in self.enum_items or []: + valid_keys.add(tuple(item.keys())[0]) + + self.valid_keys = valid_keys + + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + for key in valid_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + break + + self.valid_value_types = (STRING_TYPE, ) + + # GUI attribute + self.placeholder = self.schema_data.get("placeholder") + + class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] From c1bb5fec8f6ecf3689408653210fc081039d93d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:56 +0200 Subject: [PATCH 21/42] use host-enum instead of list of strings for hosts --- .../projects_schema/schema_project_slack.json | 4 ++-- .../schemas/schema_global_publish.json | 12 ++++++------ .../projects_schema/schemas/schema_global_tools.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json index 58708776ca..170de7c8a2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json @@ -59,10 +59,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Host names", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "separator" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8ca203e3bc..496635287f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -90,10 +90,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -358,10 +358,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -492,10 +492,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 224389d42e..8c92a45a56 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -35,10 +35,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", @@ -75,10 +75,10 @@ "type": "dict", "children": [ { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", From 9d84a39e11653d6f88f774d94e0963816ea51943 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:44:18 +0200 Subject: [PATCH 22/42] replaced hosts enum in applications with hosts-enum --- .../template_host_unchangables.json | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json index e8b2a70076..c4d8d89209 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json @@ -14,25 +14,11 @@ "roles": ["developer"] }, { - "type": "enum", + "type": "hosts-enum", "key": "host_name", "label": "Host implementation", - "enum_items": [ - { "": "< without host >" }, - { "aftereffects": "aftereffects" }, - { "blender": "blender" }, - { "celaction": "celaction" }, - { "fusion": "fusion" }, - { "harmony": "harmony" }, - { "hiero": "hiero" }, - { "houdini": "houdini" }, - { "maya": "maya" }, - { "nuke": "nuke" }, - { "photoshop": "photoshop" }, - { "resolve": "resolve" }, - { "tvpaint": "tvpaint" }, - { "unreal": "unreal" } - ], + "multiselection": false, + "use_empty_value": true, "roles": ["developer"] } ] From fbcb46ac0922cc03c69262d709c765ba319d5803 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:51:44 +0200 Subject: [PATCH 23/42] adde hosts-enum to readme --- openpype/settings/entities/schemas/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 6c31b61f59..0ad13bfe1a 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -272,6 +272,22 @@ } ``` +### hosts-enum +- enumeration of available hosts +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) +- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +``` +{ + "key": "host", + "label": "Host name", + "type": "hosts-enum", + "multiselection": false, + "use_empty_value": true, + "empty_label": "N/A" +} +``` + ## Inputs for setting value using Pure inputs - these inputs also have required `"key"` - attribute `"label"` is required in few conditions From d7ab6bb7ae8e2c33adf247978f884552c808e10d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:57:19 +0200 Subject: [PATCH 24/42] added few comments --- openpype/settings/entities/enum_entity.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index add5c0298c..050f0038f7 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -102,6 +102,21 @@ class EnumEntity(BaseEnumEntity): class HostsEnumEntity(BaseEnumEntity): + """Enumeration of host names. + + Enum items are hardcoded in definition of the entity. + + Hosts enum can have defined empty value as valid option which is + represented by empty string. Schema key to set this option is + `use_empty_value` (true/false). And to set label of empty value set + `empty_label` (string). + + Enum can have single and multiselection. + + NOTE: + Host name is not the same as application name. Host name defines + implementation instead of application name. + """ schema_types = ["hosts-enum"] def _item_initalization(self): @@ -113,6 +128,7 @@ class HostsEnumEntity(BaseEnumEntity): self.schema_data.get("empty_label") or "< without host >" ) + # These are hardcoded there is not list of available host in OpenPype self.enum_items = [ {"aftereffects": "aftereffects"}, {"blender": "blender"}, From ca36bb2fb7bd4e554ab8014bf20ba6c64573d09f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:19 +0200 Subject: [PATCH 25/42] Added settings for ExtractSequence to be able modify color of thumbnail background --- .../defaults/project_settings/tvpaint.json | 8 ++++++++ .../projects_schema/schema_project_tvpaint.json | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index b4f3b315ec..25b7056ce1 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,5 +1,13 @@ { "publish": { + "ExtractSequence": { + "thumbnail_bg": [ + 255, + 255, + 255, + 255 + ] + }, "ValidateProjectSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 6f90bb4263..1894384bb9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -11,6 +11,21 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ExtractSequence", + "label": "ExtractSequence", + "is_group": true, + "children": [ + { + "type": "color", + "key": "thumbnail_bg", + "label": "Thumbnail BG color", + "use_alpha": false + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 4628b7627a492642f7cb086b46be683260bbd51d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:43 +0200 Subject: [PATCH 26/42] define default value in ExtractSequence fot thumbnail background color --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8588a5b07c..e0ea207db3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -13,6 +13,9 @@ class ExtractSequence(pyblish.api.Extractor): hosts = ["tvpaint"] families = ["review", "renderPass", "renderLayer"] + # Modifiable with settings + thumbnail_bg = [255, 255, 255, 255] + def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) From 4d484189ed71114602bf9ebd46e1f434bde33e77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:13:51 +0200 Subject: [PATCH 27/42] added function to get thumbnail bg color --- .../plugins/publish/extract_sequence.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index e0ea207db3..c13066d2c3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -300,10 +300,18 @@ class ExtractSequence(pyblish.api.Extractor): # - just making sure source_img = Image.open(first_frame_filepath) if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -403,14 +411,32 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath + def _get_thumbnail_bg_color(self): + red = green = blue = 255 + if self.thumbnail_bg: + if len(self.thumbnail_bg) == 4: + red, green, blue, _ = self.thumbnail_bg + elif len(self.thumbnail_bg) == 3: + red, green, blue = self.thumbnail_bg + return (red, green, blue) + def _render_layer( self, layer, From a7962aa3a05769a14a8091a7445e63945656b900 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:09:48 +0200 Subject: [PATCH 28/42] modified how labels in hosts enum works --- openpype/settings/entities/enum_entity.py | 52 +++++++++++--------- openpype/settings/entities/schemas/README.md | 7 ++- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 050f0038f7..d197683afe 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -124,34 +124,38 @@ class HostsEnumEntity(BaseEnumEntity): self.use_empty_value = self.schema_data.get( "use_empty_value", not self.multiselection ) - self.empty_label = ( - self.schema_data.get("empty_label") or "< without host >" - ) + custom_labels = self.schema_data.get("custom_labels") or {} + + host_names = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal" + ] + if self.use_empty_value: + host_names.insert(0, "") + # Add default label for empty value if not available + if "" not in custom_labels: + custom_labels[""] = "< without host >" # These are hardcoded there is not list of available host in OpenPype - self.enum_items = [ - {"aftereffects": "aftereffects"}, - {"blender": "blender"}, - {"celaction": "celaction"}, - {"fusion": "fusion"}, - {"harmony": "harmony"}, - {"hiero": "hiero"}, - {"houdini": "houdini"}, - {"maya": "maya"}, - {"nuke": "nuke"}, - {"photoshop": "photoshop"}, - {"resolve": "resolve"}, - {"tvpaint": "tvpaint"}, - {"unreal": "unreal"} - ] - - if self.use_empty_value: - self.enum_items.insert(0, {"": self.empty_label}) - + enum_items = [] valid_keys = set() - for item in self.enum_items or []: - valid_keys.add(tuple(item.keys())[0]) + for key in host_names: + label = custom_labels.get(key, key) + valid_keys.add(key) + enum_items.append({key, label}) + self.enum_items = enum_items self.valid_keys = valid_keys if self.multiselection: diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 0ad13bfe1a..bbd53fa46b 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -276,7 +276,7 @@ - enumeration of available hosts - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) -- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +- it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) ``` { "key": "host", @@ -284,7 +284,10 @@ "type": "hosts-enum", "multiselection": false, "use_empty_value": true, - "empty_label": "N/A" + "custom_labels": { + "": "N/A", + "nuke": "Nuke" + } } ``` From 9cbb10251947577e3e7b2c9b542b088c7bf2bc3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:11:57 +0200 Subject: [PATCH 29/42] fix dict/set type --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index d197683afe..63e0afeb47 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -153,7 +153,7 @@ class HostsEnumEntity(BaseEnumEntity): for key in host_names: label = custom_labels.get(key, key) valid_keys.add(key) - enum_items.append({key, label}) + enum_items.append({key: label}) self.enum_items = enum_items self.valid_keys = valid_keys From b5d7da72404aa7edde91d1527d9f4978ba3bab73 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:19:01 +0200 Subject: [PATCH 30/42] nuke: create render crop format to nuke.root.format if selected to none --- .../hosts/nuke/plugins/create/create_write_render.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 9ddf0e4a87..a1381122ee 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -100,6 +100,13 @@ class CreateWriteRender(plugin.PypeCreator): "/{subset}.{frame}.{ext}")}) # add crop node to cut off all outside of format bounding box + # get width and height + try: + width, height = (selected_node.width(), selected_node.height()) + except AttributeError: + actual_format = nuke.root().knob('format').value() + width, height = (actual_format.width(), actual_format.height()) + _prenodes = [ { "name": "Crop01", @@ -108,8 +115,8 @@ class CreateWriteRender(plugin.PypeCreator): ("box", [ 0.0, 0.0, - selected_node.width(), - selected_node.height() + width, + height ]) ], "dependent": None From 7ca8ce40a2aecafa272406d2fbd1908dd12ab8a1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:20:07 +0200 Subject: [PATCH 31/42] pep8 fix --- openpype/hosts/nuke/plugins/create/create_write_prerender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index bb01236801..1b925014ad 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -128,5 +128,4 @@ class CreateWritePrerender(plugin.PypeCreator): w_node["first"].setValue(nuke.root()["first_frame"].value()) w_node["last"].setValue(nuke.root()["last_frame"].value()) - return write_node From e430797b39cd4acd061af10210bc89785922477c Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 13:36:12 +0000 Subject: [PATCH 32/42] Create draft PR for #1688 From 4f66746d9e1e44ed69d95a9f49dfb995084c5102 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:46:40 +0200 Subject: [PATCH 33/42] collect scene bg color --- .../plugins/publish/collect_workfile_data.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 13c6c9eb78..d8bb03f541 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -1,5 +1,6 @@ import os import json +import tempfile import pyblish.api import avalon.api @@ -153,9 +154,45 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkIn": int(mark_in_frame), "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), - "sceneMarkOutState": mark_out_state == "set" + "sceneMarkOutState": mark_out_state == "set", + "sceneBgColor": self._get_bg_color() } self.log.debug( "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def _get_bg_color(self): + """Background color set on scene. + + Is important for review exporting where scene bg color is used as + background. + """ + output_file = tempfile.NamedTemporaryFile( + mode="w", prefix="a_tvp_", suffix=".txt", delete=False + ) + output_file.close() + output_filepath = output_file.name.replace("\\", "/") + george_script_lines = [ + # Variable containing full path to output file + "output_path = \"{}\"".format(output_filepath), + "tv_background", + "bg_color = result", + # Write data to output file + ( + "tv_writetextfile" + " \"strict\" \"append\" '\"'output_path'\"' bg_color" + ) + ] + + george_script = "\n".join(george_script_lines) + lib.execute_george_through_file(george_script) + + with open(output_filepath, "r") as stream: + data = stream.read() + + os.remove(output_filepath) + data = data.strip() + if not data: + return None + return data.split(" ") From ddd3668277ace0136e95e3b489e46e935bfeb2ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:51:56 +0200 Subject: [PATCH 34/42] renamed thumbnail_bg_color to review_bg_color --- .../plugins/publish/extract_sequence.py | 32 ++++++------------- .../defaults/project_settings/tvpaint.json | 2 +- .../schema_project_tvpaint.json | 8 +++-- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c13066d2c3..6e5ec39f46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -14,7 +14,7 @@ class ExtractSequence(pyblish.api.Extractor): families = ["review", "renderPass", "renderLayer"] # Modifiable with settings - thumbnail_bg = [255, 255, 255, 255] + review_bg = [255, 255, 255, 255] def process(self, instance): self.log.info( @@ -299,20 +299,6 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure source_img = Image.open(first_frame_filepath) - if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() - self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) - )) - bg_image = Image.new("RGBA", source_img.size, bg_color) - thumbnail_obj = Image.alpha_composite(bg_image, source_img) - thumbnail_obj.convert("RGB").save(thumbnail_filepath) - else: - self.log.info(( - "Source for thumbnail has mode \"{}\" (Expected: RGBA)." - " Can't use thubmanail background color." - ).format(source_img.mode)) - source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -411,9 +397,9 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() + bg_color = self._get_review_bg_color() self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) + " ".join([str(val) for val in bg_color]) )) bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) @@ -428,13 +414,13 @@ class ExtractSequence(pyblish.api.Extractor): return output_filenames, thumbnail_filepath - def _get_thumbnail_bg_color(self): + def _get_review_bg_color(self): red = green = blue = 255 - if self.thumbnail_bg: - if len(self.thumbnail_bg) == 4: - red, green, blue, _ = self.thumbnail_bg - elif len(self.thumbnail_bg) == 3: - red, green, blue = self.thumbnail_bg + if self.review_bg: + if len(self.review_bg) == 4: + red, green, blue, _ = self.review_bg + elif len(self.review_bg) == 3: + red, green, blue = self.review_bg return (red, green, blue) def _render_layer( diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 25b7056ce1..763802a73f 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,7 +1,7 @@ { "publish": { "ExtractSequence": { - "thumbnail_bg": [ + "review_bg": [ 255, 255, 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 1894384bb9..67aa4b0a06 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -18,10 +18,14 @@ "label": "ExtractSequence", "is_group": true, "children": [ + { + "type": "label", + "label": "Review BG color is used for whole scene review and for thumbnails." + }, { "type": "color", - "key": "thumbnail_bg", - "label": "Thumbnail BG color", + "key": "review_bg", + "label": "Review BG color", "use_alpha": false } ] From d5b0976b2d07ef75f2c4c3f58897094b6918f904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:26 +0200 Subject: [PATCH 35/42] pass scene bg color to review extaction --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 6e5ec39f46..dc6e7eb64f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -56,6 +56,8 @@ class ExtractSequence(pyblish.api.Extractor): handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] + scene_bg_color = instance.context.data["sceneBgColor"] + # --- Fallbacks ---------------------------------------------------- # This is required if validations of ranges are ignored. # - all of this code won't change processing if range to render @@ -123,7 +125,8 @@ class ExtractSequence(pyblish.api.Extractor): if instance.data["family"] == "review": output_filenames, thumbnail_fullpath = self.render_review( - filename_template, output_dir, mark_in, mark_out + filename_template, output_dir, mark_in, mark_out, + scene_bg_color ) else: # Render output @@ -244,7 +247,9 @@ class ExtractSequence(pyblish.api.Extractor): for path in repre_filepaths ] - def render_review(self, filename_template, output_dir, mark_in, mark_out): + def render_review( + self, filename_template, output_dir, mark_in, mark_out, scene_bg_color + ): """ Export images from TVPaint using `tv_savesequence` command. Args: @@ -255,6 +260,8 @@ class ExtractSequence(pyblish.api.Extractor): output_dir (str): Directory where files will be stored. mark_in (int): Starting frame index from which export will begin. mark_out (int): On which frame index export will end. + scene_bg_color (list): Bg color set in scene. Result of george + script command `tv_background`. Retruns: tuple: With 2 items first is list of filenames second is path to From 70bd4460a054c8843145874aed9e5fa9168399b6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:46 +0200 Subject: [PATCH 36/42] change bg color to color from settings and back --- .../tvpaint/plugins/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index dc6e7eb64f..c09cf08d82 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -1,5 +1,6 @@ import os import shutil +import copy import tempfile import pyblish.api @@ -273,7 +274,11 @@ class ExtractSequence(pyblish.api.Extractor): filename_template.format(frame=mark_in) ) + bg_color = self._get_review_bg_color() + george_script_lines = [ + # Change bg color to color from settings + "tv_background \"color\" {} {} {}".format(*bg_color), "tv_SaveMode \"PNG\"", "export_path = \"{}\"".format( first_frame_filepath.replace("\\", "/") @@ -282,6 +287,18 @@ class ExtractSequence(pyblish.api.Extractor): mark_in, mark_out ) ] + if scene_bg_color: + # Change bg color back to previous scene bg color + _scene_bg_color = copy.deepcopy(scene_bg_color) + bg_type = _scene_bg_color.pop(0) + orig_color_command = [ + "tv_background", + "\"{}\"".format(bg_type) + ] + orig_color_command.extend(_scene_bg_color) + + george_script_lines.append(" ".join(orig_color_command)) + lib.execute_george_through_file("\n".join(george_script_lines)) first_frame_filepath = None From e708f95f0dd61f069f3add62ebb6e7fec64ab77a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:53:16 +0200 Subject: [PATCH 37/42] thumbnail creation from review is simplified as bg color is already applied on output --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c09cf08d82..536df2adb0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -318,11 +318,13 @@ class ExtractSequence(pyblish.api.Extractor): if first_frame_filepath is None: first_frame_filepath = filepath - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + thumbnail_filepath = None if first_frame_filepath and os.path.exists(first_frame_filepath): - # Composite background only on rgba images - # - just making sure + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") source_img = Image.open(first_frame_filepath) + if source_img.mode.lower() != "rgb": + source_img = source_img.convert("RGB") + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From 9105b9eb6d314471f4a0ba91994494e964de55f3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:16:13 +0200 Subject: [PATCH 38/42] hiero: fixing creator (deepcopy) --- .../hiero/plugins/create/create_shot_clip.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/hiero/plugins/create/create_shot_clip.py b/openpype/hosts/hiero/plugins/create/create_shot_clip.py index 25be9f090b..0c5bf93a3f 100644 --- a/openpype/hosts/hiero/plugins/create/create_shot_clip.py +++ b/openpype/hosts/hiero/plugins/create/create_shot_clip.py @@ -1,3 +1,4 @@ +from copy import deepcopy import openpype.hosts.hiero.api as phiero # from openpype.hosts.hiero.api import plugin, lib # reload(lib) @@ -206,20 +207,24 @@ class CreateShotClip(phiero.Creator): presets = None def process(self): + # Creator copy of object attributes that are modified during `process` + presets = deepcopy(self.presets) + gui_inputs = deepcopy(self.gui_inputs) + # get key pares from presets and match it on ui inputs - for k, v in self.gui_inputs.items(): + for k, v in gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed # for sections and dict) for _k, _v in v["value"].items(): - if self.presets.get(_k): - self.gui_inputs[k][ - "value"][_k]["value"] = self.presets[_k] - if self.presets.get(k): - self.gui_inputs[k]["value"] = self.presets[k] + if presets.get(_k): + gui_inputs[k][ + "value"][_k]["value"] = presets[_k] + if presets.get(k): + gui_inputs[k]["value"] = presets[k] # open widget for plugins inputs - widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) + widget = self.widget(self.gui_name, self.gui_info, gui_inputs) widget.exec_() if len(self.selected) < 1: From 0c91aee6e16500e620f2135eb54ad18c551df6da Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 14:19:38 +0000 Subject: [PATCH 39/42] Create draft PR for #1740 From c739967b48c78ab540df9bc283d370776b203c89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:53:32 +0200 Subject: [PATCH 40/42] hiero: collector ignoring audio clips --- .../hiero/plugins/publish/precollect_instances.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 6e5c640e35..40c6be0f78 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -46,12 +46,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): source_clip = track_item.source() self.log.debug("clip_name: {}".format(clip_name)) - # get clips subtracks and anotations - annotations = self.clip_annotations(source_clip) - subtracks = self.clip_subtrack(track_item) - self.log.debug("Annotations: {}".format(annotations)) - self.log.debug(">> Subtracks: {}".format(subtracks)) - # get openpype tag data tag_data = phiero.get_track_item_pype_data(track_item) self.log.debug("__ tag_data: {}".format(pformat(tag_data))) @@ -62,6 +56,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue + # get clips subtracks and anotations + annotations = self.clip_annotations(source_clip) + subtracks = self.clip_subtrack(track_item) + self.log.debug("Annotations: {}".format(annotations)) + self.log.debug(">> Subtracks: {}".format(subtracks)) + # solve handles length tag_data["handleStart"] = min( tag_data["handleStart"], int(track_item.handleInLength())) From 374f327ccd9145f23849c3627752db4b118c9101 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:58:09 +0200 Subject: [PATCH 41/42] pep8 fix --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 40c6be0f78..4984849aa7 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -56,7 +56,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue - # get clips subtracks and anotations + # get clips subtracks and anotations annotations = self.clip_annotations(source_clip) subtracks = self.clip_subtrack(track_item) self.log.debug("Annotations: {}".format(annotations)) From 43ddbf89991b49a31463bbd3c190fbd8e19be36d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:01:11 +0200 Subject: [PATCH 42/42] update acre commit to fixed version of acre --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 48e6f95469..30dbe50c19 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "efc1b8faa8f84568538b936688ae6f7604dd194c" +resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7" [[package]] name = "aiohttp"