From f5ab38d7d5bf092196a4e9f42846dda40b84fafe Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Feb 2021 08:34:55 +0000 Subject: [PATCH 001/264] Only extend clip range when collecting. This allows support for track items that are shorter than the rest. For example publishing the first couple of frames for static camera shots. --- pype/plugins/hiero/publish/collect_clips.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pype/plugins/hiero/publish/collect_clips.py b/pype/plugins/hiero/publish/collect_clips.py index 724e4730ed1..44de68224d5 100644 --- a/pype/plugins/hiero/publish/collect_clips.py +++ b/pype/plugins/hiero/publish/collect_clips.py @@ -84,7 +84,7 @@ def process(self, context): **locals()) if not source.singleFile(): - self.log.info("Single file") + self.log.info("Sequence files.") is_sequence = True source_path = file_info.filename() @@ -153,10 +153,24 @@ def process(self, context): self.log.info("Created instance.data: {}".format(instance.data)) self.log.debug(">> effects: {}".format(instance.data["effects"])) + # Collect clip range. Only extending the range. + shared_asset = context.data["assetsShared"].get(asset, {}) + + shared_clip_in = shared_asset.get("_clipIn", clip_in) + if shared_clip_in > clip_in: + shared_clip_in = clip_in + + shared_clip_out = shared_asset.get("_clipOut", clip_out) + if shared_clip_out < clip_out: + shared_clip_out = clip_out + context.data["assetsShared"][asset] = { - "_clipIn": clip_in, - "_clipOut": clip_out + "_clipIn": shared_clip_in, + "_clipOut": shared_clip_out } + self.log.debug( + "Clip range: {}-{}".format(shared_clip_in, shared_clip_out) + ) # from now we are collecting only subtrackitems on # track with no video items From 4aedf3519390345326ce5f8ebb51452a1dff047e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 1 Mar 2021 17:49:59 +0100 Subject: [PATCH 002/264] added profiles filtering to pype.lib --- pype/lib/__init__.py | 6 +- pype/lib/profiles_filtering.py | 211 +++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 pype/lib/profiles_filtering.py diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 1167f3b5d1b..d4039f3922f 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -26,6 +26,8 @@ _subprocess ) +from .profiles_filtering import filter_profiles + from .plugin_tools import ( filter_pyblish_plugins, source_hash, @@ -44,7 +46,7 @@ get_paths_from_environ, get_ffmpeg_tool_path ) - +from . from .ffmpeg_utils import ffprobe_streams from .packaging import make_workload_package @@ -69,6 +71,8 @@ "launch_application", "ApplicationAction", + "filter_profiles", + "filter_pyblish_plugins", "get_unique_layer_name", "get_background_layers", diff --git a/pype/lib/profiles_filtering.py b/pype/lib/profiles_filtering.py new file mode 100644 index 00000000000..05bd07eee63 --- /dev/null +++ b/pype/lib/profiles_filtering.py @@ -0,0 +1,211 @@ +import re +import logging + +log = logging.getLogger(__name__) + + +def compile_list_of_regexes(in_list): + """Convert strings in entered list to compiled regex objects.""" + regexes = list() + if not in_list: + return regexes + + for item in in_list: + if not item: + continue + try: + regexes.append(re.compile(item)) + except TypeError: + print(( + "Invalid type \"{}\" value \"{}\"." + " Expected string based object. Skipping." + ).format(str(type(item)), str(item))) + return regexes + + +def _profile_exclusion(matching_profiles, logger): + """Find out most matching profile byt host, task and family match. + + Profiles are selectively filtered. Each item in passed argument must + contain tuple of (profile, profile's score) where score is list of + booleans. Each boolean represents existence of filter for specific key. + Profiles are looped in sequence. In each sequence are profiles split into + true_list and false_list. For next sequence loop are used profiles in + true_list if there are any profiles else false_list is used. + + Filtering ends when only one profile left in true_list. Or when all + existence booleans loops passed, in that case first profile from remainded + profiles is returned. + + Args: + matching_profiles (list): Profiles with same scores. Each item is tuple + with (profile, profile values) + + Returns: + dict: Most matching profile. + """ + + logger.info( + "Search for first most matching profile in match order:" + " Host name -> Task name -> Family." + ) + + if not matching_profiles: + return None + + if len(matching_profiles) == 1: + return matching_profiles[0][0] + + scores_len = len(matching_profiles[0][1]) + for idx in range(scores_len): + profiles_true = [] + profiles_false = [] + for profile, score in matching_profiles: + if score[idx]: + profiles_true.append((profile, score)) + else: + profiles_false.append((profile, score)) + + if profiles_true: + matching_profiles = profiles_true + else: + matching_profiles = profiles_false + + if len(matching_profiles) == 1: + return matching_profiles[0][0] + + return matching_profiles[0][0] + + +def validate_value_by_regexes(value, in_list): + """Validates in any regex from list match entered value. + + Args: + value (str): String where regexes is checked. + in_list (list): List with regexes. + + Returns: + int: Returns `0` when list is not set, is empty or contain "*". + Returns `1` when any regex match value and returns `-1` + when none of regexes match entered value. + """ + if not in_list: + return 0 + + if not isinstance(in_list, (list, tuple, set)): + in_list = [in_list] + + if "*" in in_list: + return 0 + + # If value is not set and in list has specific values then resolve value + # as not matching. + if not value: + return -1 + + regexes = compile_list_of_regexes(in_list) + for regex in regexes: + if re.match(regex, value): + return 1 + return -1 + + +def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): + """ Filter profiles by entered key -> values. + + Profile if marked with score for each key/value from `key_values` with + points -1, 0 or 1. + - if profile contain the key and profile's value contain value from + `key_values` then profile gets 1 point + - if profile does not contain the key or profile's value is empty or + contain "*" then got 0 point + - if profile contain the key, profile's value is not empty and does not + contain "*" and value from `key_values` is not available in the value + then got -1 point + + If profile gets -1 point at any time then is skipped and not used for + output. Profile with higher score is returned. If there are multiple + profiles with same score then first in order is used (order of profiles + matter). + + Args: + profiles_data (list): Profile definitions as dictionaries. + key_values (dict): Mapping of Key <-> Value. Key is checked if is + available in profile and if Value is matching it's values. + keys_order (list, tuple): Order of keys from `key_values` which matters + only when multiple profiles have same score. + logger (logging.Logger): Optionally can be passed different logger. + + Returns: + dict/None: Return most matching profile or None if none of profiles + match at least one criteria. + """ + if not profiles_data: + return None + + if not logger: + logger = log + + if not keys_order: + keys_order = tuple(key_values.keys()) + else: + _keys_order = list(keys_order) + # Make all keys from `key_values` are passed + for key in key_values.keys(): + if key not in _keys_order: + _keys_order.append(key) + keys_order = tuple(_keys_order) + + matching_profiles = None + highest_profile_points = -1 + # Each profile get 1 point for each matching filter. Profile with most + # points is returned. For cases when more than one profile will match + # are also stored ordered lists of matching values. + for profile in profiles_data: + profile_points = 0 + profile_scores = [] + + for key in keys_order: + value = key_values[key] + match = validate_value_by_regexes(value, profile.get(key)) + if match == -1: + profile_value = profile.get(key) or [] + logger.debug( + "\"{}\" not found in {}".format(key, profile_value) + ) + profile_points = -1 + break + + profile_points += match + profile_scores.append(bool(match)) + + if ( + profile_points < 0 + or profile_points < highest_profile_points + ): + continue + + if profile_points > highest_profile_points: + matching_profiles = [] + highest_profile_points = profile_points + + if profile_points == highest_profile_points: + matching_profiles.append((profile, profile_scores)) + + log_parts = " | ".join([ + "{}: \"{}\"".format(*item) + for item in key_values.items() + ]) + + if not matching_profiles: + logger.warning( + "None of profiles match your setup. {}".format(log_parts) + ) + return None + + if len(matching_profiles) > 1: + logger.warning( + "More than one profile match your setup. {}".format(log_parts) + ) + + return _profile_exclusion(matching_profiles, logger) From 6a14ef68d868720925de6fc1a3b5254c45a84189 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 10:19:34 +0100 Subject: [PATCH 003/264] fix pype.lib init file --- pype/lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index d4039f3922f..7edc360c149 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -46,7 +46,7 @@ get_paths_from_environ, get_ffmpeg_tool_path ) -from . + from .ffmpeg_utils import ffprobe_streams from .packaging import make_workload_package From eeb166255daeab14765fdf386e3afe713fc40dca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 10:20:40 +0100 Subject: [PATCH 004/264] implemented PypeCreatorMixin and Creator --- pype/api.py | 5 +++ pype/plugin.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/pype/api.py b/pype/api.py index b88be4cc888..b2d8739c2b3 100644 --- a/pype/api.py +++ b/pype/api.py @@ -20,6 +20,9 @@ from . import resources from .plugin import ( + PypeCreatorMixin, + Creator, + Extractor, ValidatePipelineOrder, @@ -67,6 +70,8 @@ "resources", # plugin classes + "PypeCreatorMixin", + "Creator", "Extractor", # ordering "ValidatePipelineOrder", diff --git a/pype/plugin.py b/pype/plugin.py index a169e82bebd..1b0bed4bb26 100644 --- a/pype/plugin.py +++ b/pype/plugin.py @@ -1,9 +1,10 @@ -import tempfile import os +import inspect +import tempfile import pyblish.api - +import avalon.api from pype.api import config -import inspect +from pype.lib import filter_profiles ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 @@ -37,6 +38,92 @@ def imprint_attributes(plugin): print("setting {}: {} on {}".format(option, value, plugin_name)) +class TaskNotSetError(KeyError): + def __init__(self, msg=None): + if not msg: + msg = "Creator's subset name template requires task name." + super(TaskNotSetError, self).__init__(msg) + + +class PypeCreatorMixin: + """Helper to override avalon's default class methods. + + Mixin class must be used as first in inheritance order to override methods. + """ + default_tempate = "{family}{Variant}" + + @classmethod + def get_subset_name( + cls, variant, task_name, asset_id, project_name, host_name=None + ): + if not cls.family: + return "" + + if not host_name: + host_name = os.environ["AVALON_APP"] + + # Use only last part of class family value split by dot (`.`) + family = cls.family.rsplit(".", 1)[-1] + + # Get settings + profiles = ( + config.get_presets(project_name) + .get("tools", {}) + .get("creator_subset_name_profiles") + ) or {} + filtering_criteria = { + "families": family, + "hosts": host_name, + "tasks": task_name + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + template = None + if matching_profile: + template = matching_profile["template"] + + # Make sure template is set (matching may have empty string) + if not template: + template = cls.default_tempate + + # Simple check of task name existence for template with {task} in + # - missing task should be possible only in Standalone publisher + if not task_name and "{task" in template.lower(): + raise TaskNotSetError() + + fill_pairs = ( + ("variant", variant), + ("family", family), + ("task", task_name) + ) + fill_data = {} + for key, value in fill_pairs: + # Handle cases when value is `None` (standalone publisher) + if value is None: + continue + # Keep value as it is + fill_data[key] = value + # Both key and value are with upper case + fill_data[key.upper()] = value.upper() + + # Capitalize only first char of value + # - conditions are because of possible index errors + 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:] + fill_data[key.capitalize()] = capitalized + + return template.format(**fill_data) + + +class Creator(PypeCreatorMixin, avalon.api.Creator): + pass + + class ContextPlugin(pyblish.api.ContextPlugin): def process(cls, *args, **kwargs): imprint_attributes(cls) From f69ba81d104cb376005469617592b6a2d6b30632 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 10:25:18 +0100 Subject: [PATCH 005/264] modified host's creator plugins to use PypeCreatorMixin --- pype/hosts/blender/plugin.py | 6 ++++++ pype/hosts/harmony/plugin.py | 6 ++++++ pype/hosts/houdini/plugin.py | 6 ++++++ pype/hosts/maya/plugin.py | 6 ++++++ pype/hosts/nuke/plugin.py | 8 ++++++-- pype/hosts/resolve/plugin.py | 3 ++- pype/hosts/tvpaint/plugin.py | 6 ++++++ pype/plugins/aftereffects/create/create_render.py | 4 ++-- pype/plugins/blender/create/create_action.py | 4 ++-- pype/plugins/blender/create/create_animation.py | 2 +- pype/plugins/blender/create/create_camera.py | 4 ++-- pype/plugins/blender/create/create_layout.py | 4 ++-- pype/plugins/blender/create/create_model.py | 4 ++-- pype/plugins/blender/create/create_rig.py | 4 ++-- pype/plugins/blender/create/create_setdress.py | 3 ++- pype/plugins/harmony/create/create_farm_render.py | 3 ++- pype/plugins/harmony/create/create_render.py | 3 ++- pype/plugins/harmony/create/create_template.py | 3 ++- pype/plugins/houdini/create/create_alembic_camera.py | 4 ++-- pype/plugins/houdini/create/create_pointcache.py | 4 ++-- pype/plugins/houdini/create/create_vbd_cache.py | 4 ++-- pype/plugins/maya/create/create_animation.py | 8 +++++--- pype/plugins/maya/create/create_ass.py | 8 +++++--- pype/plugins/maya/create/create_assembly.py | 4 ++-- pype/plugins/maya/create/create_camera.py | 8 +++++--- pype/plugins/maya/create/create_layout.py | 4 ++-- pype/plugins/maya/create/create_look.py | 8 +++++--- pype/plugins/maya/create/create_mayaascii.py | 4 ++-- pype/plugins/maya/create/create_model.py | 4 ++-- pype/plugins/maya/create/create_pointcache.py | 8 +++++--- pype/plugins/maya/create/create_render.py | 8 +++++--- pype/plugins/maya/create/create_rendersetup.py | 8 +++++--- pype/plugins/maya/create/create_review.py | 8 +++++--- pype/plugins/maya/create/create_rig.py | 8 +++++--- pype/plugins/maya/create/create_setdress.py | 4 ++-- pype/plugins/maya/create/create_unreal_staticmesh.py | 4 ++-- pype/plugins/maya/create/create_vrayproxy.py | 4 ++-- pype/plugins/maya/create/create_vrayscene.py | 8 +++++--- pype/plugins/maya/create/create_yeti_cache.py | 8 +++++--- pype/plugins/maya/create/create_yeti_rig.py | 8 +++++--- pype/plugins/nuke/create/create_backdrop.py | 4 ++-- pype/plugins/nuke/create/create_camera.py | 4 ++-- pype/plugins/nuke/create/create_gizmo.py | 4 ++-- pype/plugins/nuke/create/create_read.py | 4 ++-- pype/plugins/photoshop/create/create_image.py | 4 ++-- pype/plugins/tvpaint/create/create_render_layer.py | 3 ++- pype/plugins/tvpaint/create/create_render_pass.py | 3 ++- pype/plugins/tvpaint/create/create_review.py | 3 ++- 48 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 pype/hosts/harmony/plugin.py create mode 100644 pype/hosts/houdini/plugin.py create mode 100644 pype/hosts/tvpaint/plugin.py diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index d0b81148c34..f216eb28be7 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -6,6 +6,8 @@ import bpy from avalon import api +import avalon.blender +from pype.api import PypeCreatorMixin VALID_EXTENSIONS = [".blend", ".json"] @@ -100,6 +102,10 @@ def get_local_collection_with_name(name): return None +class Creator(PypeCreatorMixin, avalon.blender.Creator): + pass + + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/hosts/harmony/plugin.py b/pype/hosts/harmony/plugin.py new file mode 100644 index 00000000000..3525ad686d1 --- /dev/null +++ b/pype/hosts/harmony/plugin.py @@ -0,0 +1,6 @@ +from avalon import harmony +from pype.api import PypeCreatorMixin + + +class Creator(PypeCreatorMixin, harmony.Creator): + pass diff --git a/pype/hosts/houdini/plugin.py b/pype/hosts/houdini/plugin.py new file mode 100644 index 00000000000..864cc59f098 --- /dev/null +++ b/pype/hosts/houdini/plugin.py @@ -0,0 +1,6 @@ +from avalon import houdini +from pype.api import PypeCreatorMixin + + +class Creator(PypeCreatorMixin, houdini.Creator): + pass diff --git a/pype/hosts/maya/plugin.py b/pype/hosts/maya/plugin.py index c7416920df0..81c89017ff1 100644 --- a/pype/hosts/maya/plugin.py +++ b/pype/hosts/maya/plugin.py @@ -1,5 +1,7 @@ from avalon import api from avalon.vendor import qargparse +import avalon.maya +from pype.api import PypeCreatorMixin def get_reference_node_parents(ref): @@ -26,6 +28,10 @@ def get_reference_node_parents(ref): return parents +class Creator(PypeCreatorMixin, avalon.maya.Creator): + pass + + class ReferenceLoader(api.Loader): """A basic ReferenceLoader for Maya diff --git a/pype/hosts/nuke/plugin.py b/pype/hosts/nuke/plugin.py index 652c0396a8c..b60c00b7889 100644 --- a/pype/hosts/nuke/plugin.py +++ b/pype/hosts/nuke/plugin.py @@ -1,9 +1,13 @@ import re import avalon.api import avalon.nuke -from pype.api import config +from pype.api import ( + config, + PypeCreatorMixin +) -class PypeCreator(avalon.nuke.pipeline.Creator): + +class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator): """Pype Nuke Creator class wrapper """ def __init__(self, *args, **kwargs): diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 72eec048960..23aeac2e522 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -3,6 +3,7 @@ from pype.hosts import resolve from avalon.vendor import qargparse from pype.api import config +import pype.api from Qt import QtWidgets, QtCore @@ -251,7 +252,7 @@ def remove(self, container): pass -class Creator(api.Creator): +class Creator(pype.api.Creator): """Creator class wrapper """ marker_color = "Purple" diff --git a/pype/hosts/tvpaint/plugin.py b/pype/hosts/tvpaint/plugin.py new file mode 100644 index 00000000000..6f069586a51 --- /dev/null +++ b/pype/hosts/tvpaint/plugin.py @@ -0,0 +1,6 @@ +from pype.api import PypeCreatorMixin +from avalon.tvpaint import pipeline + + +class Creator(PypeCreatorMixin, pipeline.Creator): + pass diff --git a/pype/plugins/aftereffects/create/create_render.py b/pype/plugins/aftereffects/create/create_render.py index 6d876e349da..b346bc60d82 100644 --- a/pype/plugins/aftereffects/create/create_render.py +++ b/pype/plugins/aftereffects/create/create_render.py @@ -1,4 +1,4 @@ -from avalon import api +import pype.api from avalon.vendor import Qt from avalon import aftereffects @@ -7,7 +7,7 @@ log = logging.getLogger(__name__) -class CreateRender(api.Creator): +class CreateRender(pype.api.Creator): """Render folder for publish.""" name = "renderDefault" diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index f5273863c49..d43738abd17 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -3,11 +3,11 @@ import bpy from avalon import api -from avalon.blender import Creator, lib +from avalon.blender import lib import pype.hosts.blender.plugin -class CreateAction(Creator): +class CreateAction(pype.hosts.blender.plugin.Creator): """Action output for character rigs""" name = "actionMain" diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index acfd6ac1f38..f0a165137aa 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -6,7 +6,7 @@ import pype.hosts.blender.plugin -class CreateAnimation(blender.Creator): +class CreateAnimation(pype.hosts.blender.plugin.Creator): """Animation output for character rigs""" name = "animationMain" diff --git a/pype/plugins/blender/create/create_camera.py b/pype/plugins/blender/create/create_camera.py index 5817985053b..0d82b2c0e1e 100644 --- a/pype/plugins/blender/create/create_camera.py +++ b/pype/plugins/blender/create/create_camera.py @@ -3,11 +3,11 @@ import bpy from avalon import api -from avalon.blender import Creator, lib +from avalon.blender import lib import pype.hosts.blender.plugin -class CreateCamera(Creator): +class CreateCamera(pype.hosts.blender.plugin.Creator): """Polygonal static geometry""" name = "cameraMain" diff --git a/pype/plugins/blender/create/create_layout.py b/pype/plugins/blender/create/create_layout.py index 010eec539b3..39beb53ddd6 100644 --- a/pype/plugins/blender/create/create_layout.py +++ b/pype/plugins/blender/create/create_layout.py @@ -3,11 +3,11 @@ import bpy from avalon import api -from avalon.blender import Creator, lib +from avalon.blender import lib import pype.hosts.blender.plugin -class CreateLayout(Creator): +class CreateLayout(pype.hosts.blender.plugin.Creator): """Layout output for character rigs""" name = "layoutMain" diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 59905edc41a..5e32e6f5618 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -3,11 +3,11 @@ import bpy from avalon import api -from avalon.blender import Creator, lib +from avalon.blender import lib import pype.hosts.blender.plugin -class CreateModel(Creator): +class CreateModel(pype.hosts.blender.plugin.Creator): """Polygonal static geometry""" name = "modelMain" diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 5c85bf969d7..5a3c879c506 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -3,11 +3,11 @@ import bpy from avalon import api -from avalon.blender import Creator, lib +from avalon.blender import lib import pype.hosts.blender.plugin -class CreateRig(Creator): +class CreateRig(pype.hosts.blender.plugin.Creator): """Artist-friendly rig with controls to direct motion""" name = "rigMain" diff --git a/pype/plugins/blender/create/create_setdress.py b/pype/plugins/blender/create/create_setdress.py index 06acf716e52..7c163f0e1e6 100644 --- a/pype/plugins/blender/create/create_setdress.py +++ b/pype/plugins/blender/create/create_setdress.py @@ -3,7 +3,8 @@ from avalon import api, blender import pype.hosts.blender.plugin -class CreateSetDress(blender.Creator): + +class CreateSetDress(pype.hosts.blender.plugin.Creator): """A grouped package of loaded content""" name = "setdressMain" diff --git a/pype/plugins/harmony/create/create_farm_render.py b/pype/plugins/harmony/create/create_farm_render.py index e134f28f432..a1b198b672d 100644 --- a/pype/plugins/harmony/create/create_farm_render.py +++ b/pype/plugins/harmony/create/create_farm_render.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """Create Composite node for render on farm.""" from avalon import harmony +from pype.hosts.harmony.api import plugin -class CreateFarmRender(harmony.Creator): +class CreateFarmRender(plugin.Creator): """Composite node for publishing renders.""" name = "renderDefault" diff --git a/pype/plugins/harmony/create/create_render.py b/pype/plugins/harmony/create/create_render.py index 5034a1bb89b..b9a0987b37d 100644 --- a/pype/plugins/harmony/create/create_render.py +++ b/pype/plugins/harmony/create/create_render.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """Create render node.""" from avalon import harmony +from pype.hosts.harmony.api import plugin -class CreateRender(harmony.Creator): +class CreateRender(plugin.Creator): """Composite node for publishing renders.""" name = "renderDefault" diff --git a/pype/plugins/harmony/create/create_template.py b/pype/plugins/harmony/create/create_template.py index babc3fe8d7e..880cc82405b 100644 --- a/pype/plugins/harmony/create/create_template.py +++ b/pype/plugins/harmony/create/create_template.py @@ -1,7 +1,8 @@ from avalon import harmony +from pype.hosts.harmony.api import plugin -class CreateTemplate(harmony.Creator): +class CreateTemplate(plugin.Creator): """Composite node for publishing to templates.""" name = "templateDefault" diff --git a/pype/plugins/houdini/create/create_alembic_camera.py b/pype/plugins/houdini/create/create_alembic_camera.py index cf8ac41b62f..a12edf2c9b9 100644 --- a/pype/plugins/houdini/create/create_alembic_camera.py +++ b/pype/plugins/houdini/create/create_alembic_camera.py @@ -1,7 +1,7 @@ -from avalon import houdini +from pype.hosts.houdini import plugin -class CreateAlembicCamera(houdini.Creator): +class CreateAlembicCamera(plugin.Creator): """Single baked camera from Alembic ROP""" name = "camera" diff --git a/pype/plugins/houdini/create/create_pointcache.py b/pype/plugins/houdini/create/create_pointcache.py index ae7e8450837..883f37a1228 100644 --- a/pype/plugins/houdini/create/create_pointcache.py +++ b/pype/plugins/houdini/create/create_pointcache.py @@ -1,7 +1,7 @@ -from avalon import houdini +from pype.hosts.houdini import plugin -class CreatePointCache(houdini.Creator): +class CreatePointCache(plugin.Creator): """Alembic ROP to pointcache""" name = "pointcache" diff --git a/pype/plugins/houdini/create/create_vbd_cache.py b/pype/plugins/houdini/create/create_vbd_cache.py index e862d5c96d0..ebbaaf3a0e8 100644 --- a/pype/plugins/houdini/create/create_vbd_cache.py +++ b/pype/plugins/houdini/create/create_vbd_cache.py @@ -1,7 +1,7 @@ -from avalon import houdini +from pype.hosts.houdini import plugin -class CreateVDBCache(houdini.Creator): +class CreateVDBCache(plugin.Creator): """OpenVDB from Geometry ROP""" name = "vbdcache" diff --git a/pype/plugins/maya/create/create_animation.py b/pype/plugins/maya/create/create_animation.py index 7bafce774ce..e0017591c7e 100644 --- a/pype/plugins/maya/create/create_animation.py +++ b/pype/plugins/maya/create/create_animation.py @@ -1,8 +1,10 @@ -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreateAnimation(avalon.maya.Creator): +class CreateAnimation(plugin.Creator): """Animation output for character rigs""" name = "animationDefault" diff --git a/pype/plugins/maya/create/create_ass.py b/pype/plugins/maya/create/create_ass.py index 7fd66e8e15f..ae286dbc691 100644 --- a/pype/plugins/maya/create/create_ass.py +++ b/pype/plugins/maya/create/create_ass.py @@ -1,12 +1,14 @@ from collections import OrderedDict -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) from maya import cmds -class CreateAss(avalon.maya.Creator): +class CreateAss(plugin.Creator): """Arnold Archive""" name = "ass" diff --git a/pype/plugins/maya/create/create_assembly.py b/pype/plugins/maya/create/create_assembly.py index 6d0321b7189..a7a024507b9 100644 --- a/pype/plugins/maya/create/create_assembly.py +++ b/pype/plugins/maya/create/create_assembly.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateAssembly(avalon.maya.Creator): +class CreateAssembly(plugin.Creator): """A grouped package of loaded content""" name = "assembly" diff --git a/pype/plugins/maya/create/create_camera.py b/pype/plugins/maya/create/create_camera.py index acff93c03cc..160882696af 100644 --- a/pype/plugins/maya/create/create_camera.py +++ b/pype/plugins/maya/create/create_camera.py @@ -1,8 +1,10 @@ -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreateCamera(avalon.maya.Creator): +class CreateCamera(plugin.Creator): """Single baked camera""" name = "cameraMain" diff --git a/pype/plugins/maya/create/create_layout.py b/pype/plugins/maya/create/create_layout.py index 7f0c82d80ec..a37ecbf4380 100644 --- a/pype/plugins/maya/create/create_layout.py +++ b/pype/plugins/maya/create/create_layout.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateLayout(avalon.maya.Creator): +class CreateLayout(plugin.Creator): """A grouped package of loaded content""" name = "layoutMain" diff --git a/pype/plugins/maya/create/create_look.py b/pype/plugins/maya/create/create_look.py index 5ea64cc7e4d..04f5603d4d1 100644 --- a/pype/plugins/maya/create/create_look.py +++ b/pype/plugins/maya/create/create_look.py @@ -1,8 +1,10 @@ -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreateLook(avalon.maya.Creator): +class CreateLook(plugin.Creator): """Shader connections defining shape look""" name = "look" diff --git a/pype/plugins/maya/create/create_mayaascii.py b/pype/plugins/maya/create/create_mayaascii.py index e7cc40dc24a..45912a7789e 100644 --- a/pype/plugins/maya/create/create_mayaascii.py +++ b/pype/plugins/maya/create/create_mayaascii.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateMayaAscii(avalon.maya.Creator): +class CreateMayaAscii(plugin.Creator): """Raw Maya Ascii file export""" name = "mayaAscii" diff --git a/pype/plugins/maya/create/create_model.py b/pype/plugins/maya/create/create_model.py index 241e2be7f92..f27a854d9a9 100644 --- a/pype/plugins/maya/create/create_model.py +++ b/pype/plugins/maya/create/create_model.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateModel(avalon.maya.Creator): +class CreateModel(plugin.Creator): """Polygonal static geometry""" name = "modelMain" diff --git a/pype/plugins/maya/create/create_pointcache.py b/pype/plugins/maya/create/create_pointcache.py index 1eb561b5ce8..a8f8033e4aa 100644 --- a/pype/plugins/maya/create/create_pointcache.py +++ b/pype/plugins/maya/create/create_pointcache.py @@ -1,8 +1,10 @@ -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreatePointCache(avalon.maya.Creator): +class CreatePointCache(plugin.Creator): """Alembic pointcache for animated data""" name = "pointcache" diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index e3cd7ee64dc..d63dd75fe41 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -8,11 +8,13 @@ from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup -from pype.hosts.maya import lib -import avalon.maya +from pype.hosts.maya import ( + lib, + plugin +) -class CreateRender(avalon.maya.Creator): +class CreateRender(plugin.Creator): """Create *render* instance. Render instances are not actually published, they hold options for diff --git a/pype/plugins/maya/create/create_rendersetup.py b/pype/plugins/maya/create/create_rendersetup.py index 98f54f2d709..815e5893fba 100644 --- a/pype/plugins/maya/create/create_rendersetup.py +++ b/pype/plugins/maya/create/create_rendersetup.py @@ -1,9 +1,11 @@ -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) from maya import cmds -class CreateRenderSetup(avalon.maya.Creator): +class CreateRenderSetup(plugin.Creator): """Create rendersetup template json data""" name = "rendersetup" diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index bfeab33f5b8..618565cb56c 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -1,9 +1,11 @@ from collections import OrderedDict -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreateReview(avalon.maya.Creator): +class CreateReview(plugin.Creator): """Single baked camera""" name = "reviewDefault" diff --git a/pype/plugins/maya/create/create_rig.py b/pype/plugins/maya/create/create_rig.py index ae1de4243e8..da82422f250 100644 --- a/pype/plugins/maya/create/create_rig.py +++ b/pype/plugins/maya/create/create_rig.py @@ -1,10 +1,12 @@ from maya import cmds -from pype.hosts.maya import lib -import avalon.maya +from pype.hosts.maya import ( + lib, + plugin +) -class CreateRig(avalon.maya.Creator): +class CreateRig(plugin.Creator): """Artist-friendly rig with controls to direct motion""" name = "rigDefault" diff --git a/pype/plugins/maya/create/create_setdress.py b/pype/plugins/maya/create/create_setdress.py index d5fc0012996..51971c26e65 100644 --- a/pype/plugins/maya/create/create_setdress.py +++ b/pype/plugins/maya/create/create_setdress.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateSetDress(avalon.maya.Creator): +class CreateSetDress(plugin.Creator): """A grouped package of loaded content""" name = "setdressMain" diff --git a/pype/plugins/maya/create/create_unreal_staticmesh.py b/pype/plugins/maya/create/create_unreal_staticmesh.py index 5a74cb22d55..af8e3aa3af2 100644 --- a/pype/plugins/maya/create/create_unreal_staticmesh.py +++ b/pype/plugins/maya/create/create_unreal_staticmesh.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateUnrealStaticMesh(avalon.maya.Creator): +class CreateUnrealStaticMesh(plugin.Creator): name = "staticMeshMain" label = "Unreal - Static Mesh" family = "unrealStaticMesh" diff --git a/pype/plugins/maya/create/create_vrayproxy.py b/pype/plugins/maya/create/create_vrayproxy.py index 010157ca9a7..f9c145ea84e 100644 --- a/pype/plugins/maya/create/create_vrayproxy.py +++ b/pype/plugins/maya/create/create_vrayproxy.py @@ -1,7 +1,7 @@ -import avalon.maya +from pype.hosts.maya import plugin -class CreateVrayProxy(avalon.maya.Creator): +class CreateVrayProxy(plugin.Creator): """Alembic pointcache for animated data""" name = "vrayproxy" diff --git a/pype/plugins/maya/create/create_vrayscene.py b/pype/plugins/maya/create/create_vrayscene.py index b8dbdaafe74..bf108a18604 100644 --- a/pype/plugins/maya/create/create_vrayscene.py +++ b/pype/plugins/maya/create/create_vrayscene.py @@ -8,11 +8,13 @@ from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup -from pype.hosts.maya import lib -import avalon.maya +from pype.hosts.maya import ( + lib, + plugin +) -class CreateVRayScene(avalon.maya.Creator): +class CreateVRayScene(plugin.Creator): """Create Vray Scene.""" label = "VRay Scene" diff --git a/pype/plugins/maya/create/create_yeti_cache.py b/pype/plugins/maya/create/create_yeti_cache.py index a4b5cc537a7..b95fc5997ed 100644 --- a/pype/plugins/maya/create/create_yeti_cache.py +++ b/pype/plugins/maya/create/create_yeti_cache.py @@ -1,10 +1,12 @@ from collections import OrderedDict -import avalon.maya -from pype.hosts.maya import lib +from pype.hosts.maya import ( + lib, + plugin +) -class CreateYetiCache(avalon.maya.Creator): +class CreateYetiCache(plugin.Creator): """Output for procedural plugin nodes of Yeti """ name = "yetiDefault" diff --git a/pype/plugins/maya/create/create_yeti_rig.py b/pype/plugins/maya/create/create_yeti_rig.py index 0b954f500d7..5c57f56cea5 100644 --- a/pype/plugins/maya/create/create_yeti_rig.py +++ b/pype/plugins/maya/create/create_yeti_rig.py @@ -1,10 +1,12 @@ from maya import cmds -from pype.hosts.maya import lib -import avalon.maya +from pype.hosts.maya import ( + lib, + plugin +) -class CreateYetiRig(avalon.maya.Creator): +class CreateYetiRig(plugin.Creator): """Output for procedural plugin nodes ( Yeti / XGen / etc)""" label = "Yeti Rig" diff --git a/pype/plugins/nuke/create/create_backdrop.py b/pype/plugins/nuke/create/create_backdrop.py index 0d2b621e213..72f4417b4cf 100644 --- a/pype/plugins/nuke/create/create_backdrop.py +++ b/pype/plugins/nuke/create/create_backdrop.py @@ -1,9 +1,9 @@ -import avalon.nuke from avalon.nuke import lib as anlib +from pype.hosts.nuke import plugin import nuke -class CreateBackdrop(avalon.nuke.Creator): +class CreateBackdrop(plugin.Creator): """Add Publishable Backdrop""" name = "nukenodes" diff --git a/pype/plugins/nuke/create/create_camera.py b/pype/plugins/nuke/create/create_camera.py index 4c668925ad7..3c5a50cfb8f 100644 --- a/pype/plugins/nuke/create/create_camera.py +++ b/pype/plugins/nuke/create/create_camera.py @@ -1,9 +1,9 @@ -import avalon.nuke from avalon.nuke import lib as anlib +from pype.hosts.nuke import plugin import nuke -class CreateCamera(avalon.nuke.Creator): +class CreateCamera(plugin.Creator): """Add Publishable Backdrop""" name = "camera" diff --git a/pype/plugins/nuke/create/create_gizmo.py b/pype/plugins/nuke/create/create_gizmo.py index eb5b1a3fb05..2b7edaabcbf 100644 --- a/pype/plugins/nuke/create/create_gizmo.py +++ b/pype/plugins/nuke/create/create_gizmo.py @@ -1,9 +1,9 @@ -import avalon.nuke from avalon.nuke import lib as anlib +from pype.hosts.nuke import plugin import nuke -class CreateGizmo(avalon.nuke.Creator): +class CreateGizmo(plugin.Creator): """Add Publishable "gizmo" group The name is symbolically gizmo as presumably diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 70db580a7e0..b2b857e0f3d 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -1,12 +1,12 @@ from collections import OrderedDict -import avalon.api import avalon.nuke from pype import api as pype +from pype.hosts.nuke import plugin import nuke -class CrateRead(avalon.nuke.Creator): +class CrateRead(plugin.Creator): # change this to template preset name = "ReadCopy" label = "Create Read Copy" diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index c1a7d92a2c1..54b6efad291 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,9 +1,9 @@ -from avalon import api +import pype.api from avalon.vendor import Qt from avalon import photoshop -class CreateImage(api.Creator): +class CreateImage(pype.api.Creator): """Image folder for publish.""" name = "imageDefault" diff --git a/pype/plugins/tvpaint/create/create_render_layer.py b/pype/plugins/tvpaint/create/create_render_layer.py index c2921cebbea..ed7c96c9047 100644 --- a/pype/plugins/tvpaint/create/create_render_layer.py +++ b/pype/plugins/tvpaint/create/create_render_layer.py @@ -1,7 +1,8 @@ from avalon.tvpaint import pipeline, lib +from pype.hosts.tvpaint.api import plugin -class CreateRenderlayer(pipeline.Creator): +class CreateRenderlayer(plugin.Creator): """Mark layer group as one instance.""" name = "render_layer" label = "RenderLayer" diff --git a/pype/plugins/tvpaint/create/create_render_pass.py b/pype/plugins/tvpaint/create/create_render_pass.py index 7e4b2a4e81d..8583f204517 100644 --- a/pype/plugins/tvpaint/create/create_render_pass.py +++ b/pype/plugins/tvpaint/create/create_render_pass.py @@ -1,7 +1,8 @@ from avalon.tvpaint import pipeline, lib +from pype.hosts.tvpaint.api import plugin -class CreateRenderPass(pipeline.Creator): +class CreateRenderPass(plugin.Creator): """Render pass is combination of one or more layers from same group. Requirement to create Render Pass is to have already created beauty diff --git a/pype/plugins/tvpaint/create/create_review.py b/pype/plugins/tvpaint/create/create_review.py index 9f7ee1396e8..cfc49a8ac6f 100644 --- a/pype/plugins/tvpaint/create/create_review.py +++ b/pype/plugins/tvpaint/create/create_review.py @@ -1,7 +1,8 @@ from avalon.tvpaint import pipeline +from pype.hosts.tvpaint.api import plugin -class CreateReview(pipeline.Creator): +class CreateReview(plugin.Creator): """Review for global review of all layers.""" name = "review" label = "Review" From 740b98276fae637d38ec767a7b217b12ee618512 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 10:26:32 +0100 Subject: [PATCH 006/264] fixed standalone publisher to work with new Creator subset names --- pype/tools/standalonepublish/__init__.py | 6 +- pype/tools/standalonepublish/__main__.py | 4 +- pype/tools/standalonepublish/app.py | 8 + .../standalonepublish/widgets/constants.py | 1 + .../standalonepublish/widgets/widget_asset.py | 22 +- .../widgets/widget_components.py | 5 +- .../widgets/widget_family.py | 211 ++++++++++++------ 7 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 pype/tools/standalonepublish/widgets/constants.py diff --git a/pype/tools/standalonepublish/__init__.py b/pype/tools/standalonepublish/__init__.py index 29a4e529045..3526a8044e5 100644 --- a/pype/tools/standalonepublish/__init__.py +++ b/pype/tools/standalonepublish/__init__.py @@ -1,8 +1,6 @@ from .app import ( - show, - cli + Window ) __all__ = [ - "show", - "cli" + "Window" ] diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py index 85a574f8dcf..5b87f027f21 100644 --- a/pype/tools/standalonepublish/__main__.py +++ b/pype/tools/standalonepublish/__main__.py @@ -1,14 +1,16 @@ import os import sys -import app import ctypes import signal +import app from Qt import QtWidgets, QtGui from avalon import style from pype.api import resources +from pype.tools.standalonepublish.widgets.constants import HOST_NAME if __name__ == "__main__": + os.environ["AVALON_APP"] = HOST_NAME # Allow to change icon of running process in windows taskbar if os.name == "nt": diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index feba46987ff..8f522874fde 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -62,6 +62,8 @@ def __init__(self, pyblish_paths, parent=None): # signals widget_assets.selection_changed.connect(self.on_asset_changed) + widget_assets.task_changed.connect(self._on_task_change) + widget_assets.project_changed.connect(self.on_project_change) widget_family.stateChanged.connect(self.set_valid_family) self.widget_assets = widget_assets @@ -116,6 +118,9 @@ def get_avalon_parent(self, entity): parents.append(parent['name']) return parents + def on_project_change(self, project_name): + self.widget_family.refresh() + def on_asset_changed(self): '''Callback on asset selection changed @@ -135,6 +140,9 @@ def on_asset_changed(self): self.widget_family.change_asset(None) self.widget_family.on_data_changed() + def _on_task_change(self): + self.widget_family.on_task_change() + def keyPressEvent(self, event): ''' Handling Ctrl+V KeyPress event Can handle: diff --git a/pype/tools/standalonepublish/widgets/constants.py b/pype/tools/standalonepublish/widgets/constants.py new file mode 100644 index 00000000000..0ecc8e82e7e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/constants.py @@ -0,0 +1 @@ +HOST_NAME = "standalonepublisher" diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 6f041a535f3..4680e88344c 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -121,9 +121,11 @@ class AssetWidget(QtWidgets.QWidget): """ + project_changed = QtCore.Signal(str) assets_refreshed = QtCore.Signal() # on model refresh selection_changed = QtCore.Signal() # on view selection change current_changed = QtCore.Signal() # on view current index change + task_changed = QtCore.Signal() def __init__(self, dbcon, parent=None): super(AssetWidget, self).__init__(parent=parent) @@ -189,6 +191,9 @@ def __init__(self, dbcon, parent=None): selection = view.selectionModel() selection.selectionChanged.connect(self.selection_changed) selection.currentChanged.connect(self.current_changed) + task_view.selectionModel().selectionChanged.connect( + self._on_task_change + ) refresh.clicked.connect(self.refresh) self.selection_changed.connect(self._refresh_tasks) @@ -249,6 +254,9 @@ def on_project_change(self): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name + + self.project_changed.emit(project_name) + self.refresh() def _refresh_model(self): @@ -265,7 +273,18 @@ def _refresh_model(self): def refresh(self): self._refresh_model() + def _on_task_change(self): + try: + index = self.task_view.selectedIndexes()[0] + task_name = self.task_model.itemData(index)[0] + except Exception: + task_name = None + + self.dbcon.Session["AVALON_TASK"] = task_name + self.task_changed.emit() + def _refresh_tasks(self): + self.dbcon.Session["AVALON_TASK"] = None tasks = [] selected = self.get_selected_assets() if len(selected) == 1: @@ -275,7 +294,8 @@ def _refresh_tasks(self): if asset: tasks = asset.get('data', {}).get('tasks', []) self.task_model.set_tasks(tasks) - self.task_view.setVisible(len(tasks)>0) + self.task_view.setVisible(len(tasks) > 0) + self.task_changed.emit() def get_active_asset(self): """Return the asset id the current asset.""" diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 7e0327f00a7..207873a92ac 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -7,6 +7,7 @@ from Qt import QtWidgets, QtCore from . import DropDataFrame +from .constants import HOST_NAME from avalon import io from pype.api import execute, Logger @@ -177,8 +178,8 @@ def set_context(project, asset, task): io.Session["current_dir"] = os.path.normpath(os.getcwd()) - os.environ["AVALON_APP"] = "standalonepublish" - io.Session["AVALON_APP"] = "standalonepublish" + os.environ["AVALON_APP"] = HOST_NAME + io.Session["AVALON_APP"] = HOST_NAME io.uninstall() diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 1c8f2238fcd..a42cc0b04f0 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -1,10 +1,16 @@ -from collections import namedtuple +import os +import re from Qt import QtWidgets, QtCore from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget -from pype.api import config +from pype.api import ( + config, + Creator +) +from pype.plugin import TaskNotSetError +from avalon.tools.creator.app import SubsetAllowedSymbols class FamilyWidget(QtWidgets.QWidget): @@ -122,6 +128,9 @@ def collect_data(self): } return data + def on_task_change(self): + self.on_data_changed() + def change_asset(self, name): if name is None: name = self.NOT_SELECTED @@ -167,65 +176,113 @@ def _on_action_clicked(self, action): def _on_data_changed(self): asset_name = self.asset_name - subset_name = self.input_subset.text() + user_input_text = self.input_subset.text() item = self.list_families.currentItem() if item is None: return - assets = None + asset_doc = None if asset_name != self.NOT_SELECTED: # Get the assets from the database which match with the name - assets_db = self.dbcon.find( - filter={"type": "asset"}, - projection={"name": 1} + asset_doc = self.dbcon.find_one( + { + "type": "asset", + "name": asset_name + }, + {"_id": 1} ) - assets = [ - asset for asset in assets_db if asset_name in asset["name"] - ] # Get plugin and family plugin = item.data(PluginRole) - if plugin is None: - return - family = plugin.family.rsplit(".", 1)[-1] + # Early exit if no asset name + if not asset_name.strip(): + self._build_menu([]) + item.setData(ExistsRole, False) + print("Asset name is required ..") + self.stateChanged.emit(False) + return - # Update the result - if subset_name: - subset_name = subset_name[0].upper() + subset_name[1:] - self.input_result.setText("{}{}".format(family, subset_name)) + # Get the asset from the database which match with the name + asset_doc = self.dbcon.find_one( + {"name": asset_name, "type": "asset"}, + projection={"_id": 1} + ) + # Get plugin + plugin = item.data(PluginRole) + if asset_doc and plugin: + project_name = self.dbcon.Session["AVALON_PROJECT"] + asset_id = asset_doc["_id"] + task_name = self.dbcon.Session["AVALON_TASK"] + + # Calculate subset name with Creator plugin + try: + subset_name = plugin.get_subset_name( + user_input_text, task_name, asset_id, project_name + ) + # Force replacement of prohibited symbols + # QUESTION should Creator care about this and here should be + # only validated with schema regex? + subset_name = re.sub( + "[^{}]+".format(SubsetAllowedSymbols), + "", + subset_name + ) + self.input_result.setText(subset_name) + + except TaskNotSetError: + subset_name = "" + self.input_result.setText("Select task please") - if assets: # Get all subsets of the current asset - asset_ids = [asset["_id"] for asset in assets] - subsets = self.dbcon.find(filter={"type": "subset", - "name": {"$regex": "{}*".format(family), - "$options": "i"}, - "parent": {"$in": asset_ids}}) or [] - - # Get all subsets' their subset name, "Default", "High", "Low" - existed_subsets = [sub["name"].split(family)[-1] - for sub in subsets] - - if plugin.defaults and isinstance(plugin.defaults, list): - defaults = plugin.defaults[:] + [self.Separator] - lowered = [d.lower() for d in plugin.defaults] - for sub in [s for s in existed_subsets - if s.lower() not in lowered]: - defaults.append(sub) - else: - defaults = existed_subsets - + subset_docs = self.dbcon.find( + { + "type": "subset", + "parent": asset_id + }, + {"name": 1} + ) + existing_subset_names = set(subset_docs.distinct("name")) + + # Defaults to dropdown + defaults = [] + # Check if Creator plugin has set defaults + if ( + plugin.defaults + and isinstance(plugin.defaults, (list, tuple, set)) + ): + defaults = list(plugin.defaults) + + # Replace + compare_regex = re.compile( + subset_name.replace(user_input_text, "(.+)") + ) + subset_hints = set() + if user_input_text: + for _name in existing_subset_names: + _result = compare_regex.search(_name) + if _result: + subset_hints |= set(_result.groups()) + + subset_hints = subset_hints - set(defaults) + if subset_hints: + if defaults: + defaults.append(self.Separator) + defaults.extend(subset_hints) self._build_menu(defaults) item.setData(ExistsRole, True) + else: + subset_name = user_input_text self._build_menu([]) item.setData(ExistsRole, False) - if asset_name != self.NOT_SELECTED: - # TODO add logging into standalone_publish - print("'%s' not found .." % asset_name) + + if not plugin: + print("No registered families ..") + else: + print("Asset '%s' not found .." % asset_name) self.on_version_refresh() @@ -248,30 +305,46 @@ def on_version_refresh(self): subset_name = self.input_result.text() version = 1 + asset_doc = None + subset_doc = None + versions = None if ( asset_name != self.NOT_SELECTED and subset_name.strip() != '' ): - asset = self.dbcon.find_one({ - 'type': 'asset', - 'name': asset_name - }) - subset = self.dbcon.find_one({ - 'type': 'subset', - 'parent': asset['_id'], - 'name': subset_name - }) - if subset: - versions = self.dbcon.find({ + asset_doc = self.dbcon.find_one( + { + 'type': 'asset', + 'name': asset_name + }, + {"_id": 1} + ) + + if asset_doc: + subset_doc = self.dbcon.find_one( + { + 'type': 'subset', + 'parent': asset_doc['_id'], + 'name': subset_name + }, + {"_id": 1} + ) + + if subset_doc: + versions = self.dbcon.find( + { 'type': 'version', - 'parent': subset['_id'] - }) - if versions: - versions = sorted( - [v for v in versions], - key=lambda ver: ver['name'] - ) - version = int(versions[-1]['name']) + 1 + 'parent': subset_doc['_id'] + }, + {"name": 1} + ).distinct("name") + + if versions: + versions = sorted( + [v for v in versions], + key=lambda ver: ver['name'] + ) + version = int(versions[-1]['name']) + 1 self.version_spinbox.setValue(version) @@ -284,7 +357,10 @@ def on_data_changed(self, *args): self.schedule(self._on_data_changed, 500, channel="gui") def on_selection_changed(self, *args): - plugin = self.list_families.currentItem().data(PluginRole) + item = self.list_families.currentItem() + if not item: + return + plugin = item.data(PluginRole) if plugin is None: return @@ -308,11 +384,20 @@ def keyPressEvent(self, event): """ def refresh(self): + self.list_families.clear() + has_families = False - presets = config.get_presets().get('standalone_publish', {}) + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if not project_name: + return - for key, creator in presets.get('families', {}).items(): - creator = namedtuple("Creator", creator.keys())(*creator.values()) + creators_data = ( + config.get_presets(project_name) + .get("standalone_publish", {}) + .get("families", {}) + ) + for key, creator_data in creators_data.items(): + creator = type(key, (Creator, ), creator_data) label = creator.label or creator.family item = QtWidgets.QListWidgetItem(label) From e27d04f3042c0ffc0a4e49b3070af071f4db2de5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 2 Mar 2021 09:36:58 +0000 Subject: [PATCH 007/264] Collect audio for farm reviews. --- pype/plugins/global/publish/collect_audio.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 pype/plugins/global/publish/collect_audio.py diff --git a/pype/plugins/global/publish/collect_audio.py b/pype/plugins/global/publish/collect_audio.py new file mode 100644 index 00000000000..4c7bda7a5aa --- /dev/null +++ b/pype/plugins/global/publish/collect_audio.py @@ -0,0 +1,47 @@ +import os + +import pyblish.api +from pype import api as pype_api +from avalon import api, io + + +class CollectAudio(pyblish.api.ContextPlugin): + """Finds asset audio based on plugin settings.""" + + order = pyblish.api.CollectorOrder + label = "Collect Audio" + + subset_name = "audioMain" + + def process(self, context): + version = pype_api.get_latest_version( + api.Session["AVALON_ASSET"], self.subset_name + ) + + if version is None: + self.log.warning( + "No audio version found on subset name: \"{}\"".format( + self.subset_name + ) + ) + return + + representation = io.find_one( + {"type": "representation", "parent": version["_id"], "name": "wav"} + ) + + if representation is None: + msg = ( + "No audio \"wav\" representation found on subset name: \"{}\"" + ) + self.log.warning(msg.format(self.subset_name)) + return + + path = api.get_representation_path(representation) + + if not os.path.exists(path): + self.log.warning("No file found at: \"{}\"".format(path)) + return + + self.log.info("Using audio file: \"{}\"".format(path)) + context.data["audioFile"] = path From 2efa640f6ac07d8208fc60996ca1d14c6e4a8618 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 10:44:47 +0100 Subject: [PATCH 008/264] fixed default data type of profiles --- pype/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugin.py b/pype/plugin.py index 1b0bed4bb26..ac76c1cbe8b 100644 --- a/pype/plugin.py +++ b/pype/plugin.py @@ -70,7 +70,7 @@ def get_subset_name( config.get_presets(project_name) .get("tools", {}) .get("creator_subset_name_profiles") - ) or {} + ) or [] filtering_criteria = { "families": family, "hosts": host_name, From c762e91e587facef55ee42435539e0a7724937b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 2 Mar 2021 13:28:57 +0100 Subject: [PATCH 009/264] removed unused import --- pype/tools/standalonepublish/widgets/widget_family.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index a42cc0b04f0..00bc5b31f9c 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -1,4 +1,3 @@ -import os import re from Qt import QtWidgets, QtCore From f84d68c0b62b49f5b94b62ab7602d49786caf420 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Mar 2021 19:56:36 +0100 Subject: [PATCH 010/264] PS - added support for .psb in workfiles Allows saving workfile as .psb, publish it as psb representation --- pype/plugins/photoshop/publish/collect_workfile.py | 5 +++-- pype/plugins/photoshop/publish/increment_workfile.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/photoshop/publish/collect_workfile.py b/pype/plugins/photoshop/publish/collect_workfile.py index 766be02354a..88817c39690 100644 --- a/pype/plugins/photoshop/publish/collect_workfile.py +++ b/pype/plugins/photoshop/publish/collect_workfile.py @@ -31,9 +31,10 @@ def process(self, context): }) # creating representation + _, ext = os.path.splitext(file_path) instance.data["representations"].append({ - "name": "psd", - "ext": "psd", + "name": ext[1:], + "ext": ext[1:], "files": base_name, "stagingDir": staging_dir, }) diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index eca2583595c..2005973ea0a 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -1,3 +1,4 @@ +import os import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up @@ -25,6 +26,7 @@ def process(self, instance): ) scene_path = version_up(instance.context.data["currentFile"]) - photoshop.stub().saveAs(scene_path, 'psd', True) + _, ext = os.path.splitext(scene_path) + photoshop.stub().saveAs(scene_path, ext[1:], True) self.log.info("Incremented workfile to: {}".format(scene_path)) From 9271101ed72761183f8ecc6bfc3e587ffd786c26 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:56:54 +0100 Subject: [PATCH 011/264] removed modifiable save modes from extractor --- .../tvpaint/publish/extract_sequence.py | 53 ++----------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index aa625a497a1..8fbf195fde1 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -45,13 +45,6 @@ class ExtractSequence(pyblish.api.Extractor): "tga" } - default_save_mode = "\"PNG\"" - save_mode_for_family = { - "review": "\"PNG\"", - "renderPass": "\"PNG\"", - "renderLayer": "\"PNG\"", - } - def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) @@ -80,34 +73,15 @@ def process(self, instance): len(layer_names), joined_layer_names ) ) - # This is plugin attribe cleanup method - self._prepare_save_modes() family_lowered = instance.data["family"].lower() - save_mode = self.save_mode_for_family.get( - family_lowered, self.default_save_mode - ) - save_mode_type = self._get_save_mode_type(save_mode) - - if not bool(save_mode_type in self.sequential_save_mode): - raise AssertionError(( - "Plugin can export only sequential frame output" - " but save mode for family \"{}\" is not for sequence > {} <" - ).format(instance.data["family"], save_mode)) - frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - filename_template = self._get_filename_template( - save_mode_type, save_mode, frame_end - ) + filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") - self.log.debug( - "Using save mode > {} < and file template \"{}\"".format( - save_mode, filename_template - ) - ) + self.log.debug("Using file template \"{}\"".format(filename_template)) # Save to staging dir output_dir = instance.data.get("stagingDir") @@ -186,19 +160,6 @@ def process(self, instance): } instance.data["representations"].append(thumbnail_repre) - def _prepare_save_modes(self): - """Lower family names in keys and skip empty values.""" - new_specifications = {} - for key, value in self.save_mode_for_family.items(): - if value: - new_specifications[key.lower()] = value - else: - self.log.warning(( - "Save mode for family \"{}\" has empty value." - " The family will use default save mode: > {} <." - ).format(key, self.default_save_mode)) - self.save_mode_for_family = new_specifications - def _get_save_mode_type(self, save_mode): """Extract type of save mode. @@ -212,7 +173,7 @@ def _get_save_mode_type(self, save_mode): self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) return save_mode_type - def _get_filename_template(self, save_mode_type, save_mode, frame_end): + def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. This is simple template contains `{frame}{ext}` for sequential outputs @@ -220,18 +181,12 @@ def _get_filename_template(self, save_mode_type, save_mode, frame_end): temporary folder so filename should not matter as integrator change them. """ - ext = self.save_mode_to_ext.get(save_mode_type) - if ext is None: - raise AssertionError(( - "Couldn't find file extension for TVPaint's save mode: > {} <" - ).format(save_mode)) - frame_padding = 4 frame_end_str_len = len(str(frame_end)) if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{frame:0>{}}}".format(frame_padding) + ext + return "{{:0>{}}}".format(frame_padding) + ".png" def render( self, save_mode, filename_template, output_dir, layers, From f85edd36ca0ef7e1337e742ad0d2106d09aad014 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:57:50 +0100 Subject: [PATCH 012/264] imlpemented method for copying same temp image files --- pype/plugins/tvpaint/publish/extract_sequence.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 8fbf195fde1..9755bb28502 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -317,3 +317,10 @@ def fill_missing_frames( ) filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From bed900fc49640b151e03a208fb7e41fc54f413cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 09:59:35 +0100 Subject: [PATCH 013/264] implemented method that will fill frames by pre behavior of a layer --- .../tvpaint/publish/extract_sequence.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 9755bb28502..87d5dfc9cc5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -318,6 +318,64 @@ def fill_missing_frames( filepaths_by_frame[frame] = space_filepath shutil.copy(previous_frame_filepath, space_filepath) + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 72a6e1d9c67040e8cb7e6971906f3a5b0373c602 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:00:17 +0100 Subject: [PATCH 014/264] implemented method that will fill frames by post behavior of a layer --- .../tvpaint/publish/extract_sequence.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 87d5dfc9cc5..0f1ce8691ac 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -376,6 +376,61 @@ def _fill_frame_by_pre_behavior( self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 5ece25b123bc0a4216ac0e2ba17df15be3f592b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:01:26 +0100 Subject: [PATCH 015/264] implemented logic of layers compositing using Pillow --- .../tvpaint/publish/extract_sequence.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 0f1ce8691ac..449da3c1e0d 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -4,6 +4,7 @@ import pyblish.api from avalon.tvpaint import lib +from PIL import Image class ExtractSequence(pyblish.api.Extractor): @@ -431,6 +432,51 @@ def _fill_frame_by_post_behavior( self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + img_obj = None + for image_filepath in image_filepaths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + continue + + img_obj.alpha_composite(_img_obj) + img_obj.save(output_filepath) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From 796e7224e1238798fce181a6f30a8f1bf03178c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:02:47 +0100 Subject: [PATCH 016/264] frame start/end are defined by mark in/out of published clip --- .../tvpaint/publish/collect_workfile_data.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index bd2e5745189..f25e2745814 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -113,7 +113,7 @@ def process(self, context): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - frame_start = int(workfile_info_parts.pop(-1)) + _frame_start = int(workfile_info_parts.pop(-1)) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -121,21 +121,14 @@ def process(self, context): width = int(workfile_info_parts.pop(-1)) workfile_path = " ".join(workfile_info_parts).replace("\"", "") - # TODO This is not porper way of getting last frame - # - but don't know better - last_frame = frame_start - for layer in layers_data: - frame_end = layer["frame_end"] - if frame_end > last_frame: - last_frame = frame_end - + frame_start, frame_end = self.collect_clip_frames() scene_data = { "currentFile": workfile_path, "sceneWidth": width, "sceneHeight": height, "pixelAspect": pixel_apsect, "frameStart": frame_start, - "frameEnd": last_frame, + "frameEnd": frame_end, "fps": frame_rate, "fieldOrder": field_order } @@ -143,3 +136,19 @@ def process(self, context): "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def collect_clip_frames(self): + clip_info_str = lib.execute_george("tv_clipinfo") + self.log.debug("Clip info: {}".format(clip_info_str)) + clip_info_items = clip_info_str.split(" ") + # Color index + color_idx = clip_info_items.pop(-1) + clip_info_items.pop(-1) + + mark_out = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + mark_in = int(clip_info_items.pop(-1)) + 1 + clip_info_items.pop(-1) + + return mark_in, mark_out From 9b78deb2ebd412fbc6395d7e3b032ddcb9cd6b19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:28 +0100 Subject: [PATCH 017/264] collect both layer's position and all layer ids --- pype/plugins/tvpaint/publish/extract_sequence.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 449da3c1e0d..576d294c425 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -214,10 +214,13 @@ def render( save_mode = "tv_SaveMode {}".format(save_mode) # Map layers by position - layers_by_position = { - layer["position"]: layer - for layer in layers - } + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) # Sort layer positions in reverse order sorted_positions = list(reversed(sorted(layers_by_position.keys()))) From df1435e80e97b191b9bf16e7ae0afbd31b59a8df Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:04:55 +0100 Subject: [PATCH 018/264] skip savemode filling --- pype/plugins/tvpaint/publish/extract_sequence.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 576d294c425..6efa22d1cdc 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -190,8 +190,8 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" def render( - self, save_mode, filename_template, output_dir, layers, - first_frame, last_frame, thumbnail_filename + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename ): """ Export images from TVPaint. @@ -210,9 +210,6 @@ def render( dict: Mapping frame to output filepath. """ - # Add save mode arguments to function - save_mode = "tv_SaveMode {}".format(save_mode) - # Map layers by position layers_by_position = {} layer_ids = [] From cf6d649cd1fe92a30b79f6a0ea386375398923fb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:00 +0100 Subject: [PATCH 019/264] removed previous logic of rendering --- .../tvpaint/publish/extract_sequence.py | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 6efa22d1cdc..0fe4018157a 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -102,17 +102,6 @@ def process(self, instance): save_mode, filename_template, output_dir, filtered_layers, frame_start, frame_end, thumbnail_filename ) - thumbnail_fullpath = output_files_by_frame.pop( - thumbnail_filename, None - ) - - # Fill gaps in sequence - self.fill_missing_frames( - output_files_by_frame, - frame_start, - frame_end, - filename_template - ) # Fill tags and new families tags = [] @@ -224,100 +213,9 @@ def render( if not sorted_positions: return - # Create temporary layer - new_layer_id = lib.execute_george("tv_layercreate _tmp_layer") - # Merge layers to temp layer - george_script_lines = [] - # Set duplicated layer as current - george_script_lines.append("tv_layerset {}".format(new_layer_id)) for position in sorted_positions: layer = layers_by_position[position] - george_script_lines.append( - "tv_layermerge {}".format(layer["layer_id"]) - ) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Frames with keyframe - exposure_frames = lib.get_exposure_frames( - new_layer_id, first_frame, last_frame - ) - - # TODO what if there is not exposue frames? - # - this force to have first frame all the time - if first_frame not in exposure_frames: - exposure_frames.insert(0, first_frame) - - # Restart george script lines - george_script_lines = [] - george_script_lines.append(save_mode) - - all_output_files = {} - for frame in exposure_frames: - filename = filename_template.format(frame, frame=frame) - dst_path = "/".join([output_dir, filename]) - all_output_files[frame] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - # Export thumbnail - if thumbnail_filename: - basename, ext = os.path.splitext(thumbnail_filename) - if not ext: - ext = ".jpg" - thumbnail_fullpath = "/".join([output_dir, basename + ext]) - all_output_files[thumbnail_filename] = thumbnail_fullpath - # Force save mode to png for thumbnail - george_script_lines.append("tv_SaveMode \"JPG\"") - # Go to frame - george_script_lines.append("tv_layerImage {}".format(first_frame)) - # Store image to output - george_script_lines.append( - "tv_saveimage \"{}\"".format(thumbnail_fullpath) - ) - - # Delete temporary layer - george_script_lines.append("tv_layerkill {}".format(new_layer_id)) - - lib.execute_george_through_file("\n".join(george_script_lines)) - - return all_output_files - - def fill_missing_frames( - self, filepaths_by_frame, first_frame, last_frame, filename_template - ): - """Fill not rendered frames with previous frame. - - Extractor is rendering only frames with keyframes (exposure frames) to - get output faster which means there may be gaps between frames. - This function fill the missing frames. - """ - output_dir = None - previous_frame_filepath = None - for frame in range(first_frame, last_frame + 1): - if frame in filepaths_by_frame: - previous_frame_filepath = filepaths_by_frame[frame] - continue - - elif previous_frame_filepath is None: - self.log.warning( - "No frames to fill. Seems like nothing was exported." - ) - break - - if output_dir is None: - output_dir = os.path.dirname(previous_frame_filepath) - - filename = filename_template.format(frame=frame) - space_filepath = os.path.normpath( - os.path.join(output_dir, filename) - ) - filepaths_by_frame[frame] = space_filepath - shutil.copy(previous_frame_filepath, space_filepath) def _fill_frame_by_pre_behavior( self, From cc9369ef8d8553552fabebdcd2b4061e1299db65 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:08:38 +0100 Subject: [PATCH 020/264] collect behavior of layer ids to process --- pype/plugins/tvpaint/publish/extract_sequence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 0fe4018157a..64bd023f5f9 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -213,6 +213,7 @@ def render( if not sorted_positions: return + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) for position in sorted_positions: layer = layers_by_position[position] From 838ebbeb060f84fafd408f19178b104e3878c107 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:09:32 +0100 Subject: [PATCH 021/264] implemented method that will render and fill all frames of given layer --- .../tvpaint/publish/extract_sequence.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 64bd023f5f9..852ec011838 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -218,6 +218,80 @@ def render( for position in sorted_positions: layer = layers_by_position[position] + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + def _fill_frame_by_pre_behavior( self, layer, From df4e28153549d00d553053f8ced31059fd122881 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:13 +0100 Subject: [PATCH 022/264] layers are rendered one by one and stored by their position (order) --- .../plugins/tvpaint/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 852ec011838..e1667997fb5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -215,8 +215,25 @@ def render( behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} for position in sorted_positions: layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + def render_layer( self, From d14e584a8d9d5bd516c8bd1399c42e167757567f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:10:49 +0100 Subject: [PATCH 023/264] rendered frames are composite to final output --- pype/plugins/tvpaint/publish/extract_sequence.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index e1667997fb5..919dd02f7c5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -234,6 +234,15 @@ def render( ) files_by_position[position] = files_by_frames + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename + ) + return output def render_layer( self, From d8033fc6cabcba26e25e3ed4a1470d42f007927f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:11:14 +0100 Subject: [PATCH 024/264] added cleanup method that will remove temp image files of individial layers --- pype/plugins/tvpaint/publish/extract_sequence.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 919dd02f7c5..f1929707b43 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -242,6 +242,7 @@ def render( filename_template, thumbnail_filename ) + self._cleanup_tmp_files(files_by_position) return output def render_layer( @@ -476,6 +477,11 @@ def _composite_files( return output_filepaths, thumbnail_filepath + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + def _copy_image(self, src_path, dst_path): # Create hardlink of image instead of copying if possible if hasattr(os, "link"): From f57ecfa2e705ba27162dfcab954056805a2ba487 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:03 +0100 Subject: [PATCH 025/264] pass different arguments and expect different output of render method --- pype/plugins/tvpaint/publish/extract_sequence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index f1929707b43..f8aeace6177 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -95,12 +95,12 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail" + thumbnail_filename = "thumbnail.jpg" # Render output - output_files_by_frame = self.render( - save_mode, filename_template, output_dir, - filtered_layers, frame_start, frame_end, thumbnail_filename + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename ) # Fill tags and new families @@ -110,7 +110,7 @@ def process(self, instance): repre_files = [ os.path.basename(filepath) - for filepath in output_files_by_frame.values() + for filepath in output_filepaths ] # Sequence of one frame if len(repre_files) == 1: From b8c57e0057a3eabb78618afc492e06adb9f7e111 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:12:17 +0100 Subject: [PATCH 026/264] keep frame start/end as they are --- pype/plugins/tvpaint/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index f8aeace6177..821b212f839 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -121,8 +121,8 @@ def process(self, instance): "ext": ext, "files": repre_files, "stagingDir": output_dir, - "frameStart": frame_start + 1, - "frameEnd": frame_end + 1, + "frameStart": frame_start, + "frameEnd": frame_end, "tags": tags } self.log.debug("Creating new representation: {}".format(new_repre)) From c452dc953f2e0cc37defba957a912334f9e8fb36 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 10:26:43 +0100 Subject: [PATCH 027/264] added some extra logs --- pype/plugins/tvpaint/publish/extract_sequence.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 821b212f839..729eb5a9488 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -61,7 +61,7 @@ def process(self, instance): layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( - f"None of the layers from the instance" + "None of the layers from the instance" " are visible. Extraction skipped." ) return @@ -198,6 +198,7 @@ def render( Retruns: dict: Mapping frame to output filepath. """ + self.log.debug("Preparing data for rendering.") # Map layers by position layers_by_position = {} @@ -213,6 +214,7 @@ def render( if not sorted_positions: return + self.log.debug("Collecting pre/post behavior of individual layers.") behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) mark_in_index = frame_start - 1 @@ -279,10 +281,17 @@ def render_layer( # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -300,6 +309,11 @@ def render_layer( # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + # Pre behavior self._fill_frame_by_pre_behavior( layer, From 043a9d9e038372e44a34b6ad7c8c631990834798 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 11:54:26 +0100 Subject: [PATCH 028/264] fixed case when all layers miss image for frame --- .../tvpaint/publish/extract_sequence.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 729eb5a9488..847292814d2 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -7,6 +7,22 @@ from PIL import Image +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) + + class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] @@ -78,6 +94,8 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,7 +118,8 @@ def process(self, instance): # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ) # Fill tags and new families @@ -180,7 +199,8 @@ def _get_filename_template(self, frame_end): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height ): """ Export images from TVPaint. @@ -242,7 +262,9 @@ def render( mark_in_index, mark_out_index, filename_template, - thumbnail_filename + thumbnail_filename, + scene_width, + scene_height ) self._cleanup_tmp_files(files_by_position) return output @@ -448,7 +470,7 @@ def _fill_frame_by_post_behavior( def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename + filename_template, thumbnail_filename, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -465,22 +487,18 @@ def _composite_files( for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 + output_filename = filename_template.format(frame) output_filepath = os.path.join(output_dir, output_filename) - img_obj = None - for image_filepath in image_filepaths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - continue - - img_obj.alpha_composite(_img_obj) - img_obj.save(output_filepath) output_filepaths.append(output_filepath) if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath + composite_images( + image_filepaths, output_filepath, scene_width, scene_height + ) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From 84f38013c972f573e924623bf5e7caf6bd2b5eaa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:05:26 +0100 Subject: [PATCH 029/264] moved composite_images to pype's tvpaint.lib --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ .../plugins/tvpaint/publish/extract_sequence.py | 17 +---------------- 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 00000000000..8172392c7f2 --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 847292814d2..d33ec3c68cb 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -4,25 +4,10 @@ import pyblish.api from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) - - class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] From 3193ade4f93893a2e847c163a21ed07d565c6268 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 12:10:38 +0100 Subject: [PATCH 030/264] using multiprocessing to speed up compositing part --- .../tvpaint/publish/extract_sequence.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index d33ec3c68cb..e43fb06f7a5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -1,6 +1,8 @@ import os import shutil +import time import tempfile +import multiprocessing import pyblish.api from avalon.tvpaint import lib @@ -467,6 +469,11 @@ def _composite_files( if frame_idx in position_data: images_by_frame[frame_idx].append(position_data[frame_idx]) + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} output_filepaths = [] thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): @@ -480,10 +487,35 @@ def _composite_files( if thumbnail_filename and thumbnail_src_filepath is None: thumbnail_src_filepath = output_filepath - composite_images( - image_filepaths, output_filepath, scene_width, scene_height + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) ) + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + thumbnail_filepath = None if thumbnail_src_filepath: source_img = Image.open(thumbnail_src_filepath) From 554e0f57b03bbfc0847bbc2a6efd4b306d3e29ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:35:43 +0100 Subject: [PATCH 031/264] simplified extractor with tv_savesequence command --- .../tvpaint/publish/extract_sequence.py | 429 +++--------------- 1 file changed, 65 insertions(+), 364 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index e43fb06f7a5..17d8dc60f47 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -1,12 +1,8 @@ import os -import shutil -import time import tempfile -import multiprocessing import pyblish.api from avalon.tvpaint import lib -from pype.hosts.tvpaint.lib import composite_images from PIL import Image @@ -61,6 +57,10 @@ def process(self, instance): for layer in layers if layer["visible"] ] + filtered_layer_ids = [ + layer["layer_id"] + for layer in filtered_layers + ] layer_names = [str(layer["name"]) for layer in filtered_layers] if not layer_names: self.log.info( @@ -81,8 +81,6 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -100,24 +98,53 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" + first_frame_filename = filename_template.format(frame_start) + first_frame_filepath = os.path.join(output_dir, first_frame_filename) + + # Store layers visibility + layer_visibility_by_id = {} + for layer in instance.context.data["layersData"]: + layer_id = layer["layer_id"] + layer_visibility_by_id[layer_id] = layer["visible"] + + george_script_lines = [] + for layer_id in layer_visibility_by_id.keys(): + visible = layer_id in filtered_layer_ids + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + repre_files = self.render( + filename_template, + output_dir, + frame_start, + frame_end ) + # Restore visibility + george_script_lines = [] + for layer_id, visible in layer_visibility_by_id.items(): + value = "on" if visible else "off" + george_script_lines.append( + "tv_layerdisplay {} \"{}\"".format(layer_id, value) + ) + lib.execute_george_through_file("\n".join(george_script_lines)) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if os.path.exists(first_frame_filepath): + 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) + # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -139,36 +166,23 @@ def process(self, instance): # Change family to render instance.data["family"] = "render" - if not thumbnail_fullpath: + if not os.path.exists(thumbnail_filepath): return thumbnail_ext = os.path.splitext( - thumbnail_fullpath + thumbnail_filepath )[1].replace(".", "") # Create thumbnail representation thumbnail_repre = { "name": "thumbnail", "ext": thumbnail_ext, "outputName": "thumb", - "files": os.path.basename(thumbnail_fullpath), + "files": os.path.basename(thumbnail_filepath), "stagingDir": output_dir, "tags": ["thumbnail"] } instance.data["representations"].append(thumbnail_repre) - def _get_save_mode_type(self, save_mode): - """Extract type of save mode. - - Helps to define output files extension. - """ - save_mode_type = ( - save_mode.lower() - .split(" ")[0] - .replace("\"", "") - ) - self.log.debug("Save mode type is \"{}\"".format(save_mode_type)) - return save_mode_type - def _get_filename_template(self, frame_end): """Get filetemplate for rendered files. @@ -184,356 +198,43 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" - def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height - ): + def render(self, filename_template, output_dir, frame_start, frame_end): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. - first_frame (int): Starting frame from which export will begin. - last_frame (int): On which frame export will end. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. Retruns: dict: Mapping frame to output filepath. """ self.log.debug("Preparing data for rendering.") - - # Map layers by position - layers_by_position = {} - layer_ids = [] - for layer in layers: - position = layer["position"] - layers_by_position[position] = layer - - layer_ids.append(layer["layer_id"]) - - # Sort layer positions in reverse order - sorted_positions = list(reversed(sorted(layers_by_position.keys()))) - if not sorted_positions: - return - - self.log.debug("Collecting pre/post behavior of individual layers.") - behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) - - mark_in_index = frame_start - 1 - mark_out_index = frame_end - 1 - - tmp_filename_template = "pos_{}." + filename_template - - files_by_position = {} - for position in sorted_positions: - layer = layers_by_position[position] - behavior = behavior_by_layer_id[layer["layer_id"]] - files_by_frames = self.render_layer( - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ) - files_by_position[position] = files_by_frames - - output = self._composite_files( - files_by_position, + first_frame_filepath = os.path.join( output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) - return output - - def render_layer( - self, - layer, - tmp_filename_template, - output_dir, - behavior, - mark_in_index, - mark_out_index - ): - layer_id = layer["layer_id"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - exposure_frames = lib.get_exposure_frames( - layer_id, frame_start_index, frame_end_index + filename_template.format(frame_start, frame=frame_start) ) - if frame_start_index not in exposure_frames: - exposure_frames.append(frame_start_index) + mark_in = frame_start - 1 + mark_out = frame_end - 1 - layer_files_by_frame = {} george_script_lines = [ - "tv_SaveMode \"PNG\"" + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) ] - layer_position = layer["position"] - - for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) - dst_path = "/".join([output_dir, filename]) - layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) - - # Go to frame - george_script_lines.append("tv_layerImage {}".format(frame_idx)) - # Store image to output - george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id - )) - # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) - # Fill frames between `frame_start_index` and `frame_end_index` - self.log.debug(( - "Filling frames between first and last frame of layer ({} - {})." - ).format(frame_start_index + 1, frame_end_index + 1)) - - prev_filepath = None - for frame_idx in range(frame_start_index, frame_end_index + 1): - if frame_idx in layer_files_by_frame: - prev_filepath = layer_files_by_frame[frame_idx] - continue - - if prev_filepath is None: - raise ValueError("BUG: First frame of layer was not rendered!") - - filename = tmp_filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(prev_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - # Fill frames by pre/post behavior of layer - pre_behavior = behavior["pre"] - post_behavior = behavior["post"] - self.log.debug(( - "Completing image sequence of layer by pre/post behavior." - " PRE: {} | POST: {}" - ).format(pre_behavior, post_behavior)) - - # Pre behavior - self._fill_frame_by_pre_behavior( - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - self._fill_frame_by_post_behavior( - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - tmp_filename_template, - output_dir - ) - return layer_files_by_frame - - def _fill_frame_by_pre_behavior( - self, - layer, - pre_behavior, - mark_in_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_in_index >= frame_start_index: - return - - if pre_behavior == "none": - return - - if pre_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_start_index] - for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = ( - (frame_end_index - frame_idx) % frame_count - ) - eq_frame_idx = frame_end_index - eq_frame_idx_offset - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif pre_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) - eq_frame_idx = frame_start_index + eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _fill_frame_by_post_behavior( - self, - layer, - post_behavior, - mark_out_index, - layer_files_by_frame, - filename_template, - output_dir - ): - layer_position = layer["position"] - frame_start_index = layer["frame_start"] - frame_end_index = layer["frame_end"] - frame_count = frame_end_index - frame_start_index + 1 - if mark_out_index <= frame_end_index: - return - - if post_behavior == "none": - return - - if post_behavior == "hold": - # Keep first frame for whole time - eq_frame_filepath = layer_files_by_frame[frame_end_index] - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "loop": - # Loop backwards from last frame of layer - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx = frame_idx % frame_count - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - elif post_behavior == "pingpong": - half_seq_len = frame_count - 1 - seq_len = half_seq_len * 2 - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len - if eq_frame_idx_offset > half_seq_len: - eq_frame_idx_offset = seq_len - eq_frame_idx_offset - eq_frame_idx = frame_end_index - eq_frame_idx_offset - - eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - - filename = filename_template.format(layer_position, frame_idx) - new_filepath = "/".join([output_dir, filename]) - self._copy_image(eq_frame_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath - - def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height - ): - # Prepare paths to images by frames into list where are stored - # in order of compositing. - images_by_frame = {} - for frame_idx in range(frame_start, frame_end + 1): - images_by_frame[frame_idx] = [] - for position in sorted(files_by_position.keys(), reverse=True): - position_data = files_by_position[position] - if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) - - process_count = os.cpu_count() - if process_count > 1: - process_count -= 1 - - processes = {} - output_filepaths = [] - thumbnail_src_filepath = None - for frame_idx in sorted(images_by_frame.keys()): - image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame) - output_filepath = os.path.join(output_dir, output_filename) - output_filepaths.append(output_filepath) - - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height - ) + output = [] + for frame in range(frame_start, frame_end + 1): + output.append( + filename_template.format(frame, frame=frame) ) - - # Wait until all processes are done - running_processes = {} - while True: - for idx in tuple(running_processes.keys()): - process = running_processes[idx] - if not process.is_alive(): - running_processes.pop(idx).join() - - if processes and len(running_processes) != process_count: - indexes = list(processes.keys()) - for _ in range(process_count - len(running_processes)): - if not indexes: - break - idx = indexes.pop(0) - running_processes[idx] = processes.pop(idx) - running_processes[idx].start() - - if not running_processes and not processes: - break - - time.sleep(0.01) - - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath - - def _cleanup_tmp_files(self, files_by_position): - for data in files_by_position.values(): - for filepath in data.values(): - os.remove(filepath) - - def _copy_image(self, src_path, dst_path): - # Create hardlink of image instead of copying if possible - if hasattr(os, "link"): - os.link(src_path, dst_path) - else: - shutil.copy(src_path, dst_path) + return output From bda4296a86e61bf12fdb70f1bcdd8e67d7e83192 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:42:21 +0100 Subject: [PATCH 032/264] removed unused lib --- pype/hosts/tvpaint/lib.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py deleted file mode 100644 index 8172392c7f2..00000000000 --- a/pype/hosts/tvpaint/lib.py +++ /dev/null @@ -1,17 +0,0 @@ -from PIL import Image - - -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): - img_obj = None - for image_filepath in input_image_paths: - _img_obj = Image.open(image_filepath) - if img_obj is None: - img_obj = _img_obj - else: - img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) - img_obj.save(output_filepath) From d6a93414cd97e24bc987b72baca1a118a6e3fca4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:50:22 +0100 Subject: [PATCH 033/264] fixed hound --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index f25e2745814..bb25e244ef7 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -113,7 +113,8 @@ def process(self, context): self.log.info("Collecting scene data from workfile") workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") - _frame_start = int(workfile_info_parts.pop(-1)) + # Project frame start - not used + workfile_info_parts.pop(-1) field_order = workfile_info_parts.pop(-1) frame_rate = float(workfile_info_parts.pop(-1)) pixel_apsect = float(workfile_info_parts.pop(-1)) @@ -141,8 +142,8 @@ def collect_clip_frames(self): clip_info_str = lib.execute_george("tv_clipinfo") self.log.debug("Clip info: {}".format(clip_info_str)) clip_info_items = clip_info_str.split(" ") - # Color index - color_idx = clip_info_items.pop(-1) + # Color index - not used + clip_info_items.pop(-1) clip_info_items.pop(-1) mark_out = int(clip_info_items.pop(-1)) + 1 From df5916e43a597c2e5ef66f0cf8d434075709d5a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 13:57:02 +0100 Subject: [PATCH 034/264] fix variable names --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index bb25e244ef7..7965112136c 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -146,10 +146,12 @@ def collect_clip_frames(self): clip_info_items.pop(-1) clip_info_items.pop(-1) - mark_out = int(clip_info_items.pop(-1)) + 1 + mark_out = int(clip_info_items.pop(-1)) + frame_end = mark_out + 1 clip_info_items.pop(-1) - mark_in = int(clip_info_items.pop(-1)) + 1 + mark_in = int(clip_info_items.pop(-1)) + frame_start = mark_in + 1 clip_info_items.pop(-1) - return mark_in, mark_out + return frame_start, frame_end From f9da2deb633055f1b622462b09fb79fd7dedbbae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 14:31:58 +0100 Subject: [PATCH 035/264] do not query subset documents if subset filters are empty --- .../global/publish/collect_anatomy_instance_data.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/collect_anatomy_instance_data.py b/pype/plugins/global/publish/collect_anatomy_instance_data.py index 99fbe8a52ee..4fd657167cf 100644 --- a/pype/plugins/global/publish/collect_anatomy_instance_data.py +++ b/pype/plugins/global/publish/collect_anatomy_instance_data.py @@ -149,10 +149,12 @@ def fill_latest_versions(self, context): "name": subset_name }) - subset_docs = list(io.find({ - "type": "subset", - "$or": subset_filters - })) + subset_docs = [] + if subset_filters: + subset_docs = list(io.find({ + "type": "subset", + "$or": subset_filters + })) subset_ids = [ subset_doc["_id"] From 5d2879f3cce8da4bd153d61dfa4aead34f53aa3d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 15:07:46 +0100 Subject: [PATCH 036/264] kept only one integrate hierarchy ftrack plugin --- .../publish/integrate_hierarchy_ftrack.py | 46 ++- .../publish/integrate_hierarchy_ftrack_SP.py | 331 ------------------ 2 files changed, 38 insertions(+), 339 deletions(-) delete mode 100644 pype/plugins/ftrack/publish/integrate_hierarchy_ftrack_SP.py diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index a1377cc7717..99e84a1856f 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -36,7 +36,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["hiero"] + hosts = ["hiero", "standalonepublisher"] optional = False def process(self, context): @@ -50,8 +50,7 @@ def process(self, context): project_name = self.context.data["projectEntity"]["name"] query = 'Project where full_name is "{}"'.format(project_name) project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] + auto_sync_state = project["custom_attributes"][CUST_ATTR_AUTO_SYNC] if not io.Session: io.install() @@ -74,6 +73,15 @@ def process(self, context): self.auto_sync_on(project) def import_to_ftrack(self, input_data, parent=None): + # Prequery hiearchical custom attributes + hier_custom_attributes = get_pype_attr(self.session)[1] + hier_attr_by_key = { + attr["key"]: attr + for attr in hier_custom_attributes + } + # Get ftrack api module (as they are different per python version) + ftrack_api = self.context.data["ftrackPythonModule"] + for entity_name in input_data: entity_data = input_data[entity_name] entity_type = entity_data['entity_type'] @@ -116,12 +124,34 @@ def import_to_ftrack(self, input_data, parent=None): i for i in self.context if i.data['asset'] in entity['name'] ] for key in custom_attributes: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) - ) + hier_attr = hier_attr_by_key.get(key) + # Use simple method if key is not hierarchical + if not hier_attr: + assert (key in entity['custom_attributes']), ( + 'Missing custom attribute key: `{0}` in attrs: ' + '`{1}`'.format(key, entity['custom_attributes'].keys()) + ) - entity['custom_attributes'][key] = custom_attributes[key] + entity['custom_attributes'][key] = custom_attributes[key] + + else: + # Use ftrack operations method to set hiearchical + # attribute value. + # - this is because there may be non hiearchical custom + # attributes with different properties + entity_key = collections.OrderedDict({ + "configuration_id": hier_attr["id"], + "entity_id": entity["id"] + }) + self.session.recorded_operations.push( + ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + custom_attributes[key] + ) + ) for instance in instances: instance.data['ftrackEntity'] = entity diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack_SP.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack_SP.py deleted file mode 100644 index ac606ed27dc..00000000000 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack_SP.py +++ /dev/null @@ -1,331 +0,0 @@ -import sys -import six -import collections -import pyblish.api -from avalon import io - -from pype.modules.ftrack.lib.avalon_sync import ( - CUST_ATTR_AUTO_SYNC, - get_pype_attr -) - - -class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): - """ - Create entities in ftrack based on collected data from premiere - Example of entry data: - { - "ProjectXS": { - "entity_type": "Project", - "custom_attributes": { - "fps": 24,... - }, - "tasks": [ - "Compositing", - "Lighting",... *task must exist as task type in project schema* - ], - "childs": { - "sq01": { - "entity_type": "Sequence", - ... - } - } - } - } - """ - - order = pyblish.api.IntegratorOrder - 0.04 - label = 'Integrate Hierarchy To Ftrack' - families = ["shot"] - hosts = ["standalonepublisher"] - optional = False - - def process(self, context): - self.context = context - if "hierarchyContext" not in self.context.data: - return - - hierarchy_context = self.context.data["hierarchyContext"] - - self.session = self.context.data["ftrackSession"] - project_name = self.context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] - - if not io.Session: - io.install() - - self.ft_project = None - - input_data = hierarchy_context - - # disable termporarily ftrack project's autosyncing - if auto_sync_state: - self.auto_sync_off(project) - - try: - # import ftrack hierarchy - self.import_to_ftrack(input_data) - except Exception: - raise - finally: - if auto_sync_state: - self.auto_sync_on(project) - - def import_to_ftrack(self, input_data, parent=None): - # Prequery hiearchical custom attributes - hier_custom_attributes = get_pype_attr(self.session)[1] - hier_attr_by_key = { - attr["key"]: attr - for attr in hier_custom_attributes - } - # Get ftrack api module (as they are different per python version) - ftrack_api = self.context.data["ftrackPythonModule"] - - for entity_name in input_data: - entity_data = input_data[entity_name] - entity_type = entity_data['entity_type'] - self.log.debug(entity_data) - self.log.debug(entity_type) - - if entity_type.lower() == 'project': - query = 'Project where full_name is "{}"'.format(entity_name) - entity = self.session.query(query).one() - self.ft_project = entity - self.task_types = self.get_all_task_types(entity) - - elif self.ft_project is None or parent is None: - raise AssertionError( - "Collected items are not in right order!" - ) - - # try to find if entity already exists - else: - query = ( - 'TypedContext where name is "{0}" and ' - 'project_id is "{1}"' - ).format(entity_name, self.ft_project["id"]) - try: - entity = self.session.query(query).one() - except Exception: - entity = None - - # Create entity if not exists - if entity is None: - entity = self.create_entity( - name=entity_name, - type=entity_type, - parent=parent - ) - # self.log.info('entity: {}'.format(dict(entity))) - # CUSTOM ATTRIBUTES - custom_attributes = entity_data.get('custom_attributes', []) - instances = [ - i for i in self.context if i.data['asset'] in entity['name'] - ] - for key in custom_attributes: - hier_attr = hier_attr_by_key.get(key) - # Use simple method if key is not hierarchical - if not hier_attr: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) - ) - - entity['custom_attributes'][key] = custom_attributes[key] - - else: - # Use ftrack operations method to set hiearchical - # attribute value. - # - this is because there may be non hiearchical custom - # attributes with different properties - entity_key = collections.OrderedDict({ - "configuration_id": hier_attr["id"], - "entity_id": entity["id"] - }) - self.session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - custom_attributes[key] - ) - ) - - for instance in instances: - instance.data['ftrackEntity'] = entity - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # TASKS - tasks = entity_data.get('tasks', []) - existing_tasks = [] - tasks_to_create = [] - for child in entity['children']: - if child.entity_type.lower() == 'task': - existing_tasks.append(child['name'].lower()) - # existing_tasks.append(child['type']['name']) - - for task_name in tasks: - task_type = tasks[task_name]["type"] - if task_name.lower() in existing_tasks: - print("Task {} already exists".format(task_name)) - continue - tasks_to_create.append((task_name, task_type)) - - for task_name, task_type in tasks_to_create: - self.create_task( - name=task_name, - task_type=task_type, - parent=entity - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Incoming links. - self.create_links(entity_data, entity) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Create notes. - user = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).first() - if user: - for comment in entity_data.get("comments", []): - entity.create_note(comment, user) - else: - self.log.warning( - "Was not able to query current User {}".format( - self.session.api_user - ) - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Import children. - if 'childs' in entity_data: - self.import_to_ftrack( - entity_data['childs'], entity) - - def create_links(self, entity_data, entity): - # Clear existing links. - for link in entity.get("incoming_links", []): - self.session.delete(link) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - # Create new links. - for input in entity_data.get("inputs", []): - input_id = io.find_one({"_id": input})["data"]["ftrackId"] - assetbuild = self.session.get("AssetBuild", input_id) - self.log.debug( - "Creating link from {0} to {1}".format( - assetbuild["name"], entity["name"] - ) - ) - self.session.create( - "TypedContextLink", {"from": assetbuild, "to": entity} - ) - - def get_all_task_types(self, project): - tasks = {} - proj_template = project['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - - def create_task(self, name, task_type, parent): - task = self.session.create('Task', { - 'name': name, - 'parent': parent - }) - # TODO not secured!!! - check if task_type exists - self.log.info(task_type) - self.log.info(self.task_types) - task['type'] = self.task_types[task_type] - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return task - - def create_entity(self, name, type, parent): - entity = self.session.create(type, { - 'name': name, - 'parent': parent - }) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return entity - - def auto_sync_off(self, project): - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - - self.log.info("Ftrack autosync swithed off") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def auto_sync_on(self, project): - - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - self.log.info("Ftrack autosync swithed on") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) From 9e2875e46a7e10e7c91855ec19b5d2ed540584c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 15:08:20 +0100 Subject: [PATCH 037/264] copied code and constant as it is not possible to import pype's ftrack module in python 2 --- .../publish/integrate_hierarchy_ftrack.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index 99e84a1856f..2d856e01560 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -1,12 +1,37 @@ import sys +import collections import six + import pyblish.api from avalon import io -try: - from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC -except Exception: - CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" +# Copy of constant `pype.modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + + +# Copy of `get_pype_attr` from pype.modules.ftrack.lib +def get_pype_attr(session, split_hierarchical=True): + custom_attributes = [] + hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query + cust_attrs_query = ( + "select id, entity_type, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where group.name in (\"avalon\", \"pype\")" + ) + all_avalon_attr = session.query(cust_attrs_query).all() + for cust_attr in all_avalon_attr: + if split_hierarchical and cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue + + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + + return custom_attributes class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): From 5e0fa02ad54babd1ed51bf03077d47c4c9b3c1b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 19:00:45 +0100 Subject: [PATCH 038/264] added ability to define bg color for extract review --- pype/plugins/global/publish/extract_review.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 5695107039a..84406d0ae23 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -41,6 +41,9 @@ class ExtractReview(pyblish.api.InstancePlugin): video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts + # Backgroud extensions + alpha_exts = ["exr", "png", "dpx"] + # FFmpeg tools paths ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") @@ -299,6 +302,14 @@ def prepare_temp_data(self, instance, repre, output_def): ): with_audio = False + input_is_sequence = self.input_is_sequence(repre) + + input_allow_bg = False + if input_is_sequence and repre["files"]: + ext = os.path.splitext(repre["files"][0])[1].replace(".", "") + if ext in self.alpha_exts: + input_allow_bg = True + return { "fps": float(instance.data["fps"]), "frame_start": frame_start, @@ -313,7 +324,8 @@ def prepare_temp_data(self, instance, repre, output_def): "resolution_width": instance.data.get("resolutionWidth"), "resolution_height": instance.data.get("resolutionHeight"), "origin_repre": repre, - "input_is_sequence": self.input_is_sequence(repre), + "input_is_sequence": input_is_sequence, + "input_allow_bg": input_allow_bg, "with_audio": with_audio, "without_handles": without_handles, "handles_are_set": handles_are_set @@ -459,6 +471,22 @@ def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) + bg_color = output_def.get("bg_color") + if bg_color: + if temp_data["input_allow_bg"]: + self.log.info("Applying BG color {}".format(bg_color)) + ffmpeg_video_filters.extend([ + "split=2[bg][fg]", + "[bg]drawbox=c={}:replace=1:t=fill[bg]".format(bg_color), + "[bg][fg]overlay=format=auto" + ]) + else: + self.log.info(( + "Outpud definition has defined BG color input was" + " resolved as does not support adding BG." + )) + + # Add argument to override output file ffmpeg_output_args.append("-y") From 5613f871ce62e05693f2c9f9651f72f8ccb1f42f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Mar 2021 19:01:44 +0100 Subject: [PATCH 039/264] PS - added Subset Manager into menu Implemented list_instances and remove_instance methods --- .../websocket_server/hosts/photoshop.py | 3 +++ .../stubs/photoshop_server_stub.py | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index cdfb9413a09..b75874aa6c5 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -54,6 +54,9 @@ async def sceneinventory_route(self): async def projectmanager_route(self): self._tool_route("projectmanager") + async def subsetmanager_route(self): + self._tool_route("subsetmanager") + def _tool_route(self, tool_name): """The address accessed when clicking on the buttons.""" partial_method = functools.partial(photoshop.show, tool_name) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index d2231537973..9677fa61a84 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -238,7 +238,14 @@ def get_layers_metadata(self): """ Reads layers metadata from Headline from active document in PS. (Headline accessible by File > File Info) - Returns(string): - json documents + + Returns: + (string): - json documents + example: + {"8":{"active":true,"subset":"imageBG", + "family":"image","id":"pyblish.avalon.instance", + "asset":"Town"}} + 8 is layer(group) id - used for deletion, update etc. """ layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) @@ -288,6 +295,19 @@ def delete_layer(self, layer_id): ('Photoshop.delete_layer', layer_id=layer_id)) + def remove_instance(self, instance_id): + cleaned_data = {} + + for key, instance in self.get_layers_metadata().items(): + if key != instance_id: + cleaned_data[key] = instance + + payload = json.dumps(cleaned_data, indent=4) + + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) + def close(self): self.client.close() From 5c8fb5247282d1e942cd0dbb2425c0db49df4981 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 3 Mar 2021 19:24:00 +0100 Subject: [PATCH 040/264] added validation of bg color value --- pype/plugins/global/publish/extract_review.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 84406d0ae23..e41ba9bfc43 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -43,6 +43,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Backgroud extensions alpha_exts = ["exr", "png", "dpx"] + color_regex = re.compile(r"^#[a-fA-F0-9]{6}$") # FFmpeg tools paths ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") @@ -473,18 +474,25 @@ def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): bg_color = output_def.get("bg_color") if bg_color: - if temp_data["input_allow_bg"]: + if not temp_data["input_allow_bg"]: + self.log.info(( + "Outpud definition has defined BG color input was" + " resolved as does not support adding BG." + )) + elif not self.color_regex.match(bg_color): + self.log.warning(( + "Color defined in output definition does not match" + " regex `^#[a-fA-F0-9]{6}$`" + )) + + else: self.log.info("Applying BG color {}".format(bg_color)) ffmpeg_video_filters.extend([ "split=2[bg][fg]", "[bg]drawbox=c={}:replace=1:t=fill[bg]".format(bg_color), "[bg][fg]overlay=format=auto" ]) - else: - self.log.info(( - "Outpud definition has defined BG color input was" - " resolved as does not support adding BG." - )) + # Add argument to override output file From e3b686ed17edb5fbbcc9af895c5ca436337b349f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:03:08 +0100 Subject: [PATCH 041/264] fix frame range of pass output --- pype/plugins/tvpaint/publish/collect_instances.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index 1a5a187c16b..efe265e7916 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -70,15 +70,8 @@ def process(self, context): if instance is None: continue - frame_start = context.data["frameStart"] - frame_end = frame_start - for layer in instance.data["layers"]: - _frame_end = layer["frame_end"] - if _frame_end > frame_end: - frame_end = _frame_end - - instance.data["frameStart"] = frame_start - instance.data["frameEnd"] = frame_end + instance.data["frameStart"] = context.data["frameStart"] + instance.data["frameEnd"] = context.data["frameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) From 86ccfb60d2a02fc88ec2d239deb304e9d8ffe2ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:17:12 +0100 Subject: [PATCH 042/264] renamed extract sequence to extract review sequence --- ...sequence.py => extract_review_sequence.py} | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) rename pype/plugins/tvpaint/publish/{extract_sequence.py => extract_review_sequence.py} (89%) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_review_sequence.py similarity index 89% rename from pype/plugins/tvpaint/publish/extract_sequence.py rename to pype/plugins/tvpaint/publish/extract_review_sequence.py index 17d8dc60f47..54f21cb9742 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_review_sequence.py @@ -6,44 +6,10 @@ from PIL import Image -class ExtractSequence(pyblish.api.Extractor): - label = "Extract Sequence" +class ExtractReviewSequence(pyblish.api.Extractor): + label = "Extract Review Sequence" hosts = ["tvpaint"] - families = ["review", "renderPass", "renderLayer"] - - save_mode_to_ext = { - "avi": ".avi", - "bmp": ".bmp", - "cin": ".cin", - "deep": ".dip", - "dps": ".dps", - "dpx": ".dpx", - "flc": ".fli", - "gif": ".gif", - "ilbm": ".iff", - "jpg": ".jpg", - "jpeg": ".jpg", - "pcx": ".pcx", - "png": ".png", - "psd": ".psd", - "qt": ".qt", - "rtv": ".rtv", - "sun": ".ras", - "tiff": ".tiff", - "tga": ".tga", - "vpb": ".vpb" - } - sequential_save_mode = { - "bmp", - "dpx", - "ilbm", - "jpg", - "jpeg", - "png", - "sun", - "tiff", - "tga" - } + families = ["review"] def process(self, instance): self.log.info( From 7cb7a5b9b1272e4fb36cc0199e6fd643d65622bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:18:40 +0100 Subject: [PATCH 043/264] moved back tvpaint's lib for compositing --- pype/hosts/tvpaint/lib.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pype/hosts/tvpaint/lib.py diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py new file mode 100644 index 00000000000..8172392c7f2 --- /dev/null +++ b/pype/hosts/tvpaint/lib.py @@ -0,0 +1,17 @@ +from PIL import Image + + +def composite_images( + input_image_paths, output_filepath, scene_width, scene_height +): + img_obj = None + for image_filepath in input_image_paths: + _img_obj = Image.open(image_filepath) + if img_obj is None: + img_obj = _img_obj + else: + img_obj.alpha_composite(_img_obj) + + if img_obj is None: + img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) + img_obj.save(output_filepath) From 253fba67a7ccd30ba5438d004b0d4fd6129f4cd4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:19:57 +0100 Subject: [PATCH 044/264] implemented extract review that can render layer by layer with alpha --- .../tvpaint/publish/extract_sequence.py | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/extract_sequence.py diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py new file mode 100644 index 00000000000..035f50c0585 --- /dev/null +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -0,0 +1,492 @@ +import os +import shutil +import time +import tempfile +import multiprocessing + +import pyblish.api +from avalon.tvpaint import lib +from pype.hosts.tvpaint.lib import composite_images +from PIL import Image + + +class ExtractSequence(pyblish.api.Extractor): + label = "Extract Sequence" + hosts = ["tvpaint"] + families = ["renderPass", "renderLayer"] + + def process(self, instance): + self.log.info( + "* Processing instance \"{}\"".format(instance.data["label"]) + ) + + # Get all layers and filter out not visible + layers = instance.data["layers"] + filtered_layers = [ + layer + for layer in layers + if layer["visible"] + ] + layer_names = [str(layer["name"]) for layer in filtered_layers] + if not layer_names: + self.log.info( + "None of the layers from the instance" + " are visible. Extraction skipped." + ) + return + + joined_layer_names = ", ".join( + ["\"{}\"".format(name) for name in layer_names] + ) + self.log.debug( + "Instance has {} layers with names: {}".format( + len(layer_names), joined_layer_names + ) + ) + + family_lowered = instance.data["family"].lower() + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + scene_width = instance.context.data["sceneWidth"] + scene_height = instance.context.data["sceneHeight"] + + filename_template = self._get_filename_template(frame_end) + ext = os.path.splitext(filename_template)[1].replace(".", "") + + self.log.debug("Using file template \"{}\"".format(filename_template)) + + # Save to staging dir + output_dir = instance.data.get("stagingDir") + if not output_dir: + # Create temp folder if staging dir is not set + output_dir = tempfile.mkdtemp().replace("\\", "/") + instance.data["stagingDir"] = output_dir + + self.log.debug( + "Files will be rendered to folder: {}".format(output_dir) + ) + + thumbnail_filename = "thumbnail.jpg" + + # Render output + output_filepaths, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ) + + # Fill tags and new families + tags = [] + if family_lowered in ("review", "renderlayer"): + tags.append("review") + + repre_files = [ + os.path.basename(filepath) + for filepath in output_filepaths + ] + # Sequence of one frame + if len(repre_files) == 1: + repre_files = repre_files[0] + + new_repre = { + "name": ext, + "ext": ext, + "files": repre_files, + "stagingDir": output_dir, + "frameStart": frame_start, + "frameEnd": frame_end, + "tags": tags + } + self.log.debug("Creating new representation: {}".format(new_repre)) + + instance.data["representations"].append(new_repre) + + if family_lowered in ("renderpass", "renderlayer"): + # Change family to render + instance.data["family"] = "render" + + if not thumbnail_fullpath: + return + + thumbnail_ext = os.path.splitext( + thumbnail_fullpath + )[1].replace(".", "") + # Create thumbnail representation + thumbnail_repre = { + "name": "thumbnail", + "ext": thumbnail_ext, + "outputName": "thumb", + "files": os.path.basename(thumbnail_fullpath), + "stagingDir": output_dir, + "tags": ["thumbnail"] + } + instance.data["representations"].append(thumbnail_repre) + + def _get_filename_template(self, frame_end): + """Get filetemplate for rendered files. + + This is simple template contains `{frame}{ext}` for sequential outputs + and `single_file{ext}` for single file output. Output is rendered to + temporary folder so filename should not matter as integrator change + them. + """ + frame_padding = 4 + frame_end_str_len = len(str(frame_end)) + if frame_end_str_len > frame_padding: + frame_padding = frame_end_str_len + + return "{{:0>{}}}".format(frame_padding) + ".png" + + def render( + self, filename_template, output_dir, layers, + frame_start, frame_end, thumbnail_filename, + scene_width, scene_height + ): + """ Export images from TVPaint. + + Args: + save_mode (str): Argument for `tv_savemode` george script function. + More about save mode in documentation. + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + layers (list): List of layers to be exported. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + + # Map layers by position + layers_by_position = {} + layer_ids = [] + for layer in layers: + position = layer["position"] + layers_by_position[position] = layer + + layer_ids.append(layer["layer_id"]) + + # Sort layer positions in reverse order + sorted_positions = list(reversed(sorted(layers_by_position.keys()))) + if not sorted_positions: + return + + self.log.debug("Collecting pre/post behavior of individual layers.") + behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) + + mark_in_index = frame_start - 1 + mark_out_index = frame_end - 1 + + tmp_filename_template = "pos_{}." + filename_template + + files_by_position = {} + for position in sorted_positions: + layer = layers_by_position[position] + behavior = behavior_by_layer_id[layer["layer_id"]] + files_by_frames = self.render_layer( + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ) + files_by_position[position] = files_by_frames + + output = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) + return output + + def render_layer( + self, + layer, + tmp_filename_template, + output_dir, + behavior, + mark_in_index, + mark_out_index + ): + layer_id = layer["layer_id"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + exposure_frames = lib.get_exposure_frames( + layer_id, frame_start_index, frame_end_index + ) + if frame_start_index not in exposure_frames: + exposure_frames.append(frame_start_index) + + layer_files_by_frame = {} + george_script_lines = [ + "tv_SaveMode \"PNG\"" + ] + layer_position = layer["position"] + + for frame_idx in exposure_frames: + filename = tmp_filename_template.format(layer_position, frame_idx) + dst_path = "/".join([output_dir, filename]) + layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) + + # Go to frame + george_script_lines.append("tv_layerImage {}".format(frame_idx)) + # Store image to output + george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + + self.log.debug("Rendering exposure frames {} of layer {}".format( + str(exposure_frames), layer_id + )) + # Let TVPaint render layer's image + lib.execute_george_through_file("\n".join(george_script_lines)) + + # Fill frames between `frame_start_index` and `frame_end_index` + self.log.debug(( + "Filling frames between first and last frame of layer ({} - {})." + ).format(frame_start_index + 1, frame_end_index + 1)) + + prev_filepath = None + for frame_idx in range(frame_start_index, frame_end_index + 1): + if frame_idx in layer_files_by_frame: + prev_filepath = layer_files_by_frame[frame_idx] + continue + + if prev_filepath is None: + raise ValueError("BUG: First frame of layer was not rendered!") + + filename = tmp_filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(prev_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + # Fill frames by pre/post behavior of layer + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + self.log.debug(( + "Completing image sequence of layer by pre/post behavior." + " PRE: {} | POST: {}" + ).format(pre_behavior, post_behavior)) + + # Pre behavior + self._fill_frame_by_pre_behavior( + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + self._fill_frame_by_post_behavior( + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + tmp_filename_template, + output_dir + ) + return layer_files_by_frame + + def _fill_frame_by_pre_behavior( + self, + layer, + pre_behavior, + mark_in_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_in_index >= frame_start_index: + return + + if pre_behavior == "none": + return + + if pre_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_start_index] + for frame_idx in range(mark_in_index, frame_start_index): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = ( + (frame_end_index - frame_idx) % frame_count + ) + eq_frame_idx = frame_end_index - eq_frame_idx_offset + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif pre_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = (seq_len - eq_frame_idx_offset) + eq_frame_idx = frame_start_index + eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _fill_frame_by_post_behavior( + self, + layer, + post_behavior, + mark_out_index, + layer_files_by_frame, + filename_template, + output_dir + ): + layer_position = layer["position"] + frame_start_index = layer["frame_start"] + frame_end_index = layer["frame_end"] + frame_count = frame_end_index - frame_start_index + 1 + if mark_out_index <= frame_end_index: + return + + if post_behavior == "none": + return + + if post_behavior == "hold": + # Keep first frame for whole time + eq_frame_filepath = layer_files_by_frame[frame_end_index] + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "loop": + # Loop backwards from last frame of layer + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx = frame_idx % frame_count + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + elif post_behavior == "pingpong": + half_seq_len = frame_count - 1 + seq_len = half_seq_len * 2 + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len + if eq_frame_idx_offset > half_seq_len: + eq_frame_idx_offset = seq_len - eq_frame_idx_offset + eq_frame_idx = frame_end_index - eq_frame_idx_offset + + eq_frame_filepath = layer_files_by_frame[eq_frame_idx] + + filename = filename_template.format(layer_position, frame_idx) + new_filepath = "/".join([output_dir, filename]) + self._copy_image(eq_frame_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath + + def _composite_files( + self, files_by_position, output_dir, frame_start, frame_end, + filename_template, thumbnail_filename, scene_width, scene_height + ): + # Prepare paths to images by frames into list where are stored + # in order of compositing. + images_by_frame = {} + for frame_idx in range(frame_start, frame_end + 1): + images_by_frame[frame_idx] = [] + for position in sorted(files_by_position.keys(), reverse=True): + position_data = files_by_position[position] + if frame_idx in position_data: + images_by_frame[frame_idx].append(position_data[frame_idx]) + + process_count = os.cpu_count() + if process_count > 1: + process_count -= 1 + + processes = {} + output_filepaths = [] + thumbnail_src_filepath = None + for frame_idx in sorted(images_by_frame.keys()): + image_filepaths = images_by_frame[frame_idx] + frame = frame_idx + 1 + + output_filename = filename_template.format(frame) + output_filepath = os.path.join(output_dir, output_filename) + output_filepaths.append(output_filepath) + + if thumbnail_filename and thumbnail_src_filepath is None: + thumbnail_src_filepath = output_filepath + + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=( + image_filepaths, output_filepath, scene_width, scene_height + ) + ) + + # Wait until all processes are done + running_processes = {} + while True: + for idx in tuple(running_processes.keys()): + process = running_processes[idx] + if not process.is_alive(): + running_processes.pop(idx).join() + + if processes and len(running_processes) != process_count: + indexes = list(processes.keys()) + for _ in range(process_count - len(running_processes)): + if not indexes: + break + idx = indexes.pop(0) + running_processes[idx] = processes.pop(idx) + running_processes[idx].start() + + if not running_processes and not processes: + break + + time.sleep(0.01) + + thumbnail_filepath = None + if thumbnail_src_filepath: + source_img = Image.open(thumbnail_src_filepath) + thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath + + def _cleanup_tmp_files(self, files_by_position): + for data in files_by_position.values(): + for filepath in data.values(): + os.remove(filepath) + + def _copy_image(self, src_path, dst_path): + # Create hardlink of image instead of copying if possible + if hasattr(os, "link"): + os.link(src_path, dst_path) + else: + shutil.copy(src_path, dst_path) From 285d91d5e2225feea8b0bc797d7a4cd5eeb46a37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:27:35 +0100 Subject: [PATCH 045/264] modified extract sequence to skip compositing is rendering only one layer --- .../tvpaint/publish/extract_sequence.py | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 035f50c0585..ad87ebbd81e 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -183,31 +183,55 @@ def render( tmp_filename_template = "pos_{}." + filename_template files_by_position = {} + is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] + + if is_single_layer: + _template = filename_template + else: + _template = tmp_filename_template + files_by_frames = self.render_layer( layer, - tmp_filename_template, + _template, output_dir, behavior, mark_in_index, mark_out_index ) - files_by_position[position] = files_by_frames + if is_single_layer: + output_filepaths = list(files_by_frames.values()) + else: + files_by_position[position] = files_by_frames + + if not is_single_layer: + output_filepaths = self._composite_files( + files_by_position, + output_dir, + mark_in_index, + mark_out_index, + filename_template, + thumbnail_filename, + scene_width, + scene_height + ) + self._cleanup_tmp_files(files_by_position) - output = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - thumbnail_filename, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) - return output + thumbnail_src_filepath = None + thumbnail_filepath = None + if output_filepaths: + thumbnail_src_filepath = tuple(sorted(output_filepaths))[0] + + 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_filename) + thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) + thumbnail_obj.paste(source_img) + thumbnail_obj.save(thumbnail_filepath) + + return output_filepaths, thumbnail_filepath def render_layer( self, @@ -469,15 +493,7 @@ def _composite_files( time.sleep(0.01) - thumbnail_filepath = None - if thumbnail_src_filepath: - source_img = Image.open(thumbnail_src_filepath) - thumbnail_filepath = os.path.join(output_dir, thumbnail_filename) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) - - return output_filepaths, thumbnail_filepath + return output_filepaths def _cleanup_tmp_files(self, files_by_position): for data in files_by_position.values(): From 87142459344234cacbea9165a38278098faf4933 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 13:58:29 +0100 Subject: [PATCH 046/264] removed thumbnail filename variable --- .../plugins/tvpaint/publish/extract_sequence.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index ad87ebbd81e..a66141fa193 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -66,13 +66,10 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - thumbnail_filename = "thumbnail.jpg" - # Render output output_filepaths, thumbnail_fullpath = self.render( filename_template, output_dir, filtered_layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ) # Fill tags and new families @@ -139,8 +136,7 @@ def _get_filename_template(self, frame_end): def render( self, filename_template, output_dir, layers, - frame_start, frame_end, thumbnail_filename, - scene_width, scene_height + frame_start, frame_end, scene_width, scene_height ): """ Export images from TVPaint. @@ -213,7 +209,6 @@ def render( mark_in_index, mark_out_index, filename_template, - thumbnail_filename, scene_width, scene_height ) @@ -226,7 +221,7 @@ def render( 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_filename) + 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) @@ -434,7 +429,7 @@ def _fill_frame_by_post_behavior( def _composite_files( self, files_by_position, output_dir, frame_start, frame_end, - filename_template, thumbnail_filename, scene_width, scene_height + filename_template, scene_width, scene_height ): # Prepare paths to images by frames into list where are stored # in order of compositing. @@ -452,7 +447,6 @@ def _composite_files( processes = {} output_filepaths = [] - thumbnail_src_filepath = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 @@ -461,9 +455,6 @@ def _composite_files( output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - if thumbnail_filename and thumbnail_src_filepath is None: - thumbnail_src_filepath = output_filepath - processes[frame_idx] = multiprocessing.Process( target=composite_images, args=( From 8004d3c47501b02a2268cb859e7f2c74e7c1d5b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:21:06 +0100 Subject: [PATCH 047/264] merged extractors to one extractor --- .../publish/extract_review_sequence.py | 206 ------------------ .../tvpaint/publish/extract_sequence.py | 88 ++++++-- 2 files changed, 74 insertions(+), 220 deletions(-) delete mode 100644 pype/plugins/tvpaint/publish/extract_review_sequence.py diff --git a/pype/plugins/tvpaint/publish/extract_review_sequence.py b/pype/plugins/tvpaint/publish/extract_review_sequence.py deleted file mode 100644 index 54f21cb9742..00000000000 --- a/pype/plugins/tvpaint/publish/extract_review_sequence.py +++ /dev/null @@ -1,206 +0,0 @@ -import os -import tempfile - -import pyblish.api -from avalon.tvpaint import lib -from PIL import Image - - -class ExtractReviewSequence(pyblish.api.Extractor): - label = "Extract Review Sequence" - hosts = ["tvpaint"] - families = ["review"] - - def process(self, instance): - self.log.info( - "* Processing instance \"{}\"".format(instance.data["label"]) - ) - - # Get all layers and filter out not visible - layers = instance.data["layers"] - filtered_layers = [ - layer - for layer in layers - if layer["visible"] - ] - filtered_layer_ids = [ - layer["layer_id"] - for layer in filtered_layers - ] - layer_names = [str(layer["name"]) for layer in filtered_layers] - if not layer_names: - self.log.info( - "None of the layers from the instance" - " are visible. Extraction skipped." - ) - return - - joined_layer_names = ", ".join( - ["\"{}\"".format(name) for name in layer_names] - ) - self.log.debug( - "Instance has {} layers with names: {}".format( - len(layer_names), joined_layer_names - ) - ) - - family_lowered = instance.data["family"].lower() - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - filename_template = self._get_filename_template(frame_end) - ext = os.path.splitext(filename_template)[1].replace(".", "") - - self.log.debug("Using file template \"{}\"".format(filename_template)) - - # Save to staging dir - output_dir = instance.data.get("stagingDir") - if not output_dir: - # Create temp folder if staging dir is not set - output_dir = tempfile.mkdtemp().replace("\\", "/") - instance.data["stagingDir"] = output_dir - - self.log.debug( - "Files will be rendered to folder: {}".format(output_dir) - ) - - first_frame_filename = filename_template.format(frame_start) - first_frame_filepath = os.path.join(output_dir, first_frame_filename) - - # Store layers visibility - layer_visibility_by_id = {} - for layer in instance.context.data["layersData"]: - layer_id = layer["layer_id"] - layer_visibility_by_id[layer_id] = layer["visible"] - - george_script_lines = [] - for layer_id in layer_visibility_by_id.keys(): - visible = layer_id in filtered_layer_ids - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - # Render output - repre_files = self.render( - filename_template, - output_dir, - frame_start, - frame_end - ) - - # Restore visibility - george_script_lines = [] - for layer_id, visible in layer_visibility_by_id.items(): - value = "on" if visible else "off" - george_script_lines.append( - "tv_layerdisplay {} \"{}\"".format(layer_id, value) - ) - lib.execute_george_through_file("\n".join(george_script_lines)) - - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - if os.path.exists(first_frame_filepath): - 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) - - # Fill tags and new families - tags = [] - if family_lowered in ("review", "renderlayer"): - tags.append("review") - - # Sequence of one frame - if len(repre_files) == 1: - repre_files = repre_files[0] - - new_repre = { - "name": ext, - "ext": ext, - "files": repre_files, - "stagingDir": output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "tags": tags - } - self.log.debug("Creating new representation: {}".format(new_repre)) - - instance.data["representations"].append(new_repre) - - if family_lowered in ("renderpass", "renderlayer"): - # Change family to render - instance.data["family"] = "render" - - if not os.path.exists(thumbnail_filepath): - return - - thumbnail_ext = os.path.splitext( - thumbnail_filepath - )[1].replace(".", "") - # Create thumbnail representation - thumbnail_repre = { - "name": "thumbnail", - "ext": thumbnail_ext, - "outputName": "thumb", - "files": os.path.basename(thumbnail_filepath), - "stagingDir": output_dir, - "tags": ["thumbnail"] - } - instance.data["representations"].append(thumbnail_repre) - - def _get_filename_template(self, frame_end): - """Get filetemplate for rendered files. - - This is simple template contains `{frame}{ext}` for sequential outputs - and `single_file{ext}` for single file output. Output is rendered to - temporary folder so filename should not matter as integrator change - them. - """ - frame_padding = 4 - frame_end_str_len = len(str(frame_end)) - if frame_end_str_len > frame_padding: - frame_padding = frame_end_str_len - - return "{{:0>{}}}".format(frame_padding) + ".png" - - def render(self, filename_template, output_dir, frame_start, frame_end): - """ Export images from TVPaint. - - Args: - filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. - - Retruns: - dict: Mapping frame to output filepath. - """ - self.log.debug("Preparing data for rendering.") - first_frame_filepath = os.path.join( - output_dir, - filename_template.format(frame_start, frame=frame_start) - ) - mark_in = frame_start - 1 - mark_out = frame_end - 1 - - george_script_lines = [ - "tv_SaveMode \"PNG\"", - "export_path = \"{}\"".format( - first_frame_filepath.replace("\\", "/") - ), - "tv_savesequence '\"'export_path'\"' {} {}".format( - mark_in, mark_out - ) - ] - lib.execute_george_through_file("\n".join(george_script_lines)) - - output = [] - for frame in range(frame_start, frame_end + 1): - output.append( - filename_template.format(frame, frame=frame) - ) - return output diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index a66141fa193..6fe35f62519 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -13,7 +13,7 @@ class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] - families = ["renderPass", "renderLayer"] + families = ["review", "renderPass", "renderLayer"] def process(self, instance): self.log.info( @@ -66,21 +66,22 @@ def process(self, instance): "Files will be rendered to folder: {}".format(output_dir) ) - # Render output - output_filepaths, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height - ) + if instance.data["family"] == "review": + repre_files, thumbnail_fullpath = self.render_review( + filename_template, output_dir, frame_start, frame_end + ) + else: + # Render output + repre_files, thumbnail_fullpath = self.render( + filename_template, output_dir, filtered_layers, + frame_start, frame_end, scene_width, scene_height + ) # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): tags.append("review") - repre_files = [ - os.path.basename(filepath) - for filepath in output_filepaths - ] # Sequence of one frame if len(repre_files) == 1: repre_files = repre_files[0] @@ -134,6 +135,58 @@ def _get_filename_template(self, frame_end): return "{{:0>{}}}".format(frame_padding) + ".png" + def render_review( + self, filename_template, output_dir, frame_start, frame_end + ): + """ Export images from TVPaint. + + Args: + filename_template (str): Filename template of an output. Template + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (list): List of layers to be exported. + frame_start (int): Starting frame from which export will begin. + frame_end (int): On which frame export will end. + + Retruns: + dict: Mapping frame to output filepath. + """ + self.log.debug("Preparing data for rendering.") + first_frame_filepath = os.path.join( + output_dir, + filename_template.format(frame_start, frame=frame_start) + ) + mark_in = frame_start - 1 + mark_out = frame_end - 1 + + george_script_lines = [ + "tv_SaveMode \"PNG\"", + "export_path = \"{}\"".format( + first_frame_filepath.replace("\\", "/") + ), + "tv_savesequence '\"'export_path'\"' {} {}".format( + mark_in, mark_out + ) + ] + lib.execute_george_through_file("\n".join(george_script_lines)) + + output = [] + first_frame_filepath = None + for frame in range(frame_start, frame_end + 1): + filename = filename_template.format(frame, frame=frame) + output.append(filename) + if first_frame_filepath is None: + first_frame_filepath = os.path.join(output_dir, filename) + + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + if first_frame_filepath and os.path.exists(first_frame_filepath): + 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) + return output, thumbnail_filepath + def render( self, filename_template, output_dir, layers, frame_start, frame_end, scene_width, scene_height @@ -189,7 +242,7 @@ def render( else: _template = tmp_filename_template - files_by_frames = self.render_layer( + files_by_frames = self._render_layer( layer, _template, output_dir, @@ -226,9 +279,13 @@ def render( thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return output_filepaths, thumbnail_filepath + repre_files = [ + os.path.basename(path) + for path in output_filepaths + ] + return repre_files, thumbnail_filepath - def render_layer( + def _render_layer( self, layer, tmp_filename_template, @@ -282,7 +339,10 @@ def render_layer( if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath From 445b6b5dd6565ab557006427e0d53d41c9363ecc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:31:53 +0100 Subject: [PATCH 048/264] extract sequence use key word arguments in filename template --- .../tvpaint/publish/extract_sequence.py | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 6fe35f62519..b7f01982edd 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -133,7 +133,7 @@ def _get_filename_template(self, frame_end): if frame_end_str_len > frame_padding: frame_padding = frame_end_str_len - return "{{:0>{}}}".format(frame_padding) + ".png" + return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( self, filename_template, output_dir, frame_start, frame_end @@ -142,9 +142,9 @@ def render_review( Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template may contain only - keyword argument `{frame}` or index argument (for same value). - Extension in template must match `save_mode`. + should already contain extension. Template must contain + keyword argument `{frame}`. Extension in template must match + `save_mode`. output_dir (list): List of layers to be exported. frame_start (int): Starting frame from which export will begin. frame_end (int): On which frame export will end. @@ -155,7 +155,7 @@ def render_review( self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( output_dir, - filename_template.format(frame_start, frame=frame_start) + filename_template.format(frame=frame_start) ) mark_in = frame_start - 1 mark_out = frame_end - 1 @@ -174,7 +174,7 @@ def render_review( output = [] first_frame_filepath = None for frame in range(frame_start, frame_end + 1): - filename = filename_template.format(frame, frame=frame) + filename = filename_template.format(frame=frame) output.append(filename) if first_frame_filepath is None: first_frame_filepath = os.path.join(output_dir, filename) @@ -229,7 +229,7 @@ def render( mark_in_index = frame_start - 1 mark_out_index = frame_end - 1 - tmp_filename_template = "pos_{}." + filename_template + tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} is_single_layer = len(sorted_positions) == 1 @@ -310,7 +310,10 @@ def _render_layer( layer_position = layer["position"] for frame_idx in exposure_frames: - filename = tmp_filename_template.format(layer_position, frame_idx) + filename = tmp_filename_template.format( + pos=layer_position, + frame=frame_idx + ) dst_path = "/".join([output_dir, filename]) layer_files_by_frame[frame_idx] = os.path.normpath(dst_path) @@ -397,7 +400,10 @@ def _fill_frame_by_pre_behavior( # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -411,7 +417,10 @@ def _fill_frame_by_pre_behavior( eq_frame_idx = frame_end_index - eq_frame_idx_offset eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -427,7 +436,10 @@ def _fill_frame_by_pre_behavior( eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -455,7 +467,10 @@ def _fill_frame_by_post_behavior( # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -466,7 +481,10 @@ def _fill_frame_by_post_behavior( eq_frame_idx = frame_idx % frame_count eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -482,7 +500,10 @@ def _fill_frame_by_post_behavior( eq_frame_filepath = layer_files_by_frame[eq_frame_idx] - filename = filename_template.format(layer_position, frame_idx) + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) new_filepath = "/".join([output_dir, filename]) self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath @@ -511,7 +532,7 @@ def _composite_files( image_filepaths = images_by_frame[frame_idx] frame = frame_idx + 1 - output_filename = filename_template.format(frame) + output_filename = filename_template.format(frame=frame) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) From aae052244bda45f3d1280e1f157fe96a17b167ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 14:46:58 +0100 Subject: [PATCH 049/264] handle "none" behavior --- .../tvpaint/publish/extract_sequence.py | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index b7f01982edd..2c318136e6b 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -7,7 +7,7 @@ import pyblish.api from avalon.tvpaint import lib from pype.hosts.tvpaint.lib import composite_images -from PIL import Image +from PIL import Image, ImageDraw class ExtractSequence(pyblish.api.Extractor): @@ -394,9 +394,29 @@ def _fill_frame_by_pre_behavior( return if pre_behavior == "none": - return - - if pre_behavior == "hold": + # Take size from first image and fill it with transparent color + first_filename = filename_template.format( + pos=layer_position, + frame=frame_start_index + ) + first_filepath = os.path.join(output_dir, first_filename) + empty_image_filepath = None + for frame_idx in reversed(range(mark_in_index, frame_start_index)): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(first_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) + + elif pre_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_start_index] for frame_idx in range(mark_in_index, frame_start_index): @@ -461,9 +481,29 @@ def _fill_frame_by_post_behavior( return if post_behavior == "none": - return - - if post_behavior == "hold": + # Take size from last image and fill it with transparent color + last_filename = filename_template.format( + pos=layer_position, + frame=frame_end_index + ) + last_filepath = os.path.join(output_dir, last_filename) + empty_image_filepath = None + for frame_idx in range(frame_end_index + 1, mark_out_index + 1): + filename = filename_template.format( + pos=layer_position, + frame=frame_idx + ) + filepath = os.path.join(output_dir, filename) + if empty_image_filepath is None: + img_obj = Image.open(last_filepath) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + empty_image_filepath = filepath + else: + self._copy_image(empty_image_filepath, filepath) + + elif post_behavior == "hold": # Keep first frame for whole time eq_frame_filepath = layer_files_by_frame[frame_end_index] for frame_idx in range(frame_end_index + 1, mark_out_index + 1): From fc89c6f8e968d34a5d2590d0254c923ad8721aca Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:02:23 +0100 Subject: [PATCH 050/264] PS - use subset name as instance name Warn if duplicate subsets found --- pype/plugins/photoshop/publish/collect_instances.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 14803cceee5..5390df768b3 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -24,6 +24,7 @@ def process(self, context): stub = photoshop.stub() layers = stub.get_layers() layers_meta = stub.get_layers_metadata() + instance_names = [] for layer in layers: layer_data = stub.read(layer, layers_meta) @@ -41,14 +42,20 @@ def process(self, context): # self.log.info("%s skipped, it was empty." % layer.Name) # continue - instance = context.create_instance(layer.name) + instance = context.create_instance(layer_data["subset"]) instance.append(layer) instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ layer_data["family"] ] instance.data["publish"] = layer.visible + instance_names.append(layer_data["subset"]) # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.info("instance: {} ".format(instance.data)) + + if len(instance_names) != len(set(instance_names)): + self.log.warning("Duplicate instances found. " + + "Remove unwanted via SubsetManager") From a50fcad7d3e4e1545e80ea0551c9c52ba6c81fde Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:54:29 +0100 Subject: [PATCH 051/264] PS - added validator for subset names uniqueness --- .../publish/validate_unique_subsets.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pype/plugins/photoshop/publish/validate_unique_subsets.py diff --git a/pype/plugins/photoshop/publish/validate_unique_subsets.py b/pype/plugins/photoshop/publish/validate_unique_subsets.py new file mode 100644 index 00000000000..91aaa9b6e3c --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_unique_subsets.py @@ -0,0 +1,26 @@ +import pyblish.api +import pype.api + + +class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): + """ + Validate that all subset's names are unique. + """ + + label = "Validate Subset Uniqueness" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + + def process(self, context): + subset_names = [] + + for instance in context: + if instance.data.get('publish'): + subset_names.append(instance.data.get('subset')) + + msg = ( + "Instance subset names are not unique. " + + "Remove duplicates via SubsetManager." + ) + assert len(subset_names) == len(set(subset_names)), msg \ No newline at end of file From 04a7963be0c41943f86cac3ccc4cbca7e15cf093 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:56:07 +0100 Subject: [PATCH 052/264] PS - fixed validate_naming repair action Updates subset name in instance metadata and layer name --- pype/plugins/photoshop/publish/validate_naming.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 2483adcb5e6..f6aeb16a601 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -29,6 +29,7 @@ def process(self, context, plugin): data = stub.read(instance[0]) data["subset"] = "image" + name stub.imprint(instance[0], data) + stub.rename_layer(instance[0].id, name) return True @@ -46,8 +47,11 @@ class ValidateNaming(pyblish.api.InstancePlugin): actions = [ValidateNamingRepair] def process(self, instance): - msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + help_msg = ' Use Repair action (A) in Pyblish to fix it.' + msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], + help_msg) assert " " not in instance.data["name"], msg - msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], + help_msg) assert " " not in instance.data["subset"], msg From 349cee30f803b59a643e8a82d3564416a7f85fd8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 16:24:24 +0100 Subject: [PATCH 053/264] PS - fixed validate_naming repair Use layer name and PUBLISH_ICON, not subset name --- pype/plugins/photoshop/publish/validate_naming.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index f6aeb16a601..6130e583754 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -29,7 +29,10 @@ def process(self, context, plugin): data = stub.read(instance[0]) data["subset"] = "image" + name stub.imprint(instance[0], data) - stub.rename_layer(instance[0].id, name) + + name = name.replace(instance.data["family"], '') + name = stub.PUBLISH_ICON + name + stub.rename_layer(instance.data["uuid"], name) return True From 775640796744be5dc7d7dbdaa5b77fdb772ae6ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 16:41:06 +0100 Subject: [PATCH 054/264] fix ordered dict for python 2 hosts --- pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index 2d856e01560..e8aa87a6f9a 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -164,10 +164,10 @@ def import_to_ftrack(self, input_data, parent=None): # attribute value. # - this is because there may be non hiearchical custom # attributes with different properties - entity_key = collections.OrderedDict({ - "configuration_id": hier_attr["id"], - "entity_id": entity["id"] - }) + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = hier_attr["id"] + entity_key["entity_id"] = entity["id"] + self.session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( "ContextCustomAttributeValue", From 7c696a09600e59e44bea42559dd4319eb36b5689 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:41 +0100 Subject: [PATCH 055/264] add files to `layer_files_by_frame` on creation --- .../tvpaint/publish/extract_sequence.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 2c318136e6b..667799a7cd2 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -406,15 +406,16 @@ def _fill_frame_by_pre_behavior( pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(first_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif pre_behavior == "hold": # Keep first frame for whole time @@ -493,15 +494,16 @@ def _fill_frame_by_post_behavior( pos=layer_position, frame=frame_idx ) - filepath = os.path.join(output_dir, filename) + new_filepath = os.path.join(output_dir, filename) if empty_image_filepath is None: img_obj = Image.open(last_filepath) painter = ImageDraw.Draw(img_obj) painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(filepath) - empty_image_filepath = filepath + img_obj.save(new_filepath) + empty_image_filepath = new_filepath else: - self._copy_image(empty_image_filepath, filepath) + self._copy_image(empty_image_filepath, new_filepath) + layer_files_by_frame[frame_idx] = new_filepath elif post_behavior == "hold": # Keep first frame for whole time From 075080b658498170800f3563ab1669176f0bba5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:26:52 +0100 Subject: [PATCH 056/264] added some debug loggins messages --- .../tvpaint/publish/extract_sequence.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 667799a7cd2..8f302cb746b 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -300,6 +300,9 @@ def _render_layer( exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer["layer_id"], layer["name"] + )) if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -333,6 +336,7 @@ def _render_layer( "Filling frames between first and last frame of layer ({} - {})." ).format(frame_start_index + 1, frame_end_index + 1)) + _debug_filled_frames = [] prev_filepath = None for frame_idx in range(frame_start_index, frame_end_index + 1): if frame_idx in layer_files_by_frame: @@ -341,7 +345,7 @@ def _render_layer( if prev_filepath is None: raise ValueError("BUG: First frame of layer was not rendered!") - + _debug_filled_frames.append(frame_idx) filename = tmp_filename_template.format( pos=layer_position, frame=frame_idx @@ -350,6 +354,8 @@ def _render_layer( self._copy_image(prev_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath + self.log.debug("Filled frames {}".format(str(_debug_filled_frames))) + # Fill frames by pre/post behavior of layer pre_behavior = behavior["pre"] post_behavior = behavior["post"] @@ -391,9 +397,16 @@ def _fill_frame_by_pre_behavior( frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_in_index >= frame_start_index: + self.log.debug(( + "Skipping pre-behavior." + " All frames after Mark In are rendered." + )) return if pre_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + mark_in_index, frame_start_index + )) # Take size from first image and fill it with transparent color first_filename = filename_template.format( pos=layer_position, @@ -479,9 +492,16 @@ def _fill_frame_by_post_behavior( frame_end_index = layer["frame_end"] frame_count = frame_end_index - frame_start_index + 1 if mark_out_index <= frame_end_index: + self.log.debug(( + "Skipping post-behavior." + " All frames up to Mark Out are rendered." + )) return if post_behavior == "none": + self.log.debug("Creating empty images for range {} - {}".format( + frame_end_index + 1, mark_out_index + 1 + )) # Take size from last image and fill it with transparent color last_filename = filename_template.format( pos=layer_position, From 97446f63a784b2e7cb2482d8f39e5e45ae8fa9b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:37 +0100 Subject: [PATCH 057/264] review instance stores copy of layers data --- pype/plugins/tvpaint/publish/collect_instances.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index efe265e7916..57602d96103 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -48,7 +48,10 @@ def process(self, context): instance_data["subset"] = new_subset_name instance = context.create_instance(**instance_data) - instance.data["layers"] = context.data["layersData"] + + instance.data["layers"] = copy.deepcopy( + context.data["layersData"] + ) # Add ftrack family instance.data["families"].append("ftrack") From d69f70082daaad3b0ea0a7cd58ae61cfb1322267 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:56:53 +0100 Subject: [PATCH 058/264] added validation of layers visibility --- .../publish/validate_layers_visibility.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/validate_layers_visibility.py diff --git a/pype/plugins/tvpaint/publish/validate_layers_visibility.py b/pype/plugins/tvpaint/publish/validate_layers_visibility.py new file mode 100644 index 00000000000..74ef34169ed --- /dev/null +++ b/pype/plugins/tvpaint/publish/validate_layers_visibility.py @@ -0,0 +1,16 @@ +import pyblish.api + + +class ValidateLayersVisiblity(pyblish.api.InstancePlugin): + """Validate existence of renderPass layers.""" + + label = "Validate Layers Visibility" + order = pyblish.api.ValidatorOrder + families = ["review", "renderPass", "renderLayer"] + + def process(self, instance): + for layer in instance.data["layers"]: + if layer["visible"]: + return + + raise AssertionError("All layers of instance are not visible.") From b1cda0b527ea7d88a5415f72de5d05a9514b0788 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:57:33 +0100 Subject: [PATCH 059/264] do not skip not visible layers for render layer --- pype/plugins/tvpaint/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index 57602d96103..f7e8b96c0ba 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -103,7 +103,7 @@ def create_render_layer_instance(self, context, instance_data): group_id = instance_data["group_id"] group_layers = [] for layer in layers_data: - if layer["group_id"] == group_id and layer["visible"]: + if layer["group_id"] == group_id: group_layers.append(layer) if not group_layers: From d7992f165bb45a01a0c31f4374baeb78798c4e81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 17:57:49 +0100 Subject: [PATCH 060/264] turn of publishing based on visibility of layers --- pype/plugins/tvpaint/publish/collect_instances.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index f7e8b96c0ba..e03833b96b0 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -73,6 +73,14 @@ def process(self, context): if instance is None: continue + any_visible = False + for layer in instance.data["layers"]: + if layer["visible"]: + any_visible = True + break + + instance.data["publish"] = any_visible + instance.data["frameStart"] = context.data["frameStart"] instance.data["frameEnd"] = context.data["frameEnd"] From 50a59f227411ad7d7c82243d5b142c72c03121af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Mar 2021 20:43:29 +0100 Subject: [PATCH 061/264] extraction is a little bit faster --- pype/hosts/tvpaint/lib.py | 15 +- .../tvpaint/publish/extract_sequence.py | 203 ++++++++---------- 2 files changed, 104 insertions(+), 114 deletions(-) diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py index 8172392c7f2..4267129fe6b 100644 --- a/pype/hosts/tvpaint/lib.py +++ b/pype/hosts/tvpaint/lib.py @@ -1,9 +1,15 @@ from PIL import Image -def composite_images( - input_image_paths, output_filepath, scene_width, scene_height -): +def composite_images(input_image_paths, output_filepath): + """Composite images in order from passed list. + + Raises: + ValueError: When entered list is empty. + """ + if not input_image_paths: + raise ValueError("Nothing to composite.") + img_obj = None for image_filepath in input_image_paths: _img_obj = Image.open(image_filepath) @@ -11,7 +17,4 @@ def composite_images( img_obj = _img_obj else: img_obj.alpha_composite(_img_obj) - - if img_obj is None: - img_obj = Image.new("RGBA", (scene_width, scene_height), (0, 0, 0, 0)) img_obj.save(output_filepath) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 8f302cb746b..cec3e2edbca 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -47,8 +47,6 @@ def process(self, instance): family_lowered = instance.data["family"].lower() frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] - scene_width = instance.context.data["sceneWidth"] - scene_height = instance.context.data["sceneHeight"] filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -73,8 +71,8 @@ def process(self, instance): else: # Render output repre_files, thumbnail_fullpath = self.render( - filename_template, output_dir, filtered_layers, - frame_start, frame_end, scene_width, scene_height + filename_template, output_dir, frame_start, frame_end, + filtered_layers ) # Fill tags and new families @@ -138,19 +136,20 @@ def _get_filename_template(self, frame_end): def render_review( self, filename_template, output_dir, frame_start, frame_end ): - """ Export images from TVPaint. + """ Export images from TVPaint using `tv_savesequence` command. Args: filename_template (str): Filename template of an output. Template - should already contain extension. Template must contain - keyword argument `{frame}`. Extension in template must match - `save_mode`. - output_dir (list): List of layers to be exported. - frame_start (int): Starting frame from which export will begin. - frame_end (int): On which frame export will end. + should already contain extension. Template may contain only + keyword argument `{frame}` or index argument (for same value). + Extension in template must match `save_mode`. + output_dir (str): Directory where files will be stored. + first_frame (int): Starting frame from which export will begin. + last_frame (int): On which frame export will end. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( @@ -188,24 +187,23 @@ def render_review( return output, thumbnail_filepath def render( - self, filename_template, output_dir, layers, - frame_start, frame_end, scene_width, scene_height + self, filename_template, output_dir, frame_start, frame_end, layers ): """ Export images from TVPaint. Args: - save_mode (str): Argument for `tv_savemode` george script function. - More about save mode in documentation. filename_template (str): Filename template of an output. Template should already contain extension. Template may contain only keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. - layers (list): List of layers to be exported. + output_dir (str): Directory where files will be stored. first_frame (int): Starting frame from which export will begin. last_frame (int): On which frame export will end. + layers (list): List of layers to be exported. Retruns: - dict: Mapping frame to output filepath. + tuple: With 2 items first is list of filenames second is path to + thumbnail. """ self.log.debug("Preparing data for rendering.") @@ -232,40 +230,28 @@ def render( tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} - is_single_layer = len(sorted_positions) == 1 for position in sorted_positions: layer = layers_by_position[position] behavior = behavior_by_layer_id[layer["layer_id"]] - if is_single_layer: - _template = filename_template - else: - _template = tmp_filename_template - files_by_frames = self._render_layer( layer, - _template, + tmp_filename_template, output_dir, behavior, mark_in_index, mark_out_index ) - if is_single_layer: - output_filepaths = list(files_by_frames.values()) - else: - files_by_position[position] = files_by_frames + files_by_position[position] = files_by_frames - if not is_single_layer: - output_filepaths = self._composite_files( - files_by_position, - output_dir, - mark_in_index, - mark_out_index, - filename_template, - scene_width, - scene_height - ) - self._cleanup_tmp_files(files_by_position) + output_filepaths = self._composite_files( + files_by_position, + mark_in_index, + mark_out_index, + filename_template, + output_dir + ) + self._cleanup_tmp_files(files_by_position) thumbnail_src_filepath = None thumbnail_filepath = None @@ -300,9 +286,7 @@ def _render_layer( exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) - self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( - str(exposure_frames), layer["layer_id"], layer["name"] - )) + if frame_start_index not in exposure_frames: exposure_frames.append(frame_start_index) @@ -325,8 +309,8 @@ def _render_layer( # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) - self.log.debug("Rendering exposure frames {} of layer {}".format( - str(exposure_frames), layer_id + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( + str(exposure_frames), layer_id, layer["name"] )) # Let TVPaint render layer's image lib.execute_george_through_file("\n".join(george_script_lines)) @@ -404,31 +388,8 @@ def _fill_frame_by_pre_behavior( return if pre_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - mark_in_index, frame_start_index - )) - # Take size from first image and fill it with transparent color - first_filename = filename_template.format( - pos=layer_position, - frame=frame_start_index - ) - first_filepath = os.path.join(output_dir, first_filename) - empty_image_filepath = None - for frame_idx in reversed(range(mark_in_index, frame_start_index)): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(first_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif pre_behavior == "hold": # Keep first frame for whole time @@ -499,31 +460,8 @@ def _fill_frame_by_post_behavior( return if post_behavior == "none": - self.log.debug("Creating empty images for range {} - {}".format( - frame_end_index + 1, mark_out_index + 1 - )) - # Take size from last image and fill it with transparent color - last_filename = filename_template.format( - pos=layer_position, - frame=frame_end_index - ) - last_filepath = os.path.join(output_dir, last_filename) - empty_image_filepath = None - for frame_idx in range(frame_end_index + 1, mark_out_index + 1): - filename = filename_template.format( - pos=layer_position, - frame=frame_idx - ) - new_filepath = os.path.join(output_dir, filename) - if empty_image_filepath is None: - img_obj = Image.open(last_filepath) - painter = ImageDraw.Draw(img_obj) - painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) - img_obj.save(new_filepath) - empty_image_filepath = new_filepath - else: - self._copy_image(empty_image_filepath, new_filepath) - layer_files_by_frame[frame_idx] = new_filepath + # Empty frames are handled during `_composite_files` + pass elif post_behavior == "hold": # Keep first frame for whole time @@ -571,9 +509,16 @@ def _fill_frame_by_post_behavior( layer_files_by_frame[frame_idx] = new_filepath def _composite_files( - self, files_by_position, output_dir, frame_start, frame_end, - filename_template, scene_width, scene_height + self, files_by_position, frame_start, frame_end, + filename_template, output_dir ): + """Composite frames when more that one layer was exported. + + This method is used when more than one layer is rendered out so and + output should be composition of each frame of rendered layers. + Missing frames are filled with transparent images. + """ + self.log.debug("Preparing files for compisiting.") # Prepare paths to images by frames into list where are stored # in order of compositing. images_by_frame = {} @@ -582,7 +527,8 @@ def _composite_files( for position in sorted(files_by_position.keys(), reverse=True): position_data = files_by_position[position] if frame_idx in position_data: - images_by_frame[frame_idx].append(position_data[frame_idx]) + filepath = position_data[frame_idx] + images_by_frame[frame_idx].append(filepath) process_count = os.cpu_count() if process_count > 1: @@ -590,22 +536,41 @@ def _composite_files( processes = {} output_filepaths = [] + missing_frame_paths = [] + random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] - frame = frame_idx + 1 - - output_filename = filename_template.format(frame=frame) + output_filename = filename_template.format(frame=frame_idx + 1) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) - processes[frame_idx] = multiprocessing.Process( - target=composite_images, - args=( - image_filepaths, output_filepath, scene_width, scene_height + # Store information about missing frame and skip + if not image_filepaths: + missing_frame_paths.append(output_filepath) + continue + + # Just rename the file if is no need of compositing + if len(image_filepaths) == 1: + os.rename(image_filepaths[0], output_filepath) + + # Prepare process for compositing of images + else: + processes[frame_idx] = multiprocessing.Process( + target=composite_images, + args=(image_filepaths, output_filepath) ) - ) - # Wait until all processes are done + # Store path of random output image that will 100% exist after all + # multiprocessing as mockup for missing frames + if random_frame_path is None: + random_frame_path = output_filepath + + self.log.info( + "Running {} compositing processes - this mey take a while.".format( + len(processes) + ) + ) + # Wait until all compositing processes are done running_processes = {} while True: for idx in tuple(running_processes.keys()): @@ -627,14 +592,36 @@ def _composite_files( time.sleep(0.01) + self.log.debug( + "Creating transparent images for frames without render {}.".format( + str(missing_frame_paths) + ) + ) + # Fill the sequence with transparent frames + transparent_filepath = None + for filepath in missing_frame_paths: + if transparent_filepath is None: + img_obj = Image.open(random_frame_path) + painter = ImageDraw.Draw(img_obj) + painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0)) + img_obj.save(filepath) + transparent_filepath = filepath + else: + self._copy_image(transparent_filepath, filepath) return output_filepaths def _cleanup_tmp_files(self, files_by_position): + """Remove temporary files that were used for compositing.""" for data in files_by_position.values(): for filepath in data.values(): - os.remove(filepath) + if os.path.exists(filepath): + os.remove(filepath) def _copy_image(self, src_path, dst_path): + """Create a copy of an image. + + This was added to be able easier change copy method. + """ # Create hardlink of image instead of copying if possible if hasattr(os, "link"): os.link(src_path, dst_path) From 3365c32a4af71abd9938bb6d95ce6aadc39e1af7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 10:15:24 +0100 Subject: [PATCH 062/264] fix versions in standalone publisher --- pype/tools/standalonepublish/widgets/widget_family.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 00bc5b31f9c..5268611b383 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -339,11 +339,8 @@ def on_version_refresh(self): ).distinct("name") if versions: - versions = sorted( - [v for v in versions], - key=lambda ver: ver['name'] - ) - version = int(versions[-1]['name']) + 1 + versions = sorted(versions) + version = int(versions[-1]) + 1 self.version_spinbox.setValue(version) From 8777f13b45db0ff3791cd33601019c0aaaea612e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 10:31:11 +0100 Subject: [PATCH 063/264] background color is not defined in output definition but in specific preset file --- pype/plugins/global/publish/extract_review.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index e41ba9bfc43..085d23dc80f 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -472,11 +472,17 @@ def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) - bg_color = output_def.get("bg_color") - if bg_color: + use_bg_color = output_def.get("use_bg_color") + bg_color = ( + instance.context.data["presets"] + .get("tools", {}) + .get("extract_colors", {}) + .get("bg_color") + ) + if use_bg_color and bg_color: if not temp_data["input_allow_bg"]: self.log.info(( - "Outpud definition has defined BG color input was" + "Output definition has defined BG color input was" " resolved as does not support adding BG." )) elif not self.color_regex.match(bg_color): @@ -493,8 +499,6 @@ def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): "[bg][fg]overlay=format=auto" ]) - - # Add argument to override output file ffmpeg_output_args.append("-y") From e47aabfe6b698d6d5132521c18c562a191088e9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 10:54:07 +0100 Subject: [PATCH 064/264] extract thumbnail and jpeg use bg color from presets too --- pype/plugins/global/publish/extract_jpeg.py | 48 +++++++++++++++++- .../publish/extract_thumbnail.py | 49 ++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index af90d4366df..0c9ee14d9e9 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -1,4 +1,5 @@ import os +import re import pyblish.api import pype.api @@ -17,6 +18,9 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): families = ["imagesequence", "render", "render2d", "source"] enabled = False + alpha_exts = ["exr", "png", "dpx"] + color_regex = re.compile(r"^#[a-fA-F0-9]{6}$") + # presetable attribute ffmpeg_args = None @@ -93,7 +97,11 @@ def process(self, instance): # input file jpeg_items.append("-i {}".format(full_input_path)) # output arguments from presets - jpeg_items.extend(ffmpeg_args.get("output") or []) + output_args = self._prepare_output_args( + ffmpeg_args.get("output"), full_input_path, instance + ) + if output_args: + jpeg_items.extend(output_args) # If its a movie file, we just want one frame. if repre["ext"] == "mov": @@ -135,3 +143,41 @@ def process(self, instance): shutil.rmtree(decompressed_dir) instance.data["representations"] = representations_new + + def _prepare_output_args(self, output_args, full_input_path, instance): + output_args = output_args or [] + ext = os.path.splitext(full_input_path)[1].replace(".", "") + bg_color = ( + instance.context.data["presets"] + .get("tools", {}) + .get("extract_colors", {}) + .get("bg_color") + ) + if not bg_color or ext not in self.alpha_exts: + return output_args + + if not self.color_regex.match(bg_color): + self.log.warning(( + "Background color set in presets \"{}\" has invalid format." + ).format(bg_color)) + return output_args + + video_args_dentifiers = ["-vf", "-filter:v"] + video_filters = [] + for idx, arg in reversed(tuple(enumerate(output_args))): + for identifier in video_args_dentifiers: + if identifier in arg: + output_args.pop(idx) + arg = arg.replace(identifier, "").strip() + video_filters.append(arg) + + video_filters.extend([ + "split=2[bg][fg]", + "[bg]drawbox=c={}:replace=1:t=fill[bg]".format(bg_color), + "[bg][fg]overlay=format=auto" + ]) + + output_args.append( + "-filter:v {}".format(",".join(video_filters)) + ) + return output_args diff --git a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py index 533421e46d6..54a5aa931ed 100644 --- a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py +++ b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py @@ -1,4 +1,5 @@ import os +import re import tempfile import subprocess import pyblish.api @@ -18,6 +19,9 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] order = pyblish.api.ExtractorOrder + alpha_exts = ["exr", "png", "dpx"] + color_regex = re.compile(r"^#[a-fA-F0-9]{6}$") + # Presetable attribute ffmpeg_args = None @@ -88,7 +92,12 @@ def process(self, instance): # extract only single file jpeg_items.append("-vframes 1") - jpeg_items.extend(ffmpeg_args.get("output") or []) + # output arguments from presets + output_args = self._prepare_output_args( + ffmpeg_args.get("output"), full_input_path, instance + ) + if output_args: + jpeg_items.extend(output_args) # output file jpeg_items.append(full_thumbnail_path) @@ -124,3 +133,41 @@ def process(self, instance): self.log.info(f"New representation {representation}") instance.data["representations"].append(representation) + + def _prepare_output_args(self, output_args, full_input_path, instance): + output_args = output_args or [] + ext = os.path.splitext(full_input_path)[1].replace(".", "") + bg_color = ( + instance.context.data["presets"] + .get("tools", {}) + .get("extract_colors", {}) + .get("bg_color") + ) + if not bg_color or ext not in self.alpha_exts: + return output_args + + if not self.color_regex.match(bg_color): + self.log.warning(( + "Background color set in presets \"{}\" has invalid format." + ).format(bg_color)) + return output_args + + video_args_dentifiers = ["-vf", "-filter:v"] + video_filters = [] + for idx, arg in reversed(tuple(enumerate(output_args))): + for identifier in video_args_dentifiers: + if identifier in arg: + output_args.pop(idx) + arg = arg.replace(identifier, "").strip() + video_filters.append(arg) + + video_filters.extend([ + "split=2[bg][fg]", + "[bg]drawbox=c={}:replace=1:t=fill[bg]".format(bg_color), + "[bg][fg]overlay=format=auto" + ]) + + output_args.append( + "-filter:v {}".format(",".join(video_filters)) + ) + return output_args From 085edd24b6a53d9e832e1ac2bceab62ee1bd0501 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 11:13:42 +0100 Subject: [PATCH 065/264] added ability to turn off using bg color with presets --- pype/plugins/global/publish/extract_jpeg.py | 7 ++++++- .../standalonepublisher/publish/extract_thumbnail.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 0c9ee14d9e9..680a64dc897 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -22,6 +22,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): color_regex = re.compile(r"^#[a-fA-F0-9]{6}$") # presetable attribute + use_bg_color = True ffmpeg_args = None def process(self, instance): @@ -153,7 +154,11 @@ def _prepare_output_args(self, output_args, full_input_path, instance): .get("extract_colors", {}) .get("bg_color") ) - if not bg_color or ext not in self.alpha_exts: + if ( + not bg_color + or ext not in self.alpha_exts + or not self.use_bg_color + ): return output_args if not self.color_regex.match(bg_color): diff --git a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py index 54a5aa931ed..1da629f563c 100644 --- a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py +++ b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py @@ -23,6 +23,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): color_regex = re.compile(r"^#[a-fA-F0-9]{6}$") # Presetable attribute + use_bg_color = True ffmpeg_args = None def process(self, instance): @@ -143,7 +144,11 @@ def _prepare_output_args(self, output_args, full_input_path, instance): .get("extract_colors", {}) .get("bg_color") ) - if not bg_color or ext not in self.alpha_exts: + if ( + not bg_color + or ext not in self.alpha_exts + or not self.use_bg_color + ): return output_args if not self.color_regex.match(bg_color): From 59d4fa5d7a57108034ab31dfdf50b199eb437881 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 11:41:34 +0100 Subject: [PATCH 066/264] PS - added highlight with icon for publishable instances --- pype/plugins/photoshop/create/create_image.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index c1a7d92a2c1..7a779c8a0f6 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -73,5 +73,17 @@ def process(self): groups.append(group) for group in groups: + long_names = [] + if group.long_name: + for directory in group.long_name[::-1]: + name = directory.replace(stub.PUBLISH_ICON, '').\ + replace(stub.LOADED_ICON, '') + long_names.append(name) + self.data.update({"subset": "image" + group.name}) + self.data.update({"uuid": str(group.id)}) + self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) + # reusing existing group, need to rename afterwards + if not create_group: + stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) From 4db33fb07f27c124f7bafddf1cccfa891673895a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:03:58 +0100 Subject: [PATCH 067/264] PS - added highlight with icon for publishable instances Changed structure of metadata from {} to [] Added rename_layer method Switched to attr instead of namedtuple (same as in AE) --- .../stubs/photoshop_server_stub.py | 163 +++++++++++++++--- 1 file changed, 135 insertions(+), 28 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 9677fa61a84..79a486a20ca 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -4,16 +4,37 @@ Used anywhere solution is calling client methods. """ import json -from collections import namedtuple +import attr -class PhotoshopServerStub(): +@attr.s +class PSItem(object): + """ + Object denoting layer or group item in PS. Each item is created in + PS by any Loader, but contains same fields, which are being used + in later processing. + """ + # metadata + id = attr.ib() # id created by AE, could be used for querying + name = attr.ib() # name of item + group = attr.ib(default=None) # item type (footage, folder, comp) + parents = attr.ib(factory=list) + visible = attr.ib(default=True) + type = attr.ib(default=None) + # all imported elements, single for + members = attr.ib(factory=list) + long_name = attr.ib(default=None) + + +class PhotoshopServerStub: """ Stub for calling function on client (Photoshop js) side. Expects that client is already connected (started when avalon menu is opened). 'self.websocketserver.call' is used as async wrapper """ + PUBLISH_ICON = '\u2117 ' + LOADED_ICON = '\u25bc' def __init__(self): self.websocketserver = WebSocketServer.get_instance() @@ -34,7 +55,7 @@ def read(self, layer, layers_meta=None): """ Parses layer metadata from Headline field of active document Args: - layer: + Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' @@ -100,12 +145,26 @@ def get_layers(self): return self._to_records(res) + def get_layer(self, layer_id): + """ + Returns PSItem for specific 'layer_id' or None if not found + Args: + layer_id (string): unique layer id, stored in 'uuid' field + + Returns: + (PSItem) or None + """ + layers = self.get_layers() + for layer in layers: + if str(layer.id) == str(layer_id): + return layer + def get_layers_in_layers(self, layers): """ Return all layers that belong to layers (might be groups). Args: - layers : - Returns: + layers : + Returns: """ all_layers = self.get_layers() ret = [] @@ -123,28 +182,30 @@ def get_layers_in_layers(self, layers): def create_group(self, name): """ Create new group (eg. LayerSet) - Returns: + Returns: """ + enhanced_name = self.PUBLISH_ICON + name ret = self.websocketserver.call(self.client.call ('Photoshop.create_group', - name=name)) + name=enhanced_name)) # create group on PS is asynchronous, returns only id - layer = {"id": ret, "name": name, "group": True} - return namedtuple('Layer', layer.keys())(*layer.values()) + return PSItem(id=ret, name=name, group=True) def group_selected_layers(self, name): """ Group selected layers into new LayerSet (eg. group) Returns: (Layer) """ + enhanced_name = self.PUBLISH_ICON + name res = self.websocketserver.call(self.client.call ('Photoshop.group_selected_layers', - name=name) + name=enhanced_name) ) res = self._to_records(res) - if res: - return res.pop() + rec = res.pop() + rec.name = rec.name.replace(self.PUBLISH_ICON, '') + return rec raise ValueError("No group record returned") def get_selected_layers(self): @@ -253,6 +314,23 @@ def get_layers_metadata(self): layers_data = json.loads(res) except json.decoder.JSONDecodeError: pass + # format of metadata changed from {} to [] because of standardization + # keep current implementation logic as its working + if not isinstance(layers_data, dict): + temp_layers_meta = {} + for layer_meta in layers_data: + layer_id = layer_meta.get("uuid") or \ + (layer_meta.get("members")[0]) + temp_layers_meta[layer_id] = layer_meta + layers_data = temp_layers_meta + else: + # legacy version of metadata + for layer_id, layer_meta in layers_data.items(): + if layer_meta.get("schema") != "avalon-core:container-2.0": + layer_meta["uuid"] = str(layer_id) + else: + layer_meta["members"] = [str(layer_id)] + return layers_data def import_smart_object(self, path, layer_name): @@ -264,11 +342,14 @@ def import_smart_object(self, path, layer_name): layer_name (str): Unique layer name to differentiate how many times same smart object was loaded """ + enhanced_name = self.LOADED_ICON + layer_name res = self.websocketserver.call(self.client.call ('Photoshop.import_smart_object', - path=path, name=layer_name)) - - return self._to_records(res).pop() + path=path, name=enhanced_name)) + rec = self._to_records(res).pop() + if rec: + rec.name = rec.name.replace(self.LOADED_ICON, '') + return rec def replace_smart_object(self, layer, path, layer_name): """ @@ -277,13 +358,14 @@ def replace_smart_object(self, layer, path, layer_name): same smart object was loaded Args: - layer (namedTuple): Layer("id":XX, "name":"YY"..). + layer (PSItem): path (str): File to import. """ + enhanced_name = self.LOADED_ICON + layer_name self.websocketserver.call(self.client.call ('Photoshop.replace_smart_object', layer_id=layer.id, - path=path, name=layer_name)) + path=path, name=enhanced_name)) def delete_layer(self, layer_id): """ @@ -295,6 +377,18 @@ def delete_layer(self, layer_id): ('Photoshop.delete_layer', layer_id=layer_id)) + def rename_layer(self, layer_id, name): + """ + Renames specific layer by it's id. + Args: + layer_id (int): id of layer to delete + name (str): new name + """ + self.websocketserver.call(self.client.call + ('Photoshop.rename_layer', + layer_id=layer_id, + name=name)) + def remove_instance(self, instance_id): cleaned_data = {} @@ -313,19 +407,32 @@ def close(self): def _to_records(self, res): """ - Converts string json representation into list of named tuples for + Converts string json representation into list of PSItem for dot notation access to work. - Returns: - res(string): - json representation + Args: + res (string): valid json + Returns: + """ try: layers_data = json.loads(res) except json.decoder.JSONDecodeError: raise ValueError("Received broken JSON {}".format(res)) ret = [] - # convert to namedtuple to use dot donation - if isinstance(layers_data, dict): # TODO refactore + + # convert to AEItem to use dot donation + if isinstance(layers_data, dict): layers_data = [layers_data] for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) + # currently implemented and expected fields + item = PSItem(d.get('id'), + d.get('name'), + d.get('group'), + d.get('parents'), + d.get('visible'), + d.get('type'), + d.get('members'), + d.get('long_name')) + + ret.append(item) return ret From fe7cfb557bb4a350b0b9e62132642c84f54a929e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:32:45 +0100 Subject: [PATCH 068/264] PS - modified escaping PS in Windows opens path with / wrong. File cannot be saved as its name is full path --- pype/hooks/photoshop/prelaunch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/hooks/photoshop/prelaunch.py b/pype/hooks/photoshop/prelaunch.py index 64264e0fa09..1ae9a2df775 100644 --- a/pype/hooks/photoshop/prelaunch.py +++ b/pype/hooks/photoshop/prelaunch.py @@ -25,7 +25,8 @@ def execute(self, *args, env: dict = None) -> bool: workfile_path = self.get_workfile_path(env, self.host_name) # adding compulsory environment var for opening file - env["PYPE_WORKFILE_PATH"] = workfile_path.replace('\\', '/') + # windows must have \\ not / + env["PYPE_WORKFILE_PATH"] = workfile_path.replace('\\', '\\\\') return True From a28f841477eaf3660db5c60e1e281bf7a8b2e6f7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:49:45 +0100 Subject: [PATCH 069/264] AE - modified escaping AE in Windows opens path with / wrong. File cannot be saved as its name is full path --- pype/hooks/aftereffects/prelaunch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hooks/aftereffects/prelaunch.py b/pype/hooks/aftereffects/prelaunch.py index b19b6d651c3..b4fa2c76214 100644 --- a/pype/hooks/aftereffects/prelaunch.py +++ b/pype/hooks/aftereffects/prelaunch.py @@ -26,7 +26,7 @@ def execute(self, *args, env: dict = None) -> bool: # adding compulsory environment var for opening file # used in .bat launcher - env["PYPE_AE_WORKFILE_PATH"] = workfile_path.replace('\\', '/') + env["PYPE_AE_WORKFILE_PATH"] = workfile_path.replace('\\', '\\\\') return True From 9aeb6e4a45eecdc2b14b422cd016997d0df377b9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 14:11:10 +0100 Subject: [PATCH 070/264] PS - fix repair validator - limit duplication of family --- pype/plugins/photoshop/publish/validate_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 6130e583754..48f59012337 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -25,12 +25,12 @@ def process(self, context, plugin): for instance in instances: self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") + name = name.replace(instance.data["family"], '') instance[0].Name = name data = stub.read(instance[0]) data["subset"] = "image" + name stub.imprint(instance[0], data) - name = name.replace(instance.data["family"], '') name = stub.PUBLISH_ICON + name stub.rename_layer(instance.data["uuid"], name) From 04b8dfed4fecf8dbe8302de7d02c297d7fbe9649 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 13:00:45 +0100 Subject: [PATCH 071/264] PS - fix multiple group create instances --- .../websocket_server/stubs/photoshop_server_stub.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 79a486a20ca..5409120a650 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -224,11 +224,10 @@ def select_layers(self, layers): layers: Returns: None """ - layer_ids = [layer.id for layer in layers] - + layers_id = [str(lay.id) for lay in layers] self.websocketserver.call(self.client.call - ('Photoshop.get_layers', - layers=layer_ids) + ('Photoshop.select_layers', + layers=json.dumps(layers_id)) ) def get_active_document_full_name(self): From ddcc99876c1fe182ef66818e39f08a6cb6870bed Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:27:11 +0100 Subject: [PATCH 072/264] reversed condition of raising exception --- .../publish/collect_matching_asset.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index 48065c46620..089ca32561e 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -31,20 +31,20 @@ def process(self, instance): matching_asset_doc = asset_doc break - if matching_asset_doc: - instance.data["asset"] = matching_asset_doc["name"] - instance.data["assetEntity"] = matching_asset_doc - self.log.info( - f"Matching asset found: {pformat(matching_asset_doc)}" - ) - - else: + if not matching_asset_doc: # TODO better error message raise AssertionError(( "Filename \"{}\" does not match" " any name of asset documents in database for your selection." ).format(instance.data["source"])) + instance.data["asset"] = matching_asset_doc["name"] + instance.data["assetEntity"] = matching_asset_doc + + self.log.info( + f"Matching asset found: {pformat(matching_asset_doc)}" + ) + def selection_children_by_name(self, instance): storing_key = "childrenDocsForSelection" From c451ca6e91738e183ccf580120fdedffa64b93c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:27:33 +0100 Subject: [PATCH 073/264] added regex for version check in source filename --- .../standalonepublisher/publish/collect_matching_asset.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index 089ca32561e..cdb5403caa3 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -1,4 +1,5 @@ import os +import re import collections import pyblish.api from avalon import io @@ -16,6 +17,9 @@ class CollectMatchingAssetToInstance(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] families = ["background_batch"] + # Version regex to parse asset name and version from filename + version_regex = re.compile(r"^(.+)_v([0-9]+)$") + def process(self, instance): source_file = os.path.basename(instance.data["source"]).lower() self.log.info("Looking for asset document for file \"{}\"".format( From f26eed2594da00ce1cd99462cd6b008b15a24058 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:29:06 +0100 Subject: [PATCH 074/264] use the regex to try check if filename contain asset name --- .../publish/collect_matching_asset.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index cdb5403caa3..93551f1c4ca 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -28,7 +28,18 @@ def process(self, instance): asset_docs_by_name = self.selection_children_by_name(instance) + version_number = None + # Always first check if source filename is in assets matching_asset_doc = asset_docs_by_name.get(source_file) + if matching_asset_doc is None: + # Check if source file contain version in name + regex_result = self.version_regex.findall(source_file) + if regex_result: + asset_name, _version_number = regex_result[0] + matching_asset_doc = asset_docs_by_name.get(asset_name) + if matching_asset_doc: + version_number = int(_version_number) + if matching_asset_doc is None: for asset_name_low, asset_doc in asset_docs_by_name.items(): if asset_name_low in source_file: From 3ed3ecc430e487f3713640d8adc6a355d8462b7c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:29:17 +0100 Subject: [PATCH 075/264] store version to instance data if is found --- .../standalonepublisher/publish/collect_matching_asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index 93551f1c4ca..8a845e60d7f 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -55,6 +55,8 @@ def process(self, instance): instance.data["asset"] = matching_asset_doc["name"] instance.data["assetEntity"] = matching_asset_doc + if version_number is not None: + instance.data["version"] = version_number self.log.info( f"Matching asset found: {pformat(matching_asset_doc)}" From 734c76d1b1f609700f5bf261fb88e42b7c41fde8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:46:12 +0100 Subject: [PATCH 076/264] created copy of psb bulk plugin but for render mov batch --- .../publish/collect_mov_instances.py | 54 +++++++++++++++++++ .../publish/collect_psd_instances.py | 1 - 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 pype/plugins/standalonepublisher/publish/collect_mov_instances.py diff --git a/pype/plugins/standalonepublisher/publish/collect_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_mov_instances.py new file mode 100644 index 00000000000..0dcbd119d91 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/collect_mov_instances.py @@ -0,0 +1,54 @@ +import copy +import pyblish.api +from pprint import pformat + + +class CollectMovInstances(pyblish.api.InstancePlugin): + """Collect all available instances from render mov batch.""" + + label = "Collect Mov Instances" + order = pyblish.api.CollectorOrder + 0.489 + hosts = ["standalonepublisher"] + families = ["render_mov_batch"] + + # presets + subsets = { + "render": { + "task": "compositing", + "family": "render" + } + } + unchecked_by_default = [] + + def process(self, instance): + context = instance.context + asset_name = instance.data["asset"] + for subset_name, subset_data in self.subsets.items(): + instance_name = f"{asset_name}_{subset_name}" + task_name = subset_data["task"] + + # create new instance + new_instance = context.create_instance(instance_name) + + # add original instance data except name key + for key, value in instance.data.items(): + if key not in ["name"]: + # Make sure value is copy since value may be object which + # can be shared across all new created objects + new_instance.data[key] = copy.deepcopy(value) + + # add subset data from preset + new_instance.data.update(subset_data) + + new_instance.data["label"] = instance_name + new_instance.data["subset"] = subset_name + new_instance.data["task"] = task_name + + if subset_name in self.unchecked_by_default: + new_instance.data["publish"] = False + + self.log.info(f"Created new instance: {instance_name}") + self.log.debug(f"New instance data: {pformat(new_instance.data)}") + + # delete original instance + context.remove(instance) diff --git a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py index b5db4374738..11cedc30b9f 100644 --- a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py @@ -55,7 +55,6 @@ def process(self, instance): new_instance.data["subset"] = subset_name new_instance.data["task"] = task - if subset_name in self.unchecked_by_default: new_instance.data["publish"] = False From d0a4062efc3f65e113dae4adae88d0372188ae13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 14:52:15 +0100 Subject: [PATCH 077/264] added render_mov_batch to collect matching asset --- .../standalonepublisher/publish/collect_matching_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index 8a845e60d7f..16147dc7384 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -15,7 +15,7 @@ class CollectMatchingAssetToInstance(pyblish.api.InstancePlugin): label = "Collect Matching Asset to Instance" order = pyblish.api.CollectorOrder - 0.05 hosts = ["standalonepublisher"] - families = ["background_batch"] + families = ["background_batch", "render_mov_batch"] # Version regex to parse asset name and version from filename version_regex = re.compile(r"^(.+)_v([0-9]+)$") From f8d7b5b90ad7d850748d25697bec68be19584e32 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 15:29:54 +0100 Subject: [PATCH 078/264] single collector for batch instances --- .../publish/collect_mov_instances.py | 54 ------------------- .../publish/collect_psd_instances.py | 34 +++++++----- 2 files changed, 22 insertions(+), 66 deletions(-) delete mode 100644 pype/plugins/standalonepublisher/publish/collect_mov_instances.py diff --git a/pype/plugins/standalonepublisher/publish/collect_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_mov_instances.py deleted file mode 100644 index 0dcbd119d91..00000000000 --- a/pype/plugins/standalonepublisher/publish/collect_mov_instances.py +++ /dev/null @@ -1,54 +0,0 @@ -import copy -import pyblish.api -from pprint import pformat - - -class CollectMovInstances(pyblish.api.InstancePlugin): - """Collect all available instances from render mov batch.""" - - label = "Collect Mov Instances" - order = pyblish.api.CollectorOrder + 0.489 - hosts = ["standalonepublisher"] - families = ["render_mov_batch"] - - # presets - subsets = { - "render": { - "task": "compositing", - "family": "render" - } - } - unchecked_by_default = [] - - def process(self, instance): - context = instance.context - asset_name = instance.data["asset"] - for subset_name, subset_data in self.subsets.items(): - instance_name = f"{asset_name}_{subset_name}" - task_name = subset_data["task"] - - # create new instance - new_instance = context.create_instance(instance_name) - - # add original instance data except name key - for key, value in instance.data.items(): - if key not in ["name"]: - # Make sure value is copy since value may be object which - # can be shared across all new created objects - new_instance.data[key] = copy.deepcopy(value) - - # add subset data from preset - new_instance.data.update(subset_data) - - new_instance.data["label"] = instance_name - new_instance.data["subset"] = subset_name - new_instance.data["task"] = task_name - - if subset_name in self.unchecked_by_default: - new_instance.data["publish"] = False - - self.log.info(f"Created new instance: {instance_name}") - self.log.debug(f"New instance data: {pformat(new_instance.data)}") - - # delete original instance - context.remove(instance) diff --git a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py index 11cedc30b9f..09ec78af39b 100644 --- a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py @@ -11,21 +11,29 @@ class CollectPsdInstances(pyblish.api.InstancePlugin): label = "Collect Psd Instances" order = pyblish.api.CollectorOrder + 0.489 hosts = ["standalonepublisher"] - families = ["background_batch"] + families = ["background_batch", "render_mov_batch"] # presets subsets = { - "backgroundLayout": { - "task": "background", - "family": "backgroundLayout" + "background_batch": { + "backgroundLayout": { + "task": "background", + "family": "backgroundLayout" + }, + "backgroundComp": { + "task": "background", + "family": "backgroundComp" + }, + "workfileBackground": { + "task": "background", + "family": "workfile" + } }, - "backgroundComp": { - "task": "background", - "family": "backgroundComp" - }, - "workfileBackground": { - "task": "background", - "family": "workfile" + "render_mov_batch": { + "renderCompositingDefault": { + "task": "Compositing", + "family": "render" + } } } unchecked_by_default = [] @@ -34,7 +42,9 @@ def process(self, instance): context = instance.context asset_data = instance.data["assetEntity"] asset_name = instance.data["asset"] - for subset_name, subset_data in self.subsets.items(): + family = instance.data["family"] + + for subset_name, subset_data in self.subsets[family].items(): instance_name = f"{asset_name}_{subset_name}" task = subset_data.get("task", "background") From 4eb8a034b8524b5fdc56bfe08a9bb8eea34038ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 15:30:15 +0100 Subject: [PATCH 079/264] renamed collect psd instances to collect batch instances --- .../{collect_psd_instances.py => collect_batch_instances.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pype/plugins/standalonepublisher/publish/{collect_psd_instances.py => collect_batch_instances.py} (96%) diff --git a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py similarity index 96% rename from pype/plugins/standalonepublisher/publish/collect_psd_instances.py rename to pype/plugins/standalonepublisher/publish/collect_batch_instances.py index 09ec78af39b..3e83a7dcace 100644 --- a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py @@ -3,12 +3,12 @@ from pprint import pformat -class CollectPsdInstances(pyblish.api.InstancePlugin): +class CollectBatchInstances(pyblish.api.InstancePlugin): """ Collect all available instances from psd batch. """ - label = "Collect Psd Instances" + label = "Collect Batch Instances" order = pyblish.api.CollectorOrder + 0.489 hosts = ["standalonepublisher"] families = ["background_batch", "render_mov_batch"] From 38f18a7936c944b6d5cfb5e89b11439c462aa4d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:15:03 +0100 Subject: [PATCH 080/264] fixed asset name lookup --- .../publish/collect_matching_asset.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index 16147dc7384..f1686dc42f5 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -21,19 +21,23 @@ class CollectMatchingAssetToInstance(pyblish.api.InstancePlugin): version_regex = re.compile(r"^(.+)_v([0-9]+)$") def process(self, instance): - source_file = os.path.basename(instance.data["source"]).lower() + source_filename = self.get_source_filename(instance) self.log.info("Looking for asset document for file \"{}\"".format( - instance.data["source"] + source_filename )) + asset_name = os.path.splitext(source_filename)[0].lower() asset_docs_by_name = self.selection_children_by_name(instance) version_number = None # Always first check if source filename is in assets - matching_asset_doc = asset_docs_by_name.get(source_file) + matching_asset_doc = asset_docs_by_name.get(asset_name) if matching_asset_doc is None: # Check if source file contain version in name - regex_result = self.version_regex.findall(source_file) + self.log.debug(( + "Asset doc by \"{}\" was not found trying version regex." + ).format(asset_name)) + regex_result = self.version_regex.findall(asset_name) if regex_result: asset_name, _version_number = regex_result[0] matching_asset_doc = asset_docs_by_name.get(asset_name) @@ -42,16 +46,19 @@ def process(self, instance): if matching_asset_doc is None: for asset_name_low, asset_doc in asset_docs_by_name.items(): - if asset_name_low in source_file: + if asset_name_low in asset_name: matching_asset_doc = asset_doc break if not matching_asset_doc: + self.log.debug("Available asset names {}".format( + str(list(asset_docs_by_name.keys())) + )) # TODO better error message raise AssertionError(( "Filename \"{}\" does not match" " any name of asset documents in database for your selection." - ).format(instance.data["source"])) + ).format(source_filename)) instance.data["asset"] = matching_asset_doc["name"] instance.data["assetEntity"] = matching_asset_doc @@ -62,6 +69,25 @@ def process(self, instance): f"Matching asset found: {pformat(matching_asset_doc)}" ) + def get_source_filename(self, instance): + if instance.data["family"] == "background_batch": + return os.path.basename(instance.data["source"]) + + if len(instance.data["representations"]) != 1: + raise ValueError(( + "Implementation bug: Instance data contain" + " more than one representation." + )) + + repre = instance.data["representations"][0] + repre_files = repre["files"] + if not isinstance(repre_files, str): + raise ValueError(( + "Implementation bug: Instance's representation contain" + " unexpected value (expected single file). {}" + ).format(str(repre_files))) + return repre_files + def selection_children_by_name(self, instance): storing_key = "childrenDocsForSelection" From 311d15e714db295e5781a315b168ec4ecb14e6ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:15:21 +0100 Subject: [PATCH 081/264] added mov batch specific method --- .../publish/collect_context.py | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 9dbeec93fbd..f6c102f2cb9 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -48,8 +48,12 @@ def process(self, context): self.log.debug(f"_ in_data: {pformat(in_data)}") # exception for editorial - if in_data["family"] in ["editorial", "background_batch"]: + if in_data["family"] == "render_mov_batch": + in_data_list = self.prepare_mov_batch_instances(context, in_data) + + elif in_data["family"] in ["editorial", "background_batch"]: in_data_list = self.multiple_instances(context, in_data) + else: in_data_list = [in_data] @@ -112,6 +116,73 @@ def multiple_instances(self, context, in_data): return in_data_list + def prepare_mov_batch_instances(self, context, in_data): + """Copy of `multiple_instances` method. + + Method was copied because `batch_extensions` is used in + `multiple_instances` but without any family filtering. Since usage + of the filtering is unknown and modification of that part may break + editorial or PSD batch publishing it was decided to create a copy with + this family specific filtering. + + TODO: + - Merge logic with `multiple_instances` method. + """ + self.log.info("Preparing data for mov batch processing.") + in_data_list = [] + + representations = in_data.pop("representations") + for repre in representations: + self.log.debug("Processing representation with files {}".format( + str(repre["files"]) + )) + ext = repre["ext"][1:] + # Skip files that are not available for mov batch publishing + # TODO add dynamic expected extensions by family from `in_data` + # - with this modification it would be possible to use only + # `multiple_instances` method + expected_exts = ["mov"] + if ext not in expected_exts: + self.log.warning(( + "Skipping representation." + " Does not match expected extensions <{}>. {}" + ).format(", ".join(expected_exts), str(repre))) + continue + + # Delete key from representation + # QUESTION is this needed in mov batch processing? + delete_repr_keys = ["frameStart", "frameEnd"] + for key in delete_repr_keys: + repre.pop(key, None) + + files = repre["files"] + # Convert files to list if it isnt + if not isinstance(files, (tuple, list)): + files = [files] + + # Loop through files and create new instance per each file + for filename in files: + # Create copy of representation and change it's files and name + new_repre = copy.deepcopy(repre) + new_repre["files"] = filename + new_repre["name"] = ext + + # Prepare new subset name (temporary name) + # - subset name will be changed in batch specific plugins + new_subset_name = "{}{}".format( + in_data["subset"], + os.path.basename(filename) + ) + # Create copy of instance data as new instance and pass in new + # representation + in_data_copy = copy.deepcopy(in_data) + in_data_copy["representations"] = [new_repre] + in_data_copy["subset"] = new_subset_name + + in_data_list.append(in_data_copy) + + return in_data_list + def create_instance(self, context, in_data): subset = in_data["subset"] From 9216fcbba22c6fd0729efbc14e6c3ecc1f1f2bd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:31:05 +0100 Subject: [PATCH 082/264] dont remove frame start and frame end from instance data --- .../standalonepublisher/publish/collect_context.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index f6c102f2cb9..318335a6d20 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -123,7 +123,8 @@ def prepare_mov_batch_instances(self, context, in_data): `multiple_instances` but without any family filtering. Since usage of the filtering is unknown and modification of that part may break editorial or PSD batch publishing it was decided to create a copy with - this family specific filtering. + this family specific filtering. Also "frameStart" and "frameEnd" keys + are removed from instance which is needed for this processing. TODO: - Merge logic with `multiple_instances` method. @@ -149,12 +150,6 @@ def prepare_mov_batch_instances(self, context, in_data): ).format(", ".join(expected_exts), str(repre))) continue - # Delete key from representation - # QUESTION is this needed in mov batch processing? - delete_repr_keys = ["frameStart", "frameEnd"] - for key in delete_repr_keys: - repre.pop(key, None) - files = repre["files"] # Convert files to list if it isnt if not isinstance(files, (tuple, list)): From 4e2323671e8f0f771a3a87420616c93257b7b026 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:31:17 +0100 Subject: [PATCH 083/264] removed unused variable --- .../standalonepublisher/publish/collect_batch_instances.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py index 3e83a7dcace..5820fc62477 100644 --- a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py @@ -31,7 +31,7 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): }, "render_mov_batch": { "renderCompositingDefault": { - "task": "Compositing", + "task": "compositing", "family": "render" } } @@ -40,7 +40,6 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): def process(self, instance): context = instance.context - asset_data = instance.data["assetEntity"] asset_name = instance.data["asset"] family = instance.data["family"] From 53f300191e96be4ff93508985dd4ca5087022977 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:59:25 +0100 Subject: [PATCH 084/264] added message before logged values --- pype/plugins/global/publish/integrate_new.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 93e5dc9c2fa..49a8e300272 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -799,7 +799,9 @@ def template_name_from_instance(self, instance): matching_profiles = {} highest_value = -1 - self.log.info(self.template_name_profiles) + self.log.debug( + "Template name profiles:\n{}".format(self.template_name_profiles) + ) for name, filters in self.template_name_profiles.items(): value = 0 families = filters.get("families") From b5d181ea692b7358e1ad2c8f2ac768cf03d6adc7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 16:59:51 +0100 Subject: [PATCH 085/264] added definitions of default task names --- .../publish/collect_batch_instances.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py index 5820fc62477..94574ad19cf 100644 --- a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py @@ -14,6 +14,10 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): families = ["background_batch", "render_mov_batch"] # presets + default_subset_task = { + "background_batch": "background", + "render_mov_batch": "compositing" + } subsets = { "background_batch": { "backgroundLayout": { @@ -43,9 +47,10 @@ def process(self, instance): asset_name = instance.data["asset"] family = instance.data["family"] + default_task_name = self.default_subset_task.get(family) for subset_name, subset_data in self.subsets[family].items(): instance_name = f"{asset_name}_{subset_name}" - task = subset_data.get("task", "background") + task_name = subset_data.get("task") or default_task_name # create new instance new_instance = context.create_instance(instance_name) @@ -62,7 +67,7 @@ def process(self, instance): new_instance.data["label"] = f"{instance_name}" new_instance.data["subset"] = subset_name - new_instance.data["task"] = task + new_instance.data["task"] = task_name if subset_name in self.unchecked_by_default: new_instance.data["publish"] = False From e5f346b33143266217707e17c513476bf1567718 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 18:13:35 +0100 Subject: [PATCH 086/264] fix variable override --- .../standalonepublisher/publish/collect_matching_asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py index f1686dc42f5..0d629b1b449 100644 --- a/pype/plugins/standalonepublisher/publish/collect_matching_asset.py +++ b/pype/plugins/standalonepublisher/publish/collect_matching_asset.py @@ -39,8 +39,8 @@ def process(self, instance): ).format(asset_name)) regex_result = self.version_regex.findall(asset_name) if regex_result: - asset_name, _version_number = regex_result[0] - matching_asset_doc = asset_docs_by_name.get(asset_name) + _asset_name, _version_number = regex_result[0] + matching_asset_doc = asset_docs_by_name.get(_asset_name) if matching_asset_doc: version_number = int(_version_number) From b00a62450fe649025a8156ec9107c169001ed3d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 12:34:02 +0100 Subject: [PATCH 087/264] replaced `override_event` function with specific function replacement in maya implementation --- pype/hosts/maya/__init__.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/pype/hosts/maya/__init__.py b/pype/hosts/maya/__init__.py index afd2e399eb4..6eeed8cef01 100644 --- a/pype/hosts/maya/__init__.py +++ b/pype/hosts/maya/__init__.py @@ -45,7 +45,7 @@ def install(): avalon.before("save", on_before_save) log.info("Overriding existing event 'taskChanged'") - override_event("taskChanged", on_task_changed) + override_task_change_event() log.info("Setting default family states for loader..") avalon.data["familiesStateToggled"] = ["imagesequence"] @@ -59,22 +59,19 @@ def uninstall(): menu.uninstall() -def override_event(event, callback): - """ - Override existing event callback - Args: - event (str): name of the event - callback (function): callback to be triggered - - Returns: - None - - """ +def override_task_change_event(): + """Override taskChanged event callback in avalon's maya implementation.""" - ref = weakref.WeakSet() - ref.add(callback) + event_name = "taskChanged" + callbacks = pipeline._registered_event_handlers.get(event_name) + if callbacks: + # Remove callback from `avalon.maya.pipeline` + from avalon.maya.pipeline import _on_task_changed - pipeline._registered_event_handlers[event] = ref + if _on_task_changed in callbacks: + callbacks.remove(_on_task_changed) + # Register pype's callback + avalon.on(event_name, on_task_changed) def on_init(_): From 85b8ef5029885574e9e6ed8b0e439d24a2d199b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 15:37:36 +0100 Subject: [PATCH 088/264] cleanup plugin can skip filpaths defined in context data --- pype/plugins/global/publish/cleanup.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index 5fded85ccbc..b8104078d9f 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -37,9 +37,16 @@ def process(self, instance): ) ) + _skip_cleanup_filepaths = instance.context.data.get( + "skipCleanupFilepaths" + ) or [] + skip_cleanup_filepaths = set() + for path in _skip_cleanup_filepaths: + skip_cleanup_filepaths.add(os.path.normpath(path)) + if self.remove_temp_renders: self.log.info("Cleaning renders new...") - self.clean_renders(instance) + self.clean_renders(instance, skip_cleanup_filepaths) if [ef for ef in self.exclude_families if instance.data["family"] in ef]: @@ -65,7 +72,7 @@ def process(self, instance): self.log.info("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) - def clean_renders(self, instance): + def clean_renders(self, instance, skip_cleanup_filepaths): transfers = instance.data.get("transfers", list()) current_families = instance.data.get("families", list()) @@ -84,6 +91,12 @@ def clean_renders(self, instance): # add dest dir into clearing dir paths (regex paterns) transfers_dirs.append(os.path.dirname(dest)) + if src in skip_cleanup_filepaths: + self.log.debug(( + "Source file is marked to be skipped in cleanup. {}" + ).format(src)) + continue + if os.path.normpath(src) != os.path.normpath(dest): if instance_family == 'render' or 'render' in current_families: self.log.info("Removing src: `{}`...".format(src)) @@ -116,6 +129,9 @@ def clean_renders(self, instance): # remove all files which match regex patern for f in files: + if os.path.normpath(f) in skip_cleanup_filepaths: + continue + for p in self.paterns: patern = re.compile(p) if not patern.findall(f): From cecfd0e3755dcc87e04918d1c965f393937ce49e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 15:38:20 +0100 Subject: [PATCH 089/264] all filepaths from standalone publisher are added to list of filepaths that won't be deleted during cleanup --- .../publish/collect_context.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 318335a6d20..8930bedab8e 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -45,8 +45,9 @@ def process(self, context): with open(input_json_path, "r") as f: in_data = json.load(f) - self.log.debug(f"_ in_data: {pformat(in_data)}") + self.log.debug(f"_ in_data: {pformat(in_data)}") + self.add_files_to_ignore_cleanup(in_data, context) # exception for editorial if in_data["family"] == "render_mov_batch": in_data_list = self.prepare_mov_batch_instances(context, in_data) @@ -63,6 +64,21 @@ def process(self, context): # create instance self.create_instance(context, in_data) + def add_files_to_ignore_cleanup(self, in_data, context): + all_filepaths = context.data.get("skipCleanupFilepaths") or [] + for repre in in_data["representations"]: + files = repre["files"] + if not isinstance(files, list): + files = [files] + + dirpath = repre["stagingDir"] + for filename in files: + filepath = os.path.normpath(os.path.join(dirpath, filename)) + if filepath not in all_filepaths: + all_filepaths.append(filepath) + + context.data["skipCleanupFilepaths"] = all_filepaths + def multiple_instances(self, context, in_data): # avoid subset name duplicity if not context.data.get("subsetNamesCheck"): From a82f4d4e4f14199deaefb20a4834a207196495de Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Mar 2021 16:24:10 +0100 Subject: [PATCH 090/264] fix(nuke): class parent to correct class name --- pype/plugins/nuke/create/create_backdrop.py | 2 +- pype/plugins/nuke/create/create_camera.py | 2 +- pype/plugins/nuke/create/create_gizmo.py | 2 +- pype/plugins/nuke/create/create_read.py | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pype/plugins/nuke/create/create_backdrop.py b/pype/plugins/nuke/create/create_backdrop.py index 72f4417b4cf..078151532ff 100644 --- a/pype/plugins/nuke/create/create_backdrop.py +++ b/pype/plugins/nuke/create/create_backdrop.py @@ -3,7 +3,7 @@ import nuke -class CreateBackdrop(plugin.Creator): +class CreateBackdrop(plugin.PypeCreator): """Add Publishable Backdrop""" name = "nukenodes" diff --git a/pype/plugins/nuke/create/create_camera.py b/pype/plugins/nuke/create/create_camera.py index 3c5a50cfb8f..178e661fc1d 100644 --- a/pype/plugins/nuke/create/create_camera.py +++ b/pype/plugins/nuke/create/create_camera.py @@ -3,7 +3,7 @@ import nuke -class CreateCamera(plugin.Creator): +class CreateCamera(plugin.PypeCreator): """Add Publishable Backdrop""" name = "camera" diff --git a/pype/plugins/nuke/create/create_gizmo.py b/pype/plugins/nuke/create/create_gizmo.py index 2b7edaabcbf..a9ed11eadc1 100644 --- a/pype/plugins/nuke/create/create_gizmo.py +++ b/pype/plugins/nuke/create/create_gizmo.py @@ -3,7 +3,7 @@ import nuke -class CreateGizmo(plugin.Creator): +class CreateGizmo(plugin.PypeCreator): """Add Publishable "gizmo" group The name is symbolically gizmo as presumably diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index b2b857e0f3d..aab611e19a6 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -1,12 +1,10 @@ from collections import OrderedDict import avalon.nuke -from pype import api as pype from pype.hosts.nuke import plugin - import nuke -class CrateRead(plugin.Creator): +class CrateRead(plugin.PypeCreator): # change this to template preset name = "ReadCopy" label = "Create Read Copy" From 6114627329e622f7c02f22cab787ca76a6108cd5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 Mar 2021 12:37:47 +0100 Subject: [PATCH 091/264] fix(nuke): AvalonTab as default open in write nodes --- pype/hosts/nuke/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 6764a21f37c..8ad2030f702 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -418,6 +418,9 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): # Deadline tab. add_deadline_tab(GN) + # open the AvalonTab as default + GN["AvalonTab"].setFlag(0) + # set tile color tile_color = _data.get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) From 6f772f84bcee451d68611ba9edff28c4184a2203 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 Mar 2021 16:00:14 +0100 Subject: [PATCH 092/264] feat(nuke): open last work-file version at start of a session --- pype/hosts/nuke/__init__.py | 104 +++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 18 deletions(-) diff --git a/pype/hosts/nuke/__init__.py b/pype/hosts/nuke/__init__.py index bc17b1e2b61..6e468b5a0d4 100644 --- a/pype/hosts/nuke/__init__.py +++ b/pype/hosts/nuke/__init__.py @@ -1,9 +1,14 @@ import os import sys -import logging import nuke - -from avalon import api as avalon +import getpass +from pype.api import Anatomy +from avalon.nuke import ( + save_file, open_file +) +from avalon import ( + io, api +) from avalon.tools import workfiles from pyblish import api as pyblish from pype.hosts.nuke import menu @@ -63,12 +68,12 @@ def install(): log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) - avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) - avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + api.register_plugin_path(api.Loader, LOAD_PATH) + api.register_plugin_path(api.Creator, CREATE_PATH) + api.register_plugin_path(api.InventoryAction, INVENTORY_PATH) # Register Avalon event for workfiles loading. - avalon.on("workio.open_file", lib.check_inventory_versions) + api.on("workio.open_file", lib.check_inventory_versions) pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) workfile_settings = lib.WorkfileSettings() @@ -80,37 +85,100 @@ def install(): "gizmo" ] - avalon.data["familiesStateDefault"] = False - avalon.data["familiesStateToggled"] = family_states - - # Workfiles. - launch_workfiles = os.environ.get("WORKFILES_STARTUP") - - if launch_workfiles: - nuke.addOnCreate(launch_workfiles_app, nodeClass="Root") + api.data["familiesStateDefault"] = False + api.data["familiesStateToggled"] = family_states # Set context settings. nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root") nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") - + nuke.addOnCreate(open_last_workfile, nodeClass="Root") + nuke.addOnCreate(launch_workfiles_app, nodeClass="Root") menu.install() def launch_workfiles_app(): '''Function letting start workfiles after start of host ''' + if not os.environ.get("WORKFILES_STARTUP"): + return + if not self.workfiles_launched: self.workfiles_launched = True workfiles.show(os.environ["AVALON_WORKDIR"]) +def open_last_workfile(): + if not os.getenv("WORKFILE_OPEN_LAST_VERSION"): + return + + log.info("Opening last workfile...") + last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") + if not last_workfile_path: + root_path = api.registered_root() + workdir = os.environ["AVALON_WORKDIR"] + task = os.environ["AVALON_TASK"] + project_name = os.environ["AVALON_PROJECT"] + asset_name = os.environ["AVALON_ASSET"] + + io.install() + project_entity = io.find_one({ + "type": "project", + "name": project_name + }) + assert project_entity, ( + "Project '{0}' was not found." + ).format(project_name) + + asset_entity = io.find_one({ + "type": "asset", + "name": asset_name, + "parent": project_entity["_id"] + }) + assert asset_entity, ( + "No asset found by the name '{0}' in project '{1}'" + ).format(asset_name, project_name) + + project_name = project_entity["name"] + + anatomy = Anatomy() + file_template = anatomy.templates["work"]["file"] + extensions = api.HOST_WORKFILE_EXTENSIONS.get("nuke") + + # create anatomy data for building file name + workdir_data = { + "root": root_path, + "project": { + "name": project_name, + "code": project_entity["data"].get("code") + }, + "asset": asset_entity["name"], + "task": task, + "version": 1, + "user": os.environ.get("PYPE_USERNAME") or getpass.getuser(), + "ext": extensions[0] + } + + # create last workfile name + last_workfile_path = api.last_workfile( + workdir, file_template, workdir_data, extensions, True + ) + if not os.path.exists(last_workfile_path): + save_file(last_workfile_path) + else: + # to avoid looping of the callback, remove it! + nuke.removeOnCreate(open_last_workfile, nodeClass="Root") + + # open workfile + open_file(last_workfile_path) + + def uninstall(): '''Uninstalling host's integration ''' log.info("Deregistering Nuke plug-ins..") pyblish.deregister_plugin_path(PUBLISH_PATH) - avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + api.deregister_plugin_path(api.Loader, LOAD_PATH) + api.deregister_plugin_path(api.Creator, CREATE_PATH) pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) From 054f260f38ed69d41da7aa66598d56f4faf5c34f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Mar 2021 16:15:18 +0100 Subject: [PATCH 093/264] added fix of repre extension with warning to integrator --- pype/plugins/global/publish/integrate_new.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 93e5dc9c2fa..f428e12152f 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -334,6 +334,17 @@ def register(self, instance): sequence_repre = isinstance(files, list) repre_context = None + + ext = repre["ext"] + if ext.startswith("."): + self.log.warning(( + "Implementaion warning: <\"{}\">" + " Representation's extension stored under \"ext\" key " + " started with dot (\"{}\")." + ).format(repre["name"], ext)) + ext = ext[1:] + repre["ext"] = ext + if sequence_repre: self.log.debug( "files: {}".format(files)) From 36e5f9127fa8fdc1402a30d82baa4c3e71539a00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Mar 2021 16:34:19 +0100 Subject: [PATCH 094/264] removed white spaces --- pype/plugins/global/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index f428e12152f..b739a6f3649 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -344,7 +344,7 @@ def register(self, instance): ).format(repre["name"], ext)) ext = ext[1:] repre["ext"] = ext - + if sequence_repre: self.log.debug( "files: {}".format(files)) From 90f430359033a969589741fdb516e93ce53f212b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 Mar 2021 17:29:32 +0100 Subject: [PATCH 095/264] feat(nuke): pype menu shortcuts form presets --- pype/hosts/nuke/menu.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pype/hosts/nuke/menu.py b/pype/hosts/nuke/menu.py index b1ef7f47c4e..10a51bfeca6 100644 --- a/pype/hosts/nuke/menu.py +++ b/pype/hosts/nuke/menu.py @@ -1,9 +1,10 @@ +import os import nuke from avalon.api import Session from pype.hosts.nuke import lib from ...lib import BuildWorkfile -from pype.api import Logger +from pype.api import Logger, config log = Logger().get_logger(__name__, "nuke") @@ -11,7 +12,9 @@ def install(): menubar = nuke.menu("Nuke") menu = menubar.findItem(Session["AVALON_LABEL"]) + workfile_settings = lib.WorkfileSettings + # replace reset resolution from avalon core to pype's name = "Reset Resolution" new_name = "Set Resolution" @@ -68,6 +71,9 @@ def install(): ) log.debug("Adding menu item: {}".format(name)) + # adding shortcuts + add_shortcuts_from_presets() + def uninstall(): @@ -77,3 +83,21 @@ def uninstall(): for item in menu.items(): log.info("Removing menu item: {}".format(item.name())) menu.removeItem(item.name()) + + +def add_shortcuts_from_presets(): + menubar = nuke.menu("Nuke") + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + nuke_presets = presets.get("nuke", {}) + if nuke_presets.get("menu"): + for menu_name, menuitems in nuke_presets.get("menu").items(): + menu = menubar.findItem(menu_name) + for mitem_name, shortcut in menuitems.items(): + log.info("Adding Shortcut `{}` to `{}`".format( + shortcut, mitem_name + )) + try: + menuitem = menu.findItem(mitem_name) + menuitem.setShortcut(shortcut) + except AttributeError as e: + log.error(e) From 61c07cc2e362207c7bfb670a712f7c6eb6297230 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 12 Mar 2021 23:31:02 +0100 Subject: [PATCH 096/264] changelog --- .github_changelog_generator | 6 ++- CHANGELOG.md | 97 ++++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index 93792be5ca1..eba22cfd39f 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -3,6 +3,8 @@ exclude-labels=duplicate,question,invalid,wontfix,weekly-digest author=False unreleased=True since-tag=2.13.6 -release-branch=master enhancement-label=**Enhancements:** -issues=False \ No newline at end of file +release-branch=2.x/develop +issues=False +exclude-tags-regex=3.\d.\d.* +future-release=2.15.4 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cc23744d660..bd0e9ef8194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,89 @@ # Changelog -## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-08) +## [2.15.4-prerelease](https://github.com/pypeclub/pype/tree/2.15.4) (2021-03-12) -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0-rc1...2.15.0) +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.15.4) **Enhancements:** +- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127) +- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124) +- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106) +- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080) +- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078) +- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072) + +**Fixed bugs:** + +- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125) +- Nuke: AvalonTab as default for write node [\#1114](https://github.com/pypeclub/pype/pull/1114) +- Nuke: class parent to correct class name [\#1109](https://github.com/pypeclub/pype/pull/1109) +- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090) +- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082) + +**Merged pull requests:** + +- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) + +## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3) + +**Fixed bugs:** + +- Fix order of wrappers in settings gui [\#1052](https://github.com/pypeclub/pype/pull/1052) +- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085) +- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059) +- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057) +- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046) + +**Merged pull requests:** + +- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) +- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) + +## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2) + +**Enhancements:** + +- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013) + +**Fixed bugs:** + +- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040) +- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036) +- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031) + +## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1) + +**Enhancements:** + +- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445) + +**Fixed bugs:** + +- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) + +**Merged pull requests:** + +- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011) +- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006) + +## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0) + +**Enhancements:** + +- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926) +- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) - Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993) - Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986) +- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981) - PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965) - AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944) - PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941) @@ -20,9 +96,8 @@ - Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840) - Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824) - DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795) +- Unreal: add 4.26 support [\#746](https://github.com/pypeclub/pype/pull/746) - Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695) -- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) -- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981) **Fixed bugs:** @@ -48,6 +123,10 @@ - Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831) - Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822) +**Merged pull requests:** + +- Ftrack plugins cleanup [\#757](https://github.com/pypeclub/pype/pull/757) + ## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15) [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6) @@ -98,6 +177,7 @@ **Fixed bugs:** - Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768) +- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766) - Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763) **Merged pull requests:** @@ -109,6 +189,10 @@ [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1) +**Enhancements:** + +- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761) + **Fixed bugs:** - After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760) @@ -124,7 +208,6 @@ **Enhancements:** -- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687) - Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736) - Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721) - Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719) @@ -132,15 +215,12 @@ - 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699) - TV Paint: initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693) - Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678) -- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675) -- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674) - After Effects: base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667) - Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666) **Fixed bugs:** - Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726) -- Fix ffmpeg executable path with spaces [\#680](https://github.com/pypeclub/pype/pull/680) - TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740) - After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738) - Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722) @@ -157,6 +237,7 @@ - Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706) - Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700) - 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697) +- Lib from illicit part 1 [\#681](https://github.com/pypeclub/pype/pull/681) ## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19) From f40dcd7968219b5a584e625259a7890df849ae2d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 Mar 2021 17:33:43 +0100 Subject: [PATCH 097/264] feat(nuke): deadline submit with preset overrides for env var --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 643952de2c3..aba51a2bb9a 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -29,6 +29,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_pool_secondary = "" deadline_group = "" deadline_department = "" + env_overrides = {} def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" @@ -261,6 +262,11 @@ def payload_submit(self, environment = clean_environment + # Finally override by preset's env vars + if self.env_overrides: + for key, value in self.env_overrides.items(): + environment[key] = value + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, From 746bf3e5b36ba71349f899fcd77def39c08db487 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:07:38 +0100 Subject: [PATCH 098/264] changed variable usage --- pype/plugins/ftrack/publish/collect_ftrack_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 59839d77108..3711d8b6276 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -35,18 +35,18 @@ def process(self, context): # Find project entity project_query = 'Project where full_name is "{0}"'.format(project_name) self.log.debug("Project query: < {0} >".format(project_query)) - project_entity = list(session.query(project_query).all()) - if len(project_entity) == 0: + project_entities = list(session.query(project_query).all()) + if len(project_entities) == 0: raise AssertionError( "Project \"{0}\" not found in Ftrack.".format(project_name) ) # QUESTION Is possible to happen? - elif len(project_entity) > 1: + elif len(project_entities) > 1: raise AssertionError(( "Found more than one project with name \"{0}\" in Ftrack." ).format(project_name)) - project_entity = project_entity[0] + project_entity = project_entities[0] self.log.debug("Project found: {0}".format(project_entity)) # Find asset entity From 43d3f72b7d86c510986f8006bff821a2b934af1b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:08:11 +0100 Subject: [PATCH 099/264] use avalon Session to get context data instead of environments --- pype/plugins/ftrack/publish/collect_ftrack_api.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 3711d8b6276..89763f21633 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -27,10 +27,9 @@ def process(self, context): context.data["ftrackSession"] = session # Collect task - - project_name = os.environ.get('AVALON_PROJECT', '') - asset_name = os.environ.get('AVALON_ASSET', '') - task_name = os.environ.get('AVALON_TASK', None) + project_name = avalon.api.Session["AVALON_PROJECT"] + asset_name = avalon.api.Session["AVALON_ASSET"] + task_name = avalon.api.Session["AVALON_TASK"] # Find project entity project_query = 'Project where full_name is "{0}"'.format(project_name) From 34fe4c643c845b85f920dacadd6e801242c276de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:08:29 +0100 Subject: [PATCH 100/264] changed order of collect ftrack api to be the latest collector --- pype/plugins/ftrack/publish/collect_ftrack_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 89763f21633..ce02e133174 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -1,6 +1,7 @@ import os -import pyblish.api import logging +import pyblish.api +import avalon.api try: import ftrack_api_old as ftrack_api @@ -11,11 +12,10 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): """ Collects an ftrack session and the current task id. """ - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.4999 label = "Collect Ftrack Api" def process(self, context): - ftrack_log = logging.getLogger('ftrack_api') ftrack_log.setLevel(logging.WARNING) ftrack_log = logging.getLogger('ftrack_api_old') From eea445d783a38dff293311b252c514c16e87fbe6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:09:04 +0100 Subject: [PATCH 101/264] added collecting per instance for cases that intance has different context --- .../ftrack/publish/collect_ftrack_api.py | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index ce02e133174..2682dd27e93 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -24,7 +24,6 @@ def process(self, context): # Collect session session = ftrack_api.Session(auto_connect_event_hub=True) self.log.debug("Ftrack user: \"{0}\"".format(session.api_user)) - context.data["ftrackSession"] = session # Collect task project_name = avalon.api.Session["AVALON_PROJECT"] @@ -95,7 +94,97 @@ def process(self, context): task_entity = None self.log.warning("Task name is not set.") + context.data["ftrackSession"] = session context.data["ftrackPythonModule"] = ftrack_api context.data["ftrackProject"] = project_entity context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity + + self.per_instance_process(context, asset_name, task_name) + + def per_instance_process( + self, context, context_asset_name, context_task_name + ): + to_query_combinations = [] + instance_by_asset_and_task = {} + for instance in context: + instance_asset_name = instance.data.get("asset") + instance_task_name = instance.data.get("task") + + if not instance_asset_name and not instance_task_name: + continue + + elif instance_asset_name and instance_task_name: + if ( + instance_asset_name == context_asset_name + and instance_task_name == context_task_name + ): + continue + asset_name = instance_asset_name + task_name = instance_task_name + + elif instance_task_name: + if instance_task_name == context_task_name: + continue + + asset_name = context_asset_name + task_name = instance_task_name + + elif instance_asset_name: + if instance_asset_name == context_asset_name: + continue + + # Do not use context's task name + task_name = instance_task_name + asset_name = instance_asset_name + + asset_name = ( + instance_asset_name + if instance_asset_name + else context_asset_name + ) + task_name = ( + instance_task_name + if instance_task_name + else context_task_name + ) + if asset_name not in instance_by_asset_and_task: + instance_by_asset_and_task[asset_name] = {} + + if task_name not in instance_by_asset_and_task[asset_name]: + instance_by_asset_and_task[asset_name][task_name] = [] + instance_by_asset_and_task[asset_name][task_name].append(instance) + + if not instance_by_asset_and_task: + return + + session = context.data["ftrackSession"] + project_entity = context.data["ftrackProject"] + for asset_name, by_task_data in instance_by_asset_and_task.items(): + entity = session.query(( + "TypedContext where project_id is \"{}\" and name is \"{}\"" + ).format(project_entity["id"], asset_name)).first() + + task_entity_by_name = {} + if not entity: + instance.data["ftrackEntity"] = None + else: + task_entities = session.query(( + "select id, name from Task where parent_id is \"{}\"" + ).format(entity["id"])).all() + for task_entity in task_entities: + task_name_low = task_entity["name"].lower() + task_entity_by_name[task_name_low] = task_entity + + for task_name, instances in by_task_data.items(): + task_entity = None + if task_name and entity: + task_entity = task_entity_by_name.get(task_name.lower()) + + for instance in instances: + instance.data["ftrackTask"] = task_entity + + self.log.debug(( + "Instance {} has own ftrack entities" + " as has different context. TypedContext: {} Task: {}" + ).format(str(instance), str(entity), str(task_entity))) From 955d82cdc8ee44264870e62b72848c682d1256e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:10:34 +0100 Subject: [PATCH 102/264] usage of ftrack entities from instance/context is more specific --- .../ftrack/publish/integrate_ftrack_api.py | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index 2c8e06a0997..0ad3995465a 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -102,22 +102,32 @@ def _set_task_status(self, instance, task_entity, session): def process(self, instance): session = instance.context.data["ftrackSession"] - if instance.data.get("ftrackTask"): - task = instance.data["ftrackTask"] - name = task - parent = task["parent"] - elif instance.data.get("ftrackEntity"): - task = None - name = instance.data.get("ftrackEntity")['name'] + context = instance.context + + name = None + # If instance has set "ftrackEntity" or "ftrackTask" then use them from + # instance. Even if they are set to None. If they are set to None it + # has a reason. (like has different context) + if "ftrackEntity" in instance.data or "ftrackTask" in instance.data: + task = instance.data.get("ftrackTask") parent = instance.data.get("ftrackEntity") - elif instance.context.data.get("ftrackTask"): - task = instance.context.data["ftrackTask"] - name = task + + elif "ftrackEntity" in context.data or "ftrackTask" in context.data: + task = context.data.get("ftrackTask") + parent = context.data.get("ftrackEntity") + + if task: parent = task["parent"] - elif instance.context.data.get("ftrackEntity"): - task = None - name = instance.context.data.get("ftrackEntity")['name'] - parent = instance.context.data.get("ftrackEntity") + name = task + elif parent: + name = parent["name"] + + if not name: + self.log.info(( + "Skipping ftrack integration. Instance \"{}\" does not" + " have specified ftrack entities." + ).format(str(instance))) + return info_msg = "Created new {entity_type} with data: {data}" info_msg += ", metadata: {metadata}." From c8708767a005d5eb25866e268515227711f1b6df Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:11:48 +0100 Subject: [PATCH 103/264] collect context changes representation names during bulk processing --- .../standalonepublisher/publish/collect_context.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 8930bedab8e..dd122527064 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -14,12 +14,12 @@ """ import os -import pyblish.api -from avalon import io import json import copy -import clique from pprint import pformat +import clique +import pyblish.api +from avalon import io class CollectContextDataSAPublish(pyblish.api.ContextPlugin): @@ -154,6 +154,11 @@ def prepare_mov_batch_instances(self, context, in_data): str(repre["files"]) )) ext = repre["ext"][1:] + + # Rename representation name + repre_name = repre["name"] + if repre_name.startswith(ext + "_"): + repre["name"] = ext # Skip files that are not available for mov batch publishing # TODO add dynamic expected extensions by family from `in_data` # - with this modification it would be possible to use only From bf12b59afb4f9ae847511b6cd4321c341c0bfdd3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:12:07 +0100 Subject: [PATCH 104/264] few formatting changes --- pype/plugins/ftrack/publish/integrate_ftrack_api.py | 6 ++++-- .../standalonepublisher/publish/collect_batch_instances.py | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index 0ad3995465a..6c25b9191e4 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -129,8 +129,10 @@ def process(self, instance): ).format(str(instance))) return - info_msg = "Created new {entity_type} with data: {data}" - info_msg += ", metadata: {metadata}." + info_msg = ( + "Created new {entity_type} with data: {data}" + ", metadata: {metadata}." + ) used_asset_versions = [] diff --git a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py index 94574ad19cf..545efcb3035 100644 --- a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py @@ -4,9 +4,7 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): - """ - Collect all available instances from psd batch. - """ + """Collect all available instances for batch publish.""" label = "Collect Batch Instances" order = pyblish.api.CollectorOrder + 0.489 @@ -65,7 +63,7 @@ def process(self, instance): # add subset data from preset new_instance.data.update(subset_data) - new_instance.data["label"] = f"{instance_name}" + new_instance.data["label"] = instance_name new_instance.data["subset"] = subset_name new_instance.data["task"] = task_name From aa10d0bf85d5ab8cb5a44177680c8172a4e4c3a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Mar 2021 19:12:56 +0100 Subject: [PATCH 105/264] removed unused variable --- pype/plugins/ftrack/publish/collect_ftrack_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 2682dd27e93..a6b911e43bc 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -105,7 +105,6 @@ def process(self, context): def per_instance_process( self, context, context_asset_name, context_task_name ): - to_query_combinations = [] instance_by_asset_and_task = {} for instance in context: instance_asset_name = instance.data.get("asset") From 99ca35df0e1b67b297497cab59432cd9dc6d91e2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:10:59 +0100 Subject: [PATCH 106/264] removed `repreProfiles` key which is not used --- pype/plugins/standalonepublisher/publish/collect_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index dd122527064..75e0b82d02d 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -240,7 +240,6 @@ def create_instance(self, context, in_data): if component["preview"]: instance.data["families"].append("review") - instance.data["repreProfiles"] = ["h264"] component["tags"] = ["review"] self.log.debug("Adding review family") From 41179a1a7467f0012830daa9efd63bc30b2f1768 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:11:23 +0100 Subject: [PATCH 107/264] add review tag and family to instance bulk instances --- .../plugins/standalonepublisher/publish/collect_context.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 75e0b82d02d..5c0dfd65c13 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -183,6 +183,10 @@ def prepare_mov_batch_instances(self, context, in_data): new_repre["files"] = filename new_repre["name"] = ext + if "tags" not in new_repre: + new_repre["tags"] = [] + new_repre["tags"].append("review") + # Prepare new subset name (temporary name) # - subset name will be changed in batch specific plugins new_subset_name = "{}{}".format( @@ -194,6 +198,9 @@ def prepare_mov_batch_instances(self, context, in_data): in_data_copy = copy.deepcopy(in_data) in_data_copy["representations"] = [new_repre] in_data_copy["subset"] = new_subset_name + if "families" not in in_data_copy: + in_data_copy["families"] = [] + in_data_copy["families"].append("review") in_data_list.append(in_data_copy) From f30534367b4a151b5c26c3253bc089770e4009dd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:20:35 +0100 Subject: [PATCH 108/264] don't pass context to bulk move preparation --- pype/plugins/standalonepublisher/publish/collect_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 5c0dfd65c13..4de6c4cb63b 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -50,7 +50,7 @@ def process(self, context): self.add_files_to_ignore_cleanup(in_data, context) # exception for editorial if in_data["family"] == "render_mov_batch": - in_data_list = self.prepare_mov_batch_instances(context, in_data) + in_data_list = self.prepare_mov_batch_instances(in_data) elif in_data["family"] in ["editorial", "background_batch"]: in_data_list = self.multiple_instances(context, in_data) @@ -132,7 +132,7 @@ def multiple_instances(self, context, in_data): return in_data_list - def prepare_mov_batch_instances(self, context, in_data): + def prepare_mov_batch_instances(self, in_data): """Copy of `multiple_instances` method. Method was copied because `batch_extensions` is used in From 2ecc2e320fa1685a20d7aa55ab6822f93a842cec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:20:58 +0100 Subject: [PATCH 109/264] add possibility to set families before `create_instance` --- .../standalonepublisher/publish/collect_context.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 4de6c4cb63b..f91a03e7424 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -208,6 +208,12 @@ def prepare_mov_batch_instances(self, in_data): def create_instance(self, context, in_data): 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( @@ -224,7 +230,7 @@ def create_instance(self, context, in_data): "frameEnd": in_data.get("representations", [None])[0].get( "frameEnd", None ), - "families": self.default_families or [], + "families": instance_families } ) self.log.info("collected instance: {}".format(pformat(instance.data))) From 1207c207893efc67a2078aa6e3ef165c47d61d28 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:21:31 +0100 Subject: [PATCH 110/264] modified docstring --- pype/plugins/standalonepublisher/publish/collect_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index f91a03e7424..53baddba367 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -142,8 +142,10 @@ def prepare_mov_batch_instances(self, in_data): this family specific filtering. Also "frameStart" and "frameEnd" keys are removed from instance which is needed for this processing. + Instance data will also care about families. + TODO: - - Merge logic with `multiple_instances` method. + - Merge possible logic with `multiple_instances` method. """ self.log.info("Preparing data for mov batch processing.") in_data_list = [] From 07fd50da99fee8a1427210fe4c97cf8fc46a3ea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:39:41 +0100 Subject: [PATCH 111/264] fixed collect ftrack api --- .../ftrack/publish/collect_ftrack_api.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index a6b911e43bc..6379aaa0755 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -107,10 +107,14 @@ def per_instance_process( ): instance_by_asset_and_task = {} for instance in context: + self.log.debug( + "Checking entities of instance \"{}\"".format(str(instance)) + ) instance_asset_name = instance.data.get("asset") instance_task_name = instance.data.get("task") if not instance_asset_name and not instance_task_name: + self.log.debug("Instance does not have set context keys.") continue elif instance_asset_name and instance_task_name: @@ -118,12 +122,20 @@ def per_instance_process( instance_asset_name == context_asset_name and instance_task_name == context_task_name ): + self.log.debug(( + "Instance's context is same as in publish context." + " Asset: {} | Task: {}" + ).format(context_asset_name, context_task_name)) continue asset_name = instance_asset_name task_name = instance_task_name elif instance_task_name: if instance_task_name == context_task_name: + self.log.debug(( + "Instance's context task is same as in publish" + " context. Task: {}" + ).format(context_task_name)) continue asset_name = context_asset_name @@ -131,22 +143,16 @@ def per_instance_process( elif instance_asset_name: if instance_asset_name == context_asset_name: + self.log.debug(( + "Instance's context asset is same as in publish" + " context. Asset: {}" + ).format(context_asset_name)) continue # Do not use context's task name task_name = instance_task_name asset_name = instance_asset_name - asset_name = ( - instance_asset_name - if instance_asset_name - else context_asset_name - ) - task_name = ( - instance_task_name - if instance_task_name - else context_task_name - ) if asset_name not in instance_by_asset_and_task: instance_by_asset_and_task[asset_name] = {} @@ -166,7 +172,9 @@ def per_instance_process( task_entity_by_name = {} if not entity: - instance.data["ftrackEntity"] = None + self.log.warning(( + "Didn't find entity with name \"{}\" in Project \"{}\"" + ).format(asset_name, project_entity["full_name"])) else: task_entities = session.query(( "select id, name from Task where parent_id is \"{}\"" @@ -181,6 +189,7 @@ def per_instance_process( task_entity = task_entity_by_name.get(task_name.lower()) for instance in instances: + instance.data["ftrackEntity"] = entity instance.data["ftrackTask"] = task_entity self.log.debug(( From 10ce5dc37581336fd9cb75cdb8c6f80f8ec01e05 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:44:55 +0100 Subject: [PATCH 112/264] faster collect ftrack api --- .../ftrack/publish/collect_ftrack_api.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 6379aaa0755..29db7827dc3 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -165,11 +165,25 @@ def per_instance_process( session = context.data["ftrackSession"] project_entity = context.data["ftrackProject"] - for asset_name, by_task_data in instance_by_asset_and_task.items(): - entity = session.query(( - "TypedContext where project_id is \"{}\" and name is \"{}\"" - ).format(project_entity["id"], asset_name)).first() + asset_names = set() + for asset_name in instance_by_asset_and_task.keys(): + asset_names.add(asset_name) + + joined_asset_names = ",".join([ + "\"{}\"".format(name) + for name in asset_names + ]) + entities = session.query(( + "TypedContext where project_id is \"{}\" and name in ({})" + ).format(project_entity["id"], joined_asset_names)).all() + + entities_by_name = { + entity["name"]: entity + for entity in entities + } + for asset_name, by_task_data in instance_by_asset_and_task.items(): + entity = entities_by_name.get(asset_name) task_entity_by_name = {} if not entity: self.log.warning(( From 81c913180e871578035088d33d502b2489086fdd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Mar 2021 11:57:18 +0100 Subject: [PATCH 113/264] add thumbnail --- pype/plugins/standalonepublisher/publish/collect_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index 53baddba367..be660820a7f 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -184,6 +184,7 @@ def prepare_mov_batch_instances(self, in_data): new_repre = copy.deepcopy(repre) new_repre["files"] = filename new_repre["name"] = ext + new_repre["thumbnail"] = True if "tags" not in new_repre: new_repre["tags"] = [] From 1a46bdd21aba6c6abace627e739705e0344aeb2e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Mar 2021 14:43:44 +0100 Subject: [PATCH 114/264] feat(nuke): read node loaders with preset for node name --- pype/plugins/nuke/load/load_image.py | 17 +++++++++++++---- pype/plugins/nuke/load/load_mov.py | 17 +++++++++++++---- pype/plugins/nuke/load/load_sequence.py | 17 +++++++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pype/plugins/nuke/load/load_image.py b/pype/plugins/nuke/load/load_image.py index 71fa987bc60..e5f09925051 100644 --- a/pype/plugins/nuke/load/load_image.py +++ b/pype/plugins/nuke/load/load_image.py @@ -33,6 +33,9 @@ class LoadImage(api.Loader): ) ] + # presets + name_expression = "{class_name}_{ext}" + def load(self, context, name, namespace, options): from avalon.nuke import ( containerise, @@ -73,10 +76,16 @@ def load(self, context, name, namespace, options): frame, format(frame_number, "0{}".format(padding))) - read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - repr_cont["representation"]) + name_data = { + "asset": repr_cont["asset"], + "subset": repr_cont["subset"], + "representation": context["representation"]["name"], + "ext": repr_cont["representation"], + "id": context["representation"]["_id"], + "class_name": self.__class__.__name__ + } + + read_name = self.name_expression.format(**name_data) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): diff --git a/pype/plugins/nuke/load/load_mov.py b/pype/plugins/nuke/load/load_mov.py index b87cdb994c1..68a92060d80 100644 --- a/pype/plugins/nuke/load/load_mov.py +++ b/pype/plugins/nuke/load/load_mov.py @@ -80,6 +80,9 @@ class LoadMov(api.Loader): icon = "code-fork" color = "orange" + # presets + name_expression = "{class_name}_{ext}" + def loader_shift(self, node, frame, relative=True): """Shift global in time by i preserving duration @@ -152,10 +155,16 @@ def load(self, context, name, namespace, data): file = file.replace("\\", "/") - read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - repr_cont["representation"]) + name_data = { + "asset": repr_cont["asset"], + "subset": repr_cont["subset"], + "representation": context["representation"]["name"], + "ext": repr_cont["representation"], + "id": context["representation"]["_id"], + "class_name": self.__class__.__name__ + } + + read_name = self.name_expression.format(**name_data) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 4810db2cd98..1648dc8b630 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -80,6 +80,9 @@ class LoadSequence(api.Loader): icon = "file-video-o" color = "white" + # presets + name_expression = "{class_name}_{ext}" + @staticmethod def fix_hashes_in_path(file, repr_cont): if "#" not in file: @@ -137,10 +140,16 @@ def load(self, context, name, namespace, data): file = self.fix_hashes_in_path(file, repr_cont).replace("\\", "/") - read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - context["representation"]["name"]) + name_data = { + "asset": repr_cont["asset"], + "subset": repr_cont["subset"], + "representation": context["representation"]["name"], + "ext": repr_cont["representation"], + "id": context["representation"]["_id"], + "class_name": self.__class__.__name__ + } + + read_name = self.name_expression.format(**name_data) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): From 1c170b0f0018442bf5003492068333e123989476 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Mar 2021 14:44:20 +0100 Subject: [PATCH 115/264] feat(global): ading .vscode to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 101c1e62246..52e14b03ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ node_modules/ package-lock.json pype/premiere/ppro/js/debug.log + +/.vscode/ \ No newline at end of file From 0a7de3e7a8c766046d01f9626c5b6b4379c69d61 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 Mar 2021 17:46:03 +0100 Subject: [PATCH 116/264] fix expected files in maya redshift --- pype/hosts/maya/expected_files.py | 36 ++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index ebdc7df4179..bb9c0cff0d2 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -58,7 +58,8 @@ ) R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) -R_REMOVE_AOV_TOKEN = re.compile(r"_%a|_|_", re.IGNORECASE) +R_REMOVE_AOV_TOKEN = re.compile( + r"_%a|\.%a|_|\.|_|\.", re.IGNORECASE) # to remove unused renderman tokens R_CLEAN_FRAME_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) R_CLEAN_EXT_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) @@ -246,7 +247,8 @@ def _get_layer_data(self): } return scene_data - def _generate_single_file_sequence(self, layer_data): + def _generate_single_file_sequence( + self, layer_data, force_aov_name=None): expected_files = [] for cam in layer_data["cameras"]: file_prefix = layer_data["filePrefix"] @@ -256,7 +258,9 @@ def _generate_single_file_sequence(self, layer_data): (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift - (R_REMOVE_AOV_TOKEN, ""), + (R_REMOVE_AOV_TOKEN, "") if not force_aov_name \ + else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name), + (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), ) @@ -709,7 +713,7 @@ def get_renderer_prefix(self): """ prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() - prefix = "{}_".format(prefix) + prefix = "{}.".format(prefix) return prefix def get_files(self): @@ -726,10 +730,6 @@ def get_files(self): # as redshift output beauty without 'beauty' in filename. layer_data = self._get_layer_data() - if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence( - layer_data - ) # Redshift doesn't merge Cryptomatte AOV to final exr. We need to check # for such condition and add it to list of expected files. @@ -741,6 +741,26 @@ def get_files(self): {aov_name: self._generate_single_file_sequence(layer_data)} ) + if layer_data.get("enabledAOVs"): + # because if Beauty is added manually, it will be rendered as + # 'Beauty_other' in file name and "standard" beauty will have + # 'Beauty' in its name. When disabled, standard output will be + # without `Beauty`. + if expected_files[0].get(u"Beauty"): + expected_files[0][u"Beauty_other"] = expected_files[0].pop( + u"Beauty") + new_list = [] + for seq in expected_files[0][u"Beauty_other"]: + new_list.append(seq.replace(".Beauty", ".Beauty_other")) + expected_files[0][u"Beauty_other"] = new_list + expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501 + layer_data, force_aov_name="Beauty" + ) + else: + expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501 + layer_data + ) + return expected_files def get_aovs(self): From 45a442a03eec04673cf55b7962a8284e243b047a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 Mar 2021 18:51:23 +0100 Subject: [PATCH 117/264] configurable support for group and limits --- pype/plugins/maya/publish/submit_maya_deadline.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 1354e3d5123..28f58f2e810 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -263,6 +263,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): use_published = True tile_assembler_plugin = "PypeTileAssembler" asset_dependencies = False + limit_groups = [] + group = "none" def process(self, instance): """Plugin entry point.""" @@ -406,6 +408,10 @@ def process(self, instance): # Set job priority self.payload_skeleton["JobInfo"]["Priority"] = self._instance.data.get( "priority", 50) + if self.group != "none": + self.payload_skeleton["JobInfo"]["Group"] = self.group + if self.limit: + self.payload_skeleton["jobInfo"]["LimitGroups"] = ",".join(self.limit) # noqa: E501 # Optional, enable double-click to preview rendered # frames from Deadline Monitor self.payload_skeleton["JobInfo"]["OutputDirectory0"] = \ From 83cad5c80371449eba6b4fce145129c5603b5830 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 18 Mar 2021 16:07:50 +0100 Subject: [PATCH 118/264] make sure paths are wrapped in quotes --- pype/plugins/global/publish/extract_jpeg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 680a64dc897..150f1a8299e 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -96,7 +96,7 @@ def process(self, instance): # use same input args like with mov jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i {}".format(full_input_path)) + jpeg_items.append("-i \"{}\"".format(full_input_path)) # output arguments from presets output_args = self._prepare_output_args( ffmpeg_args.get("output"), full_input_path, instance @@ -109,7 +109,7 @@ def process(self, instance): jpeg_items.append("-vframes 1") # output file - jpeg_items.append(full_output_path) + jpeg_items.append("\"{}\"".format(full_output_path)) subprocess_jpeg = " ".join(jpeg_items) From 62042fbc42e8b21ddf857204be2a0136b39276dd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 18 Mar 2021 17:39:14 +0100 Subject: [PATCH 119/264] fix typos --- pype/plugins/maya/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 28f58f2e810..e7b5dfcf1cc 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -411,7 +411,7 @@ def process(self, instance): if self.group != "none": self.payload_skeleton["JobInfo"]["Group"] = self.group if self.limit: - self.payload_skeleton["jobInfo"]["LimitGroups"] = ",".join(self.limit) # noqa: E501 + self.payload_skeleton["JobInfo"]["LimitGroups"] = ",".join(self.limit_groups) # noqa: E501 # Optional, enable double-click to preview rendered # frames from Deadline Monitor self.payload_skeleton["JobInfo"]["OutputDirectory0"] = \ From 28307102d7f9093fd8f99a573459e474d69b8dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 19 Mar 2021 14:26:07 +0100 Subject: [PATCH 120/264] fix typo in condition --- pype/plugins/maya/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index e7b5dfcf1cc..c4f7578cfd8 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -410,7 +410,7 @@ def process(self, instance): "priority", 50) if self.group != "none": self.payload_skeleton["JobInfo"]["Group"] = self.group - if self.limit: + if self.limit_groups: self.payload_skeleton["JobInfo"]["LimitGroups"] = ",".join(self.limit_groups) # noqa: E501 # Optional, enable double-click to preview rendered # frames from Deadline Monitor From 9f95022a4945269621e3d77930995c09b1465d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 19 Mar 2021 16:03:10 +0100 Subject: [PATCH 121/264] fixed superclass for CreateCameraRig --- pype/plugins/maya/create/create_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/create/create_camera.py b/pype/plugins/maya/create/create_camera.py index 160882696af..9a87a6ca301 100644 --- a/pype/plugins/maya/create/create_camera.py +++ b/pype/plugins/maya/create/create_camera.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): self.data['bakeToWorldSpace'] = True -class CreateCameraRig(avalon.maya.Creator): +class CreateCameraRig(plugin.Creator): """Complex hierarchy with camera.""" name = "camerarigMain" From 4ffa6c30ffa6331c3e3d696db4472fed68698df1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Mar 2021 16:57:26 +0100 Subject: [PATCH 122/264] feat(nuke): submit job with limit groups filtered per node class --- .../nuke/publish/submit_nuke_deadline.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index aba51a2bb9a..91f532a8ac9 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -6,7 +6,7 @@ from avalon.vendor import requests import re import pyblish.api - +import nuke class NukeSubmitDeadline(pyblish.api.InstancePlugin): """Submit write to Deadline @@ -29,6 +29,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_pool_secondary = "" deadline_group = "" deadline_department = "" + deadline_limit_groups = {} env_overrides = {} def process(self, instance): @@ -177,7 +178,10 @@ def payload_submit(self, # Optional, enable double-click to preview rendered # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/") + "OutputFilename0": output_filename_0.replace("\\", "/"), + + # limiting groups + "LimitGroups": self.get_limiting_group_filter() }, "PluginInfo": { @@ -352,3 +356,23 @@ def expected_files(self, for i in range(self._frame_start, (self._frame_end + 1)): instance.data["expectedFiles"].append( os.path.join(dir, (file % i)).replace("\\", "/")) + + def get_limiting_group_filter(self): + """Search for limit group nodes and return group name. + + Limit groups will be defined as pairs in Nuke deadline submitter + presents where the key will be name of limit group and value will be + a list of plugin's node class names. Thus, when a plugin uses more + than one node, these will be captured and the triggered process + will add the appropriate limit group to the payload jobinfo attributes. + """ + captured_groups = [] + for lg_name, list_node_class in self.deadline_limit_groups.items(): + for node_class in list_node_class: + for node in nuke.allNodes(recurseGroups=True): + if node.Class() not in node_class: + continue + if lg_name not in captured_groups: + captured_groups.append(lg_name) + + return ",".join(captured_groups) From 94d5decada3869793299bf4e5ede4daa448638be Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Mar 2021 17:09:13 +0100 Subject: [PATCH 123/264] feat(nuke): improving doc-string and ignoring disabled nodes --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 91f532a8ac9..9a692e8ce03 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -365,13 +365,21 @@ def get_limiting_group_filter(self): a list of plugin's node class names. Thus, when a plugin uses more than one node, these will be captured and the triggered process will add the appropriate limit group to the payload jobinfo attributes. + + Returning: + str: captured groups devided by comma and no space """ captured_groups = [] for lg_name, list_node_class in self.deadline_limit_groups.items(): for node_class in list_node_class: for node in nuke.allNodes(recurseGroups=True): + # ignore all nodes not member of defined class if node.Class() not in node_class: continue + # ignore all disabled nodes + if node["disable"].value(): + continue + # add group name if not already added if lg_name not in captured_groups: captured_groups.append(lg_name) From cba4bdff6146ebd393d377cf72c9d9b6aa51a753 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Mar 2021 09:02:49 +0000 Subject: [PATCH 124/264] Limit to nuke host only. --- pype/plugins/global/publish/collect_audio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/global/publish/collect_audio.py b/pype/plugins/global/publish/collect_audio.py index 4c7bda7a5aa..edc8396f622 100644 --- a/pype/plugins/global/publish/collect_audio.py +++ b/pype/plugins/global/publish/collect_audio.py @@ -12,6 +12,7 @@ class CollectAudio(pyblish.api.ContextPlugin): label = "Collect Audio" subset_name = "audioMain" + hosts = ["nuke"] def process(self, context): version = pype_api.get_latest_version( From 1f5bd89f035bb7f2d6fb5285d053060ed3389429 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Mar 2021 11:42:03 +0100 Subject: [PATCH 125/264] fix(nuke): PR suggestions --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 9a692e8ce03..f33f6484171 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -147,6 +147,10 @@ def payload_submit(self, if not priority: priority = self.deadline_priority + # resolve any limit groups + limit_groups = self.get_limit_groups() + self.log.info("Limit groups: `{}`".format(limit_groups)) + payload = { "JobInfo": { # Top-level group name @@ -181,7 +185,7 @@ def payload_submit(self, "OutputFilename0": output_filename_0.replace("\\", "/"), # limiting groups - "LimitGroups": self.get_limiting_group_filter() + "LimitGroups": ",".join(limit_groups) }, "PluginInfo": { @@ -357,7 +361,7 @@ def expected_files(self, instance.data["expectedFiles"].append( os.path.join(dir, (file % i)).replace("\\", "/")) - def get_limiting_group_filter(self): + def get_limit_groups(self): """Search for limit group nodes and return group name. Limit groups will be defined as pairs in Nuke deadline submitter @@ -383,4 +387,4 @@ def get_limiting_group_filter(self): if lg_name not in captured_groups: captured_groups.append(lg_name) - return ",".join(captured_groups) + return captured_groups From 3a907e332efcea5c370ec25d9a58d0f700e66748 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Mar 2021 13:33:47 +0100 Subject: [PATCH 126/264] fix(nuke): false info in doc string --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index f33f6484171..08cbfc4d8e2 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -371,7 +371,7 @@ def get_limit_groups(self): will add the appropriate limit group to the payload jobinfo attributes. Returning: - str: captured groups devided by comma and no space + list: captured groups list """ captured_groups = [] for lg_name, list_node_class in self.deadline_limit_groups.items(): From d6c7a4e9a6f81ba2e3dba8f55318a2f0dde50a20 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 22 Mar 2021 22:03:19 +0100 Subject: [PATCH 127/264] 2.16.0 version and changelog --- .github_changelog_generator | 2 +- CHANGELOG.md | 67 ++++++++++++++++++++----------------- pype/version.py | 2 +- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index eba22cfd39f..fa87c93ccfe 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -7,4 +7,4 @@ enhancement-label=**Enhancements:** release-branch=2.x/develop issues=False exclude-tags-regex=3.\d.\d.* -future-release=2.15.4 \ No newline at end of file +future-release=2.16.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bd0e9ef8194..eaa10957c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,47 +1,54 @@ # Changelog -## [2.15.4-prerelease](https://github.com/pypeclub/pype/tree/2.15.4) (2021-03-12) +## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22) -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.15.4) +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0) **Enhancements:** +- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167) +- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156) +- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152) +- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146) +- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142) - Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127) - Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124) - Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106) +- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101) +- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) +- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088) - TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080) - Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078) - Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072) +- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008) **Fixed bugs:** +- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166) +- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163) - Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125) - Nuke: AvalonTab as default for write node [\#1114](https://github.com/pypeclub/pype/pull/1114) - Nuke: class parent to correct class name [\#1109](https://github.com/pypeclub/pype/pull/1109) - Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090) - Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082) -**Merged pull requests:** - -- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) - ## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26) [Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3) +**Enhancements:** + +- Populate plugins settings [\#1051](https://github.com/pypeclub/pype/pull/1051) +- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) +- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) + **Fixed bugs:** -- Fix order of wrappers in settings gui [\#1052](https://github.com/pypeclub/pype/pull/1052) - Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085) - Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059) - TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057) - Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046) -**Merged pull requests:** - -- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) -- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) - ## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19) [Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2) @@ -52,6 +59,7 @@ **Fixed bugs:** +- Fix entity move under project [\#1041](https://github.com/pypeclub/pype/pull/1041) - Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040) - smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036) - TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031) @@ -62,16 +70,14 @@ **Enhancements:** +- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011) - Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445) **Fixed bugs:** -- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) - -**Merged pull requests:** - -- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011) +- PS fix - removed unwanted functions from pywin32 [\#1007](https://github.com/pypeclub/pype/pull/1007) - PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006) +- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) ## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09) @@ -79,6 +85,9 @@ **Enhancements:** +- Pype 3: Poetry linux issues [\#992](https://github.com/pypeclub/pype/pull/992) +- nuke: wrong frame offset in mov loader [\#975](https://github.com/pypeclub/pype/pull/975) +- PSD Bulk export of ANIM group [\#966](https://github.com/pypeclub/pype/pull/966) - Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926) - Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) - Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993) @@ -96,21 +105,19 @@ - Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840) - Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824) - DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795) -- Unreal: add 4.26 support [\#746](https://github.com/pypeclub/pype/pull/746) - Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695) **Fixed bugs:** - Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995) +- fix\(nuke\): add nuke related env var to sumbission [\#989](https://github.com/pypeclub/pype/pull/989) - Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982) -- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960) -- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990) +- Get creator by name fix [\#980](https://github.com/pypeclub/pype/pull/980) - Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988) - Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984) - Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979) - Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972) - nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971) -- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967) - PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964) - Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956) - DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954) @@ -123,10 +130,6 @@ - Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831) - Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822) -**Merged pull requests:** - -- Ftrack plugins cleanup [\#757](https://github.com/pypeclub/pype/pull/757) - ## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15) [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6) @@ -158,6 +161,7 @@ **Fixed bugs:** +- Avalon context lib fix [\#800](https://github.com/pypeclub/pype/pull/800) - TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809) - Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807) - Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806) @@ -172,7 +176,7 @@ **Enhancements:** -- AE: load background [\#774](https://github.com/pypeclub/pype/pull/774) +- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770) **Fixed bugs:** @@ -182,6 +186,7 @@ **Merged pull requests:** +- Anatomy load data from settings [\#787](https://github.com/pypeclub/pype/pull/787) - AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781) - TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769) @@ -191,6 +196,7 @@ **Enhancements:** +- Colorspace Management \(Image I/O\) settings [\#753](https://github.com/pypeclub/pype/pull/753) - Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761) **Fixed bugs:** @@ -208,6 +214,8 @@ **Enhancements:** +- Feature \#111 dwaa support [\#745](https://github.com/pypeclub/pype/pull/745) +- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687) - Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736) - Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721) - Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719) @@ -215,6 +223,8 @@ - 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699) - TV Paint: initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693) - Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678) +- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675) +- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674) - After Effects: base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667) - Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666) @@ -237,16 +247,11 @@ - Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706) - Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700) - 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697) -- Lib from illicit part 1 [\#681](https://github.com/pypeclub/pype/pull/681) ## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19) [Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7) -**Fixed bugs:** - -- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729) - # Changelog ## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15) diff --git a/pype/version.py b/pype/version.py index 44e30b12666..8f4a351703b 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.15.3" +__version__ = "2.16.0" From 2850ff900136231e9f647ab517a05b3198d308db Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 22 Mar 2021 22:17:35 +0100 Subject: [PATCH 128/264] update changelog --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa10957c61..52f9808d327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ - Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152) - Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146) - nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142) +- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138) - Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127) - Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124) +- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117) - Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106) - Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101) - Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) @@ -21,16 +23,19 @@ - Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078) - Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072) - Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008) +- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073) + **Fixed bugs:** +- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174) - Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166) - Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163) - Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125) -- Nuke: AvalonTab as default for write node [\#1114](https://github.com/pypeclub/pype/pull/1114) -- Nuke: class parent to correct class name [\#1109](https://github.com/pypeclub/pype/pull/1109) - Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090) - Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082) +- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067) +- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064) ## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26) @@ -38,7 +43,6 @@ **Enhancements:** -- Populate plugins settings [\#1051](https://github.com/pypeclub/pype/pull/1051) - Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) - Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) @@ -59,7 +63,6 @@ **Fixed bugs:** -- Fix entity move under project [\#1041](https://github.com/pypeclub/pype/pull/1041) - Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040) - smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036) - TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031) @@ -75,7 +78,6 @@ **Fixed bugs:** -- PS fix - removed unwanted functions from pywin32 [\#1007](https://github.com/pypeclub/pype/pull/1007) - PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006) - Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) @@ -85,9 +87,7 @@ **Enhancements:** -- Pype 3: Poetry linux issues [\#992](https://github.com/pypeclub/pype/pull/992) -- nuke: wrong frame offset in mov loader [\#975](https://github.com/pypeclub/pype/pull/975) -- PSD Bulk export of ANIM group [\#966](https://github.com/pypeclub/pype/pull/966) +- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932) - Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926) - Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) - Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993) @@ -110,15 +110,18 @@ **Fixed bugs:** - Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995) -- fix\(nuke\): add nuke related env var to sumbission [\#989](https://github.com/pypeclub/pype/pull/989) - Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982) -- Get creator by name fix [\#980](https://github.com/pypeclub/pype/pull/980) +- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960) +- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839) +- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990) - Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988) - Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984) - Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979) - Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972) - nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971) +- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967) - PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964) +- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959) - Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956) - DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954) - TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953) @@ -130,6 +133,10 @@ - Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831) - Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822) +**Merged pull requests:** + +- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934) + ## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15) [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6) @@ -147,6 +154,10 @@ [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5) +**Merged pull requests:** + +- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866) + ## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18) [Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4) @@ -161,7 +172,6 @@ **Fixed bugs:** -- Avalon context lib fix [\#800](https://github.com/pypeclub/pype/pull/800) - TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809) - Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807) - Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806) @@ -176,7 +186,7 @@ **Enhancements:** -- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770) +- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767) **Fixed bugs:** @@ -186,7 +196,6 @@ **Merged pull requests:** -- Anatomy load data from settings [\#787](https://github.com/pypeclub/pype/pull/787) - AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781) - TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769) @@ -196,7 +205,7 @@ **Enhancements:** -- Colorspace Management \(Image I/O\) settings [\#753](https://github.com/pypeclub/pype/pull/753) +- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770) - Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761) **Fixed bugs:** @@ -214,14 +223,12 @@ **Enhancements:** -- Feature \#111 dwaa support [\#745](https://github.com/pypeclub/pype/pull/745) - Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687) - Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736) - Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721) - Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719) - Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716) - 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699) -- TV Paint: initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693) - Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678) - TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675) - Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674) @@ -230,6 +237,7 @@ **Fixed bugs:** +- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743) - Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726) - TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740) - After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738) @@ -244,6 +252,7 @@ **Merged pull requests:** +- Application manager [\#728](https://github.com/pypeclub/pype/pull/728) - Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706) - Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700) - 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697) @@ -252,6 +261,10 @@ [Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7) +**Fixed bugs:** + +- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729) + # Changelog ## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15) From a3cebfff99066b647cbcf99e840671fd9a8acdad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Mar 2021 16:56:10 +0100 Subject: [PATCH 129/264] fix(nuke): reverse search to make it more versatile --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 189c0f0049b..7b7e045fce7 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -378,7 +378,7 @@ def get_limit_groups(self): for node_class in list_node_class: for node in nuke.allNodes(recurseGroups=True): # ignore all nodes not member of defined class - if node.Class() not in node_class: + if node_class not in node.Class(): continue # ignore all disabled nodes if node["disable"].value(): From fb6453b8f294130d293b5482792abe6924232a7b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 Mar 2021 17:51:19 +0100 Subject: [PATCH 130/264] Nuke: deadline submission with search replaced env values from preset --- pype/plugins/nuke/publish/collect_writes.py | 13 ++++++-- .../nuke/publish/submit_nuke_deadline.py | 32 +++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index fb00aeb1ae8..d1dad9e001a 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,7 +1,7 @@ import os import nuke import pyblish.api - +import re @pyblish.api.log class CollectNukeWrites(pyblish.api.InstancePlugin): @@ -113,9 +113,16 @@ def process(self, instance): instance.data["representations"].append(representation) self.log.debug("couldn't collect frames: {}".format(label)) + colorspace = node["colorspace"].value() + + # remove default part of the string + if "default (" in colorspace: + colorspace = re.sub(r"default.\(|\)", "", colorspace) + self.log.debug("colorspace: `{}`".format(colorspace)) + # Add version data to instance version_data = { - "colorspace": node["colorspace"].value(), + "colorspace": colorspace, } group_node = [x for x in instance if x.Class() == "Group"][0] @@ -141,7 +148,7 @@ def process(self, instance): "frameEndHandle": last_frame, "outputType": output_type, "families": families, - "colorspace": node["colorspace"].value(), + "colorspace": colorspace, "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 7b7e045fce7..93cac1f5483 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -1,3 +1,6 @@ +""" +Submit write to Deadline. +""" import os import json import getpass @@ -8,8 +11,9 @@ import pyblish.api import nuke + class NukeSubmitDeadline(pyblish.api.InstancePlugin): - """Submit write to Deadline + """Submit write to Deadline. Renders are submitted to a Deadline Web Service as supplied via the environment variable DEADLINE_REST_URL @@ -30,7 +34,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_group = "" deadline_department = "" deadline_limit_groups = {} - env_overrides = {} + env_overrides = {}, + env_search_replace_values = {} def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" @@ -237,9 +242,13 @@ def payload_submit(self, for path in os.environ: if path.lower().startswith('pype_'): environment[path] = os.environ[path] - if path.lower().startswith('nuke_') and path != "NUKE_TEMP_DIR": + if path.lower().startswith('nuke_'): + if "temp" in path.lower(): + continue + environment[path] = os.environ[path] + if "license" in path.lower(): environment[path] = os.environ[path] - if 'license' in path.lower(): + if "ofx" in path.lower(): environment[path] = os.environ[path] clean_environment = {} @@ -270,11 +279,17 @@ def payload_submit(self, environment = clean_environment - # Finally override by preset's env vars + # override by preset's env vars if self.env_overrides: for key, value in self.env_overrides.items(): environment[key] = value + # finally search replace in values of any key + if self.env_search_replace_values: + for key, value in environment.items(): + for _k, _v in self.env_search_replace_values.items(): + environment[key] = value.replace(_k, _v) + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, @@ -333,9 +348,8 @@ def preview_fname(self, path): return int(search_results[1]) if "#" in path: self.log.debug("_ path: `{}`".format(path)) - return path - else: - return path + + return path def expected_files(self, instance, @@ -343,7 +357,7 @@ def expected_files(self, """ Create expected files in instance data """ if not instance.data.get("expectedFiles"): - instance.data["expectedFiles"] = list() + instance.data["expectedFiles"] = [] dir = os.path.dirname(path) file = os.path.basename(path) From 6016f7521c73e6aebaae399062f886e6f66c9313 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 26 Mar 2021 14:07:29 +0100 Subject: [PATCH 131/264] Nuke: write node colorspace ignore `default()` label --- pype/plugins/nuke/publish/collect_writes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index fb00aeb1ae8..d1dad9e001a 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,7 +1,7 @@ import os import nuke import pyblish.api - +import re @pyblish.api.log class CollectNukeWrites(pyblish.api.InstancePlugin): @@ -113,9 +113,16 @@ def process(self, instance): instance.data["representations"].append(representation) self.log.debug("couldn't collect frames: {}".format(label)) + colorspace = node["colorspace"].value() + + # remove default part of the string + if "default (" in colorspace: + colorspace = re.sub(r"default.\(|\)", "", colorspace) + self.log.debug("colorspace: `{}`".format(colorspace)) + # Add version data to instance version_data = { - "colorspace": node["colorspace"].value(), + "colorspace": colorspace, } group_node = [x for x in instance if x.Class() == "Group"][0] @@ -141,7 +148,7 @@ def process(self, instance): "frameEndHandle": last_frame, "outputType": output_type, "families": families, - "colorspace": node["colorspace"].value(), + "colorspace": colorspace, "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) From fbb620c1fe963b533cd788b318a471169da14483 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 26 Mar 2021 14:12:41 +0100 Subject: [PATCH 132/264] nuke: reversing commit --- pype/plugins/nuke/publish/collect_writes.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index d1dad9e001a..fb00aeb1ae8 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,7 +1,7 @@ import os import nuke import pyblish.api -import re + @pyblish.api.log class CollectNukeWrites(pyblish.api.InstancePlugin): @@ -113,16 +113,9 @@ def process(self, instance): instance.data["representations"].append(representation) self.log.debug("couldn't collect frames: {}".format(label)) - colorspace = node["colorspace"].value() - - # remove default part of the string - if "default (" in colorspace: - colorspace = re.sub(r"default.\(|\)", "", colorspace) - self.log.debug("colorspace: `{}`".format(colorspace)) - # Add version data to instance version_data = { - "colorspace": colorspace, + "colorspace": node["colorspace"].value(), } group_node = [x for x in instance if x.Class() == "Group"][0] @@ -148,7 +141,7 @@ def process(self, instance): "frameEndHandle": last_frame, "outputType": output_type, "families": families, - "colorspace": colorspace, + "colorspace": node["colorspace"].value(), "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) From 91c0ed9b192f05c8b5433734eaf140630fc5d874 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 26 Mar 2021 14:19:35 +0100 Subject: [PATCH 133/264] Nuke: change env override workflow to allowed keys --- .../nuke/publish/submit_nuke_deadline.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 93cac1f5483..65c41ca2ab2 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -34,9 +34,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_group = "" deadline_department = "" deadline_limit_groups = {} - env_overrides = {}, + env_allowed_keys = [] env_search_replace_values = {} + def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" families = instance.data["families"] @@ -236,20 +237,17 @@ def payload_submit(self, "TOOL_ENV", "PYPE_DEV" ] + + # add allowed keys from preset if any + if self.env_allowed_keys: + keys += self.env_allowed_keys + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) for path in os.environ: if path.lower().startswith('pype_'): environment[path] = os.environ[path] - if path.lower().startswith('nuke_'): - if "temp" in path.lower(): - continue - environment[path] = os.environ[path] - if "license" in path.lower(): - environment[path] = os.environ[path] - if "ofx" in path.lower(): - environment[path] = os.environ[path] clean_environment = {} for key, value in environment.items(): @@ -279,11 +277,6 @@ def payload_submit(self, environment = clean_environment - # override by preset's env vars - if self.env_overrides: - for key, value in self.env_overrides.items(): - environment[key] = value - # finally search replace in values of any key if self.env_search_replace_values: for key, value in environment.items(): From 34b8b29be0333a5150ad5e0918cba04fb66e8a0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 11:52:16 +0200 Subject: [PATCH 134/264] updated custom ftrack session with changes from newer version of ftrack api --- pype/modules/ftrack/ftrack_server/lib.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 79b708b17a1..b7e3f376d81 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -19,6 +19,11 @@ import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L +try: + from weakref import WeakMethod +except ImportError: + from ftrack_api._weakref import WeakMethod + from pype.api import ( Logger, get_default_components, @@ -276,8 +281,8 @@ class SocketSession(ftrack_api.session.Session): def __init__( self, server_url=None, api_key=None, api_user=None, auto_populate=True, plugin_paths=None, cache=None, cache_key_maker=None, - auto_connect_event_hub=None, schema_cache_path=None, - plugin_arguments=None, sock=None, Eventhub=None + auto_connect_event_hub=False, schema_cache_path=None, + plugin_arguments=None, timeout=60, sock=None, Eventhub=None ): super(ftrack_api.session.Session, self).__init__() self.logger = logging.getLogger( @@ -354,6 +359,7 @@ def __init__( self._request.auth = ftrack_api.session.SessionAuthentication( self._api_key, self._api_user ) + self.request_timeout = timeout self.auto_populate = auto_populate @@ -374,7 +380,7 @@ def __init__( ) self._auto_connect_event_hub_thread = None - if auto_connect_event_hub in (None, True): + if auto_connect_event_hub: # Connect to event hub in background thread so as not to block main # session usage waiting for event hub connection. self._auto_connect_event_hub_thread = threading.Thread( @@ -383,14 +389,8 @@ def __init__( self._auto_connect_event_hub_thread.daemon = True self._auto_connect_event_hub_thread.start() - # To help with migration from auto_connect_event_hub default changing - # from True to False. - self._event_hub._deprecation_warning_auto_connect = ( - auto_connect_event_hub is None - ) - # Register to auto-close session on exit. - atexit.register(self.close) + atexit.register(WeakMethod(self.close)) self._plugin_paths = plugin_paths if self._plugin_paths is None: @@ -404,8 +404,9 @@ def __init__( # rebuilding types)? if schema_cache_path is not False: if schema_cache_path is None: + schema_cache_path = appdirs.user_cache_dir() schema_cache_path = os.environ.get( - 'FTRACK_API_SCHEMA_CACHE_PATH', tempfile.gettempdir() + 'FTRACK_API_SCHEMA_CACHE_PATH', schema_cache_path ) schema_cache_path = os.path.join( From 21ac845767667a91bfce8b85880237e426e5ce96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 29 Mar 2021 11:59:09 +0200 Subject: [PATCH 135/264] fix appdirs import --- pype/modules/ftrack/ftrack_server/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index b7e3f376d81..e4f5b65ea82 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -11,6 +11,7 @@ import pymongo import requests +import appdirs import ftrack_api import ftrack_api.session import ftrack_api.cache From 87982cd2977924dfd619251ea89bb088627610d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 12:51:57 +0200 Subject: [PATCH 136/264] bulk mov instance collecting separated from batch instances --- .../publish/collect_batch_instances.py | 11 +--- .../publish/collect_bulk_mov_instances.py | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py diff --git a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py index 545efcb3035..4ca1f72cc41 100644 --- a/pype/plugins/standalonepublisher/publish/collect_batch_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_batch_instances.py @@ -9,12 +9,11 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): label = "Collect Batch Instances" order = pyblish.api.CollectorOrder + 0.489 hosts = ["standalonepublisher"] - families = ["background_batch", "render_mov_batch"] + families = ["background_batch"] # presets default_subset_task = { - "background_batch": "background", - "render_mov_batch": "compositing" + "background_batch": "background" } subsets = { "background_batch": { @@ -30,12 +29,6 @@ class CollectBatchInstances(pyblish.api.InstancePlugin): "task": "background", "family": "workfile" } - }, - "render_mov_batch": { - "renderCompositingDefault": { - "task": "compositing", - "family": "render" - } } } unchecked_by_default = [] diff --git a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py new file mode 100644 index 00000000000..0876520cf7d --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py @@ -0,0 +1,62 @@ +import copy +import pyblish.api +from pprint import pformat + + +class CollectBulkMovInstances(pyblish.api.InstancePlugin): + """Collect all available instances for batch publish.""" + + label = "Collect Bulk Mov Instances" + order = pyblish.api.CollectorOrder + 0.489 + hosts = ["standalonepublisher"] + families = ["render_mov_batch"] + + # presets + default_subset_task = { + "render_mov_batch": "compositing" + } + subsets = { + "render_mov_batch": { + "renderCompositingDefault": { + "task": "compositing", + "family": "render" + } + } + } + unchecked_by_default = [] + + def process(self, instance): + context = instance.context + asset_name = instance.data["asset"] + family = instance.data["family"] + + default_task_name = self.default_subset_task.get(family) + for subset_name, subset_data in self.subsets[family].items(): + instance_name = f"{asset_name}_{subset_name}" + task_name = subset_data.get("task") or default_task_name + + # create new instance + new_instance = context.create_instance(instance_name) + + # add original instance data except name key + for key, value in instance.data.items(): + if key not in ["name"]: + # Make sure value is copy since value may be object which + # can be shared across all new created objects + new_instance.data[key] = copy.deepcopy(value) + + # add subset data from preset + new_instance.data.update(subset_data) + + new_instance.data["label"] = instance_name + new_instance.data["subset"] = subset_name + new_instance.data["task"] = task_name + + if subset_name in self.unchecked_by_default: + new_instance.data["publish"] = False + + self.log.info(f"Created new instance: {instance_name}") + self.log.debug(f"_ inst_data: {pformat(new_instance.data)}") + + # delete original instance + context.remove(instance) From 62ff8f0817b912987e54ed83061bbac3e9bc4947 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 12:54:01 +0200 Subject: [PATCH 137/264] moved `get_subset_name` logic from pype.plugin to pype.lib --- pype/lib/__init__.py | 4 + pype/lib/plugin_tools.py | 89 ++++++++++++++++++- pype/plugin.py | 73 +-------------- .../widgets/widget_family.py | 2 +- 4 files changed, 96 insertions(+), 72 deletions(-) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 7edc360c149..8b4b3d790b8 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -29,6 +29,8 @@ from .profiles_filtering import filter_profiles from .plugin_tools import ( + TaskNotSetError, + get_subset_name, filter_pyblish_plugins, source_hash, get_unique_layer_name, @@ -73,6 +75,8 @@ "filter_profiles", + "TaskNotSetError", + "get_subset_name", "filter_pyblish_plugins", "get_unique_layer_name", "get_background_layers", diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index d310ac2b8de..4ec4dd0ffae 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -7,12 +7,99 @@ import json import pype.api import tempfile - +from . import filter_profiles from ..api import config log = logging.getLogger(__name__) +# Subset name template used when plugin does not have defined any +DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" + + +class TaskNotSetError(KeyError): + """Exception raised when subset name requires task name but is not set.""" + def __init__(self, msg=None): + if not msg: + msg = "Creator's subset name template requires task name." + super(TaskNotSetError, self).__init__(msg) + + +def get_subset_name( + family, + variant, + task_name, + asset_id, + project_name=None, + host_name=None, + default_template=None +): + if not family: + return "" + + if not host_name: + host_name = os.environ["AVALON_APP"] + + if project_name is None: + project_name = os.environ["AVALON_PROJECT"] + + # Use only last part of class family value split by dot (`.`) + family = family.rsplit(".", 1)[-1] + + # Get settings + profiles = ( + config.get_presets(project_name) + .get("tools", {}) + .get("creator_subset_name_profiles") + ) or [] + filtering_criteria = { + "families": family, + "hosts": host_name, + "tasks": task_name + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + template = None + if matching_profile: + template = matching_profile["template"] + + # Make sure template is set (matching may have empty string) + if not template: + template = default_template or DEFAULT_SUBSET_TEMPLATE + + # Simple check of task name existence for template with {task} in + # - missing task should be possible only in Standalone publisher + if not task_name and "{task" in template.lower(): + raise TaskNotSetError() + + fill_pairs = ( + ("variant", variant), + ("family", family), + ("task", task_name) + ) + fill_data = {} + for key, value in fill_pairs: + # Handle cases when value is `None` (standalone publisher) + if value is None: + continue + # Keep value as it is + fill_data[key] = value + # Both key and value are with upper case + fill_data[key.upper()] = value.upper() + + # Capitalize only first char of value + # - conditions are because of possible index errors + 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:] + fill_data[key.capitalize()] = capitalized + + return template.format(**fill_data) + def filter_pyblish_plugins(plugins): """Filter pyblish plugins by presets. diff --git a/pype/plugin.py b/pype/plugin.py index ac76c1cbe8b..f151dcec46b 100644 --- a/pype/plugin.py +++ b/pype/plugin.py @@ -4,7 +4,7 @@ import pyblish.api import avalon.api from pype.api import config -from pype.lib import filter_profiles +from pype.lib import get_subset_name ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 @@ -38,86 +38,19 @@ def imprint_attributes(plugin): print("setting {}: {} on {}".format(option, value, plugin_name)) -class TaskNotSetError(KeyError): - def __init__(self, msg=None): - if not msg: - msg = "Creator's subset name template requires task name." - super(TaskNotSetError, self).__init__(msg) - - class PypeCreatorMixin: """Helper to override avalon's default class methods. Mixin class must be used as first in inheritance order to override methods. """ - default_tempate = "{family}{Variant}" @classmethod def get_subset_name( cls, variant, task_name, asset_id, project_name, host_name=None ): - if not cls.family: - return "" - - if not host_name: - host_name = os.environ["AVALON_APP"] - - # Use only last part of class family value split by dot (`.`) - family = cls.family.rsplit(".", 1)[-1] - - # Get settings - profiles = ( - config.get_presets(project_name) - .get("tools", {}) - .get("creator_subset_name_profiles") - ) or [] - filtering_criteria = { - "families": family, - "hosts": host_name, - "tasks": task_name - } - - matching_profile = filter_profiles(profiles, filtering_criteria) - template = None - if matching_profile: - template = matching_profile["template"] - - # Make sure template is set (matching may have empty string) - if not template: - template = cls.default_tempate - - # Simple check of task name existence for template with {task} in - # - missing task should be possible only in Standalone publisher - if not task_name and "{task" in template.lower(): - raise TaskNotSetError() - - fill_pairs = ( - ("variant", variant), - ("family", family), - ("task", task_name) + return get_subset_name( + cls.family, variant, task_name, asset_id, project_name, host_name ) - fill_data = {} - for key, value in fill_pairs: - # Handle cases when value is `None` (standalone publisher) - if value is None: - continue - # Keep value as it is - fill_data[key] = value - # Both key and value are with upper case - fill_data[key.upper()] = value.upper() - - # Capitalize only first char of value - # - conditions are because of possible index errors - 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:] - fill_data[key.capitalize()] = capitalized - - return template.format(**fill_data) class Creator(PypeCreatorMixin, avalon.api.Creator): diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 5268611b383..077371030e8 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -8,7 +8,7 @@ config, Creator ) -from pype.plugin import TaskNotSetError +from pype.lib import TaskNotSetError from avalon.tools.creator.app import SubsetAllowedSymbols From 0bd983972c44b4b672b809a9f0917c4471a25efa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 12:54:43 +0200 Subject: [PATCH 138/264] modified bulk mov to use `get_subset_name` from pype.lib to get subset name --- .../publish/collect_bulk_mov_instances.py | 103 +++++++++++------- 1 file changed, 66 insertions(+), 37 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py index 0876520cf7d..537e8e0d727 100644 --- a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py @@ -1,6 +1,9 @@ import copy +import json import pyblish.api -from pprint import pformat + +from avalon import io +from pype.lib import get_subset_name class CollectBulkMovInstances(pyblish.api.InstancePlugin): @@ -11,52 +14,78 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] families = ["render_mov_batch"] - # presets - default_subset_task = { - "render_mov_batch": "compositing" - } - subsets = { - "render_mov_batch": { - "renderCompositingDefault": { - "task": "compositing", - "family": "render" - } - } - } - unchecked_by_default = [] + new_instance_family = "render" + instance_task_names = [ + "compositing", + "comp" + ] + default_task_name = "compositing" + subset_name_variant = "Default" def process(self, instance): context = instance.context asset_name = instance.data["asset"] - family = instance.data["family"] - default_task_name = self.default_subset_task.get(family) - for subset_name, subset_data in self.subsets[family].items(): - instance_name = f"{asset_name}_{subset_name}" - task_name = subset_data.get("task") or default_task_name + asset_doc = io.find_one( + { + "type": "asset", + "name": asset_name + }, + {"data.tasks": 1} + ) + if not asset_doc: + raise AssertionError(( + "Couldn't find Asset document with name \"{}\"" + ).format(asset_name)) - # create new instance - new_instance = context.create_instance(instance_name) + available_task_names = {} + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + for task_name in asset_tasks.keys(): + available_task_names[task_name.lower()] = task_name - # add original instance data except name key - for key, value in instance.data.items(): - if key not in ["name"]: - # Make sure value is copy since value may be object which - # can be shared across all new created objects - new_instance.data[key] = copy.deepcopy(value) + task_name = self.default_task_name + for _task_name in self.instance_task_names: + _task_name_low = _task_name.lower() + if _task_name_low in available_task_names: + task_name = available_task_names[_task_name_low] + break - # add subset data from preset - new_instance.data.update(subset_data) + subset_name = get_subset_name( + self.new_instance_family, + self.subset_name_variant, + task_name, + asset_doc["_id"], + io.Session["AVALON_PROJECT"] + ) + instance_name = f"{asset_name}_{subset_name}" - new_instance.data["label"] = instance_name - new_instance.data["subset"] = subset_name - new_instance.data["task"] = task_name - - if subset_name in self.unchecked_by_default: - new_instance.data["publish"] = False + # create new instance + new_instance = context.create_instance(instance_name) + new_instance_data = { + "name": instance_name, + "label": instance_name, + "family": self.new_instance_family, + "subset": subset_name, + "task": task_name + } + new_instance.data.update(new_instance_data) + # add original instance data except name key + for key, value in instance.data.items(): + if key in new_instance_data: + continue + # Make sure value is copy since value may be object which + # can be shared across all new created objects + new_instance.data[key] = copy.deepcopy(value) - self.log.info(f"Created new instance: {instance_name}") - self.log.debug(f"_ inst_data: {pformat(new_instance.data)}") # delete original instance context.remove(instance) + + self.log.info(f"Created new instance: {instance_name}") + + def convertor(value): + return str(value) + + self.log.debug("Instance data: {}".format( + json.dumps(new_instance.data, indent=4, default=convertor) + )) From 11ed498d2508fa58197302ef38b1b54c6a852fe1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 12:55:00 +0200 Subject: [PATCH 139/264] implemented validator of task name existence on asset document --- .../publish/validate_task_existence.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pype/plugins/standalonepublisher/publish/validate_task_existence.py diff --git a/pype/plugins/standalonepublisher/publish/validate_task_existence.py b/pype/plugins/standalonepublisher/publish/validate_task_existence.py new file mode 100644 index 00000000000..e93a93a57de --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_task_existence.py @@ -0,0 +1,54 @@ +import collections +import pyblish.api +from avalon import io + + +class ValidateTaskExistence(pyblish.api.ContextPlugin): + """Validating tasks on instances are filled and existing.""" + + label = "Validate Task Existence" + order = pyblish.api.ValidatorOrder + + hosts = ["standalonepublisher"] + families = ["render_mov_batch"] + + def process(self, context): + asset_names = set() + for instance in context: + asset_names.add(instance.data["asset"]) + + asset_docs = io.find( + { + "type": "asset", + "name": {"$in": list(asset_names)} + }, + {"data.tasks": 1} + ) + tasks_by_asset_names = {} + for asset_doc in asset_docs: + asset_name = asset_doc["name"] + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + tasks_by_asset_names[asset_name] = list(asset_tasks.keys()) + + missing_tasks = [] + for instance in context: + asset_name = instance.data["asset"] + task_name = instance.data["task"] + task_names = tasks_by_asset_names.get(asset_name) or [] + if task_name and task_name in task_names: + continue + missing_tasks.append((asset_name, task_name)) + + # Everything is OK + if not missing_tasks: + return + + # Raise an exception + msg = "Couldn't find task name/s required for publishing.\n{}" + pair_msgs = [] + for missing_pair in missing_tasks: + pair_msgs.append( + "Asset: \"{}\" Task: \"{}\"".format(*missing_pair) + ) + + raise AssertionError(msg.format("\n".join(pair_msgs))) From 9d89d675b07cfcb18d19680cb8fc235de8ae5f29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 12:55:43 +0200 Subject: [PATCH 140/264] keep "render_mov_batch" family on new instance --- .../standalonepublisher/publish/collect_bulk_mov_instances.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py index 537e8e0d727..880e10ec770 100644 --- a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py @@ -77,6 +77,10 @@ def process(self, instance): # can be shared across all new created objects new_instance.data[key] = copy.deepcopy(value) + # Add `render_mov_batch` for specific validators + if "families" not in new_instance.data: + new_instance.data["families"] = [] + new_instance.data["families"].append("render_mov_batch") # delete original instance context.remove(instance) From 48db9e1ba886f9b61d75b941cc2c5311a2cef06a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 13:05:27 +0200 Subject: [PATCH 141/264] show error message as message in error detail --- pype/tools/pyblish_pype/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index ec9689381ed..2cafef1f0cf 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1012,7 +1012,9 @@ def append(self, record_items): all_record_items = [] for record_item in record_items: record_type = record_item["type"] - + # Add error message to detail + if record_type == "error": + record_item["msg"] = record_item["label"] terminal_item_type = None if record_type == "record": for level, _type in self.level_to_record: From f06395adcd282c15bd58026e89284d1f06aa0b44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Mar 2021 13:40:55 +0200 Subject: [PATCH 142/264] fix projections --- .../publish/collect_bulk_mov_instances.py | 5 ++++- .../standalonepublisher/publish/validate_task_existence.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py index 880e10ec770..cbb9d95e01c 100644 --- a/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_bulk_mov_instances.py @@ -31,7 +31,10 @@ def process(self, instance): "type": "asset", "name": asset_name }, - {"data.tasks": 1} + { + "_id": 1, + "data.tasks": 1 + } ) if not asset_doc: raise AssertionError(( diff --git a/pype/plugins/standalonepublisher/publish/validate_task_existence.py b/pype/plugins/standalonepublisher/publish/validate_task_existence.py index e93a93a57de..8bd4fb997af 100644 --- a/pype/plugins/standalonepublisher/publish/validate_task_existence.py +++ b/pype/plugins/standalonepublisher/publish/validate_task_existence.py @@ -22,7 +22,10 @@ def process(self, context): "type": "asset", "name": {"$in": list(asset_names)} }, - {"data.tasks": 1} + { + "name": 1, + "data.tasks": 1 + } ) tasks_by_asset_names = {} for asset_doc in asset_docs: From 55cdeb2ef7eb93c4a32e821b1e4df42fb4d455dd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 30 Mar 2021 10:48:31 +0200 Subject: [PATCH 143/264] check for full match instead of simple match --- pype/lib/profiles_filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib/profiles_filtering.py b/pype/lib/profiles_filtering.py index 05bd07eee63..efa0c6ebea0 100644 --- a/pype/lib/profiles_filtering.py +++ b/pype/lib/profiles_filtering.py @@ -105,7 +105,7 @@ def validate_value_by_regexes(value, in_list): regexes = compile_list_of_regexes(in_list) for regex in regexes: - if re.match(regex, value): + if re.fullmatch(regex, value): return 1 return -1 From 815babec53438b27c46885d1af43a107d2d7df27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Mar 2021 12:21:28 +0200 Subject: [PATCH 144/264] Fix - modified path of plugin loads for Harmony and TVPaint It was pointing to 3.0 structure ('.hosts.api') previously --- pype/plugins/harmony/create/create_farm_render.py | 2 +- pype/plugins/harmony/create/create_render.py | 2 +- pype/plugins/harmony/create/create_template.py | 2 +- pype/plugins/tvpaint/create/create_render_layer.py | 2 +- pype/plugins/tvpaint/create/create_render_pass.py | 2 +- pype/plugins/tvpaint/create/create_review.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/plugins/harmony/create/create_farm_render.py b/pype/plugins/harmony/create/create_farm_render.py index a1b198b672d..dc8d7741477 100644 --- a/pype/plugins/harmony/create/create_farm_render.py +++ b/pype/plugins/harmony/create/create_farm_render.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Create Composite node for render on farm.""" from avalon import harmony -from pype.hosts.harmony.api import plugin +from pype.hosts.harmony import plugin class CreateFarmRender(plugin.Creator): diff --git a/pype/plugins/harmony/create/create_render.py b/pype/plugins/harmony/create/create_render.py index b9a0987b37d..5df9ce2e5c9 100644 --- a/pype/plugins/harmony/create/create_render.py +++ b/pype/plugins/harmony/create/create_render.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Create render node.""" from avalon import harmony -from pype.hosts.harmony.api import plugin +from pype.hosts.harmony import plugin class CreateRender(plugin.Creator): diff --git a/pype/plugins/harmony/create/create_template.py b/pype/plugins/harmony/create/create_template.py index 880cc82405b..1456d4271ca 100644 --- a/pype/plugins/harmony/create/create_template.py +++ b/pype/plugins/harmony/create/create_template.py @@ -1,5 +1,5 @@ from avalon import harmony -from pype.hosts.harmony.api import plugin +from pype.hosts.harmony import plugin class CreateTemplate(plugin.Creator): diff --git a/pype/plugins/tvpaint/create/create_render_layer.py b/pype/plugins/tvpaint/create/create_render_layer.py index ed7c96c9047..7787a3023c3 100644 --- a/pype/plugins/tvpaint/create/create_render_layer.py +++ b/pype/plugins/tvpaint/create/create_render_layer.py @@ -1,5 +1,5 @@ from avalon.tvpaint import pipeline, lib -from pype.hosts.tvpaint.api import plugin +from pype.hosts.tvpaint import plugin class CreateRenderlayer(plugin.Creator): diff --git a/pype/plugins/tvpaint/create/create_render_pass.py b/pype/plugins/tvpaint/create/create_render_pass.py index 8583f204517..9066be53533 100644 --- a/pype/plugins/tvpaint/create/create_render_pass.py +++ b/pype/plugins/tvpaint/create/create_render_pass.py @@ -1,5 +1,5 @@ from avalon.tvpaint import pipeline, lib -from pype.hosts.tvpaint.api import plugin +from pype.hosts.tvpaint import plugin class CreateRenderPass(plugin.Creator): diff --git a/pype/plugins/tvpaint/create/create_review.py b/pype/plugins/tvpaint/create/create_review.py index cfc49a8ac6f..fc8f6c0c733 100644 --- a/pype/plugins/tvpaint/create/create_review.py +++ b/pype/plugins/tvpaint/create/create_review.py @@ -1,5 +1,5 @@ from avalon.tvpaint import pipeline -from pype.hosts.tvpaint.api import plugin +from pype.hosts.tvpaint import plugin class CreateReview(plugin.Creator): From a51c8f0501d1574b095e35f43fcb4a8f9a73cd12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 30 Mar 2021 14:04:24 +0200 Subject: [PATCH 145/264] aded python 2 compatibility for fullmatch --- pype/lib/profiles_filtering.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pype/lib/profiles_filtering.py b/pype/lib/profiles_filtering.py index efa0c6ebea0..9e909d6d64d 100644 --- a/pype/lib/profiles_filtering.py +++ b/pype/lib/profiles_filtering.py @@ -77,6 +77,14 @@ def _profile_exclusion(matching_profiles, logger): return matching_profiles[0][0] +def fullmatch(regex, string, flags=0): + """Emulate python-3.4 re.fullmatch().""" + matched = re.match(regex, string, flags=flags) + if matched and matched.span()[1] == len(string): + return matched + return None + + def validate_value_by_regexes(value, in_list): """Validates in any regex from list match entered value. @@ -105,7 +113,11 @@ def validate_value_by_regexes(value, in_list): regexes = compile_list_of_regexes(in_list) for regex in regexes: - if re.fullmatch(regex, value): + if hasattr(regex, "fullmatch"): + result = regex.fullmatch(value) + else: + result = fullmatch(regex, value) + if result: return 1 return -1 From 540ca309f2563a71486419b13908f3796b8a83e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Mar 2021 17:41:34 +0200 Subject: [PATCH 146/264] Fix - handle duplication of Task name If Task name is explicitly set in template, it duplicated it. Task name doesnt need to be in template, but by standard it should be in subset name. This whole replace situation is because of avalon's Creator which modify subset name even if it shouldn't. If Creator app is reworked (could have wide impact!), this should be cleaned up. --- pype/plugins/harmony/publish/collect_farm_render.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/plugins/harmony/publish/collect_farm_render.py b/pype/plugins/harmony/publish/collect_farm_render.py index 98706ad951a..c283b7f8da4 100644 --- a/pype/plugins/harmony/publish/collect_farm_render.py +++ b/pype/plugins/harmony/publish/collect_farm_render.py @@ -124,10 +124,16 @@ def get_instances(self, context): # TODO: handle pixel aspect and frame step # TODO: set Deadline stuff (pools, priority, etc. by presets) # because of using 'renderFarm' as a family, replace 'Farm' with - # capitalized task name - subset_name = node.split("/")[1].replace( + # capitalized task name - issue of avalon-core Creator app + subset_name = node.split("/")[1] + task_name = context.data["anatomyData"]["task"].capitalize() + replace_str = "" + if task_name.lower() not in subset_name.lower(): + replace_str = task_name + subset_name = subset_name.replace( 'Farm', - context.data["anatomyData"]["task"].capitalize()) + replace_str) + render_instance = HarmonyRenderInstance( version=version, time=api.time(), From 4d39e4fa86573156bea5546ec33ae03c8d6dd370 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 31 Mar 2021 09:59:59 +0200 Subject: [PATCH 147/264] usage of app names from project config can expect app names with slashes --- pype/modules/ftrack/lib/ftrack_app_handler.py | 3 ++- pype/tools/launcher/lib.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 23776aced7b..a4d305d673c 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -101,7 +101,8 @@ def discover(self, session, entities, event): project_apps_config = avalon_project_doc["config"].get("apps", []) avalon_project_apps = [ - app["name"] for app in project_apps_config + app["name"].replace("/", "_") + for app in project_apps_config ] or False event["data"]["avalon_project_apps"] = avalon_project_apps diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index a6d6ff68654..ebd5065a01f 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -38,7 +38,7 @@ def get_application_actions(project): apps = [] for app in project["config"]["apps"]: try: - app_name = app["name"] + app_name = app["name"].replace("/", "_") app_definition = lib.get_application(app_name) except Exception as exc: print("Unable to load application: %s - %s" % (app['name'], exc)) From e9f5ac08c45c70f54254d516cfea35a829516011 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 31 Mar 2021 12:42:18 +0200 Subject: [PATCH 148/264] AE - added SubsetManager --- .../websocket_server/hosts/aftereffects.py | 3 ++ .../stubs/aftereffects_server_stub.py | 41 ++++++++++++++++++- .../aftereffects/create/create_render.py | 31 +++++++++----- .../aftereffects/load/load_background.py | 5 ++- pype/plugins/aftereffects/load/load_file.py | 5 ++- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/pype/modules/websocket_server/hosts/aftereffects.py b/pype/modules/websocket_server/hosts/aftereffects.py index 14d2c04338d..9ffb9407c23 100644 --- a/pype/modules/websocket_server/hosts/aftereffects.py +++ b/pype/modules/websocket_server/hosts/aftereffects.py @@ -54,6 +54,9 @@ async def sceneinventory_route(self): async def projectmanager_route(self): self._tool_route("projectmanager") + async def subsetmanager_route(self): + self._tool_route("subsetmanager") + def _tool_route(self, tool_name): """The address accessed when clicking on the buttons.""" partial_method = functools.partial(aftereffects.show, tool_name) diff --git a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py index 9449d0b3780..5d8debf2296 100644 --- a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py +++ b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py @@ -36,6 +36,8 @@ class AfterEffectsServerStub(): is opened). 'self.websocketserver.call' is used as async wrapper """ + PUBLISH_ICON = '\u2117 ' + LOADED_ICON = '\u25bc' def __init__(self): self.websocketserver = WebSocketServer.get_instance() @@ -61,7 +63,7 @@ def get_metadata(self): by Creator. Returns: - (dict) + (list) """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_metadata') @@ -212,6 +214,19 @@ def get_selected_items(self, comps, folders=False, footages=False): ) return self._to_records(res) + def get_item(self, item_id): + """ + Returns metadata for particular 'item_id' or None + + Args: + item_id (int, or string) + """ + for item in self.get_items(True, True, True): + if str(item.id) == str(item_id): + return item + + return None + def import_file(self, path, item_name, import_options=None): """ Imports file as a FootageItem. Used in Loader @@ -272,6 +287,30 @@ def delete_item(self, item_id): item_id=item_id )) + def remove_instance(self, instance_id): + """ + Removes instance with 'instance_id' from file's metadata and + saves them. + + Keep matching item in file though. + + Args: + instance_id(string): instance uuid + """ + cleaned_data = [] + + for instance in self.get_metadata(): + uuid_val = instance.get("uuid") + if not uuid_val: + uuid_val = instance.get("members")[0] # legacy + if uuid_val != instance_id: + cleaned_data.append(instance) + + payload = json.dumps(cleaned_data, indent=4) + self.websocketserver.call(self.client.call + ('AfterEffects.imprint', payload=payload) + ) + def is_saved(self): # TODO return True diff --git a/pype/plugins/aftereffects/create/create_render.py b/pype/plugins/aftereffects/create/create_render.py index b346bc60d82..70abd364065 100644 --- a/pype/plugins/aftereffects/create/create_render.py +++ b/pype/plugins/aftereffects/create/create_render.py @@ -8,7 +8,13 @@ class CreateRender(pype.api.Creator): - """Render folder for publish.""" + """Render folder for publish. + + Creates subsets in format 'familyTaskSubsetname', + eg 'renderCompositingMain'. + + Create only single instance from composition at a time. + """ name = "renderDefault" label = "Render on Farm" @@ -20,7 +26,7 @@ def process(self): items = stub.get_selected_items(comps=True, folders=False, footages=False) - else: + if len(items) > 1: self._show_msg("Please select only single composition at time.") return False @@ -30,15 +36,20 @@ def process(self): "one composition.") return False - for item in items: + existing_subsets = [instance['subset'].lower() + for instance in aftereffects.list_instances()] + + item = items.pop() + if self.name.lower() in existing_subsets: txt = "Instance with name \"{}\" already exists.".format(self.name) - if self.name.lower() == item.name.lower(): - self._show_msg(txt) - return False - self.data["members"] = [item.id] - stub.imprint(item, self.data) - stub.set_label_color(item.id, 14) # Cyan options 0 - 16 - stub.rename_item(item, self.data["subset"]) + self._show_msg(txt) + return False + + self.data["members"] = [item.id] + self.data["uuid"] = item.id # for SubsetManager + stub.imprint(item, self.data) + stub.set_label_color(item.id, 14) # Cyan options 0 - 16 + stub.rename_item(item, stub.PUBLISH_ICON + self.data["subset"]) def _show_msg(self, txt): msg = Qt.QtWidgets.QMessageBox() diff --git a/pype/plugins/aftereffects/load/load_background.py b/pype/plugins/aftereffects/load/load_background.py index 879734e4f9a..e6f8b6a0326 100644 --- a/pype/plugins/aftereffects/load/load_background.py +++ b/pype/plugins/aftereffects/load/load_background.py @@ -29,7 +29,8 @@ def load(self, context, name=None, namespace=None, data=None): "{}_{}".format(context["asset"]["name"], name)) layers = get_background_layers(self.fname) - comp = stub.import_background(None, comp_name, layers) + comp = stub.import_background(None, stub.LOADED_ICON + comp_name, + layers) if not comp: self.log.warning( @@ -72,7 +73,7 @@ def update(self, container, representation): layers = get_background_layers(path) comp = stub.reload_background(container["members"][1], - comp_name, + stub.LOADED_ICON + comp_name, layers) # update container diff --git a/pype/plugins/aftereffects/load/load_file.py b/pype/plugins/aftereffects/load/load_file.py index ba118566789..500a53a69b8 100644 --- a/pype/plugins/aftereffects/load/load_file.py +++ b/pype/plugins/aftereffects/load/load_file.py @@ -48,7 +48,8 @@ def load(self, context, name=None, namespace=None, data=None): if '.psd' in file: import_options['ImportAsType'] = 'ImportAsType.COMP' - comp = stub.import_file(self.fname, comp_name, import_options) + comp = stub.import_file(self.fname, stub.LOADED_ICON + comp_name, + import_options) if not comp: self.log.warning( @@ -87,7 +88,7 @@ def update(self, container, representation): layer_name = container["namespace"] path = api.get_representation_path(representation) # with aftereffects.maintained_selection(): # TODO - stub.replace_item(layer, path, layer_name) + stub.replace_item(layer, path, stub.LOADED_ICON + layer_name) stub.imprint( layer, {"representation": str(representation["_id"]), "name": context["subset"], From bda4ba89720eae138eb5e682c7897d72727e3c64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 1 Apr 2021 11:13:04 +0200 Subject: [PATCH 149/264] ftrack group can be also `openpype` --- pype/modules/ftrack/lib/avalon_sync.py | 2 +- pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index ca3df64fe3e..c52a45073b8 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -86,7 +86,7 @@ def get_pype_attr(session, split_hierarchical=True): cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name in (\"avalon\", \"pype\")" + " where group.name in (\"avalon\", \"pype\", \"openpype\")" ) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index e8aa87a6f9a..8f3a2a129d2 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -17,7 +17,7 @@ def get_pype_attr(session, split_hierarchical=True): cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name in (\"avalon\", \"pype\")" + " where group.name in (\"avalon\", \"pype\", \"openpype\")" ) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: From 8c9314ac6bdad37086fed83df384ad7ce1fbd3f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 1 Apr 2021 11:23:40 +0200 Subject: [PATCH 150/264] fix schema names in code --- pype/plugins/global/publish/extract_hierarchy_avalon.py | 4 ++-- pype/tools/assetcreator/app.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index 74751c68074..a9e729b6623 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -148,7 +148,7 @@ def unarchive_entity(self, entity, data): # Unarchived asset should not use same data new_entity = { "_id": entity["_id"], - "schema": "avalon-core:asset-3.0", + "schema": "pype:asset-3.0", "name": entity["name"], "parent": self.project["_id"], "type": "asset", @@ -162,7 +162,7 @@ def unarchive_entity(self, entity, data): def create_avalon_asset(self, name, data): item = { - "schema": "avalon-core:asset-3.0", + "schema": "pype:asset-3.0", "name": name, "parent": self.project["_id"], "type": "asset", diff --git a/pype/tools/assetcreator/app.py b/pype/tools/assetcreator/app.py index 71b1027ef49..2bef5a88ee2 100644 --- a/pype/tools/assetcreator/app.py +++ b/pype/tools/assetcreator/app.py @@ -396,7 +396,7 @@ def create_asset(self): new_asset_info = { 'parent': av_project['_id'], 'name': name, - 'schema': "avalon-core:asset-3.0", + 'schema': "pype:asset-3.0", 'type': 'asset', 'data': new_asset_data } From c4fc31aede6f2ab128b6219f9a74cbddd6aa758d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Apr 2021 16:04:55 +0200 Subject: [PATCH 151/264] AE remove orphaned instance from workfile Orphaned instance points to composition that doesn't exist anymore. Delete that instance, log warning --- pype/plugins/aftereffects/publish/collect_render.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/aftereffects/publish/collect_render.py b/pype/plugins/aftereffects/publish/collect_render.py index 7f7d5a52bc4..8c80ab928ea 100644 --- a/pype/plugins/aftereffects/publish/collect_render.py +++ b/pype/plugins/aftereffects/publish/collect_render.py @@ -44,6 +44,12 @@ def get_instances(self, context): "Please recreate instance.") item_id = inst["members"][0] work_area_info = aftereffects.stub().get_work_area(int(item_id)) + + if not work_area_info: + self.log.warning("Orphaned instance, deleting metadata") + self.stub.remove_instance(int(item_id)) + continue + frameStart = work_area_info.workAreaStart frameEnd = round(work_area_info.workAreaStart + From 8a75d3f0636a89929449aabd52101076e0f18ee0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Apr 2021 16:11:26 +0200 Subject: [PATCH 152/264] AE remove orphaned instance from workfile Orphaned instance points to composition that doesn't exist anymore. Delete that instance, log warning --- pype/plugins/aftereffects/publish/collect_render.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/aftereffects/publish/collect_render.py b/pype/plugins/aftereffects/publish/collect_render.py index 7f7d5a52bc4..8c80ab928ea 100644 --- a/pype/plugins/aftereffects/publish/collect_render.py +++ b/pype/plugins/aftereffects/publish/collect_render.py @@ -44,6 +44,12 @@ def get_instances(self, context): "Please recreate instance.") item_id = inst["members"][0] work_area_info = aftereffects.stub().get_work_area(int(item_id)) + + if not work_area_info: + self.log.warning("Orphaned instance, deleting metadata") + self.stub.remove_instance(int(item_id)) + continue + frameStart = work_area_info.workAreaStart frameEnd = round(work_area_info.workAreaStart + From e0b7e4d09b6d1682cacc728a214d9b418b2e35ee Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 8 Apr 2021 10:15:17 +0200 Subject: [PATCH 153/264] AE remove orphaned instance from workfile - fix self.stub --- pype/plugins/aftereffects/publish/collect_render.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/plugins/aftereffects/publish/collect_render.py b/pype/plugins/aftereffects/publish/collect_render.py index 8c80ab928ea..e693dff144b 100644 --- a/pype/plugins/aftereffects/publish/collect_render.py +++ b/pype/plugins/aftereffects/publish/collect_render.py @@ -23,6 +23,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): padding_width = 6 rendered_extension = 'png' + stub = aftereffects.stub() + def get_instances(self, context): instances = [] @@ -31,9 +33,9 @@ def get_instances(self, context): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - compositions = aftereffects.stub().get_items(True) + compositions = self.stub.get_items(True) compositions_by_id = {item.id: item for item in compositions} - for inst in aftereffects.stub().get_metadata(): + for inst in self.stub.get_metadata(): schema = inst.get('schema') # loaded asset container skip it if schema and 'container' in schema: @@ -43,7 +45,7 @@ def get_instances(self, context): raise ValueError("Couldn't find id, unable to publish. " + "Please recreate instance.") item_id = inst["members"][0] - work_area_info = aftereffects.stub().get_work_area(int(item_id)) + work_area_info = self.stub.get_work_area(int(item_id)) if not work_area_info: self.log.warning("Orphaned instance, deleting metadata") @@ -119,7 +121,7 @@ def get_expected_files(self, render_instance): end = render_instance.frameEnd # pull file name from Render Queue Output module - render_q = aftereffects.stub().get_render_info() + render_q = self.stub.get_render_info() if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) From 18fd96642c397d67bb3bfbcd8543a27fddfddcbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 9 Apr 2021 11:02:11 +0100 Subject: [PATCH 154/264] Validate project settings - resolution - framerate - pixel aspect - frame range --- .../tvpaint/publish/collect_instances.py | 11 ++++-- .../tvpaint/publish/collect_workfile_data.py | 10 +++--- .../publish/validate_project_settings.py | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 pype/plugins/tvpaint/publish/validate_project_settings.py diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index e03833b96b0..5456f24dfa2 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -18,7 +18,7 @@ def process(self, context): )) for instance_data in workfile_instances: - instance_data["fps"] = context.data["fps"] + instance_data["fps"] = context.data["sceneFps"] # Store workfile instance data to instance data instance_data["originData"] = copy.deepcopy(instance_data) @@ -32,6 +32,11 @@ def process(self, context): subset_name = instance_data["subset"] name = instance_data.get("name", subset_name) instance_data["name"] = name + instance_data["label"] = "{} [{}-{}]".format( + name, + context.data["sceneFrameStart"], + context.data["sceneFrameEnd"] + ) active = instance_data.get("active", True) instance_data["active"] = active @@ -81,8 +86,8 @@ def process(self, context): instance.data["publish"] = any_visible - instance.data["frameStart"] = context.data["frameStart"] - instance.data["frameEnd"] = context.data["frameEnd"] + instance.data["frameStart"] = context.data["sceneFrameStart"] + instance.data["frameEnd"] = context.data["sceneFrameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index 7965112136c..e683c66ea9d 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -127,11 +127,11 @@ def process(self, context): "currentFile": workfile_path, "sceneWidth": width, "sceneHeight": height, - "pixelAspect": pixel_apsect, - "frameStart": frame_start, - "frameEnd": frame_end, - "fps": frame_rate, - "fieldOrder": field_order + "scenePixelAspect": pixel_apsect, + "sceneFrameStart": frame_start, + "sceneFrameEnd": frame_end, + "sceneFps": frame_rate, + "sceneFieldOrder": field_order } self.log.debug( "Scene data: {}".format(json.dumps(scene_data, indent=4)) diff --git a/pype/plugins/tvpaint/publish/validate_project_settings.py b/pype/plugins/tvpaint/publish/validate_project_settings.py new file mode 100644 index 00000000000..fead3393ae9 --- /dev/null +++ b/pype/plugins/tvpaint/publish/validate_project_settings.py @@ -0,0 +1,36 @@ +import json + +import pyblish.api + + +class ValidateProjectSettings(pyblish.api.ContextPlugin): + """Validate project settings against database. + """ + + label = "Validate Project Settings" + order = pyblish.api.ValidatorOrder + optional = True + + def process(self, context): + scene_data = { + "frameStart": context.data.get("sceneFrameStart"), + "frameEnd": context.data.get("sceneFrameEnd"), + "fps": context.data.get("sceneFps"), + "resolutionWidth": context.data.get("sceneWidth"), + "resolutionHeight": context.data.get("sceneHeight"), + "pixelAspect": context.data.get("scenePixelAspect") + } + invalid = {} + for k in scene_data.keys(): + expected_value = context.data["assetEntity"]["data"][k] + if scene_data[k] != expected_value: + invalid[k] = { + "current": scene_data[k], "expected": expected_value + } + + if invalid: + raise AssertionError( + "Project settings does not match database:\n{}".format( + json.dumps(invalid, sort_keys=True, indent=4) + ) + ) From 6f35ce49b21707cd8e749d94fd5e9b440e8b7bcd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 9 Apr 2021 11:14:20 +0100 Subject: [PATCH 155/264] Validate mark in and out. --- .../plugins/tvpaint/publish/validate_marks.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/validate_marks.py diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py new file mode 100644 index 00000000000..e6fd3c05afa --- /dev/null +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -0,0 +1,66 @@ +import json + +import pyblish.api +from avalon.tvpaint import lib + + +class ValidateMarksRepair(pyblish.api.Action): + """Repair the marks.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + expected_data = ValidateMarks().get_expected_data(context) + lib.execute_george("tv_markin {} set".format(expected_data["markIn"])) + lib.execute_george( + "tv_markout {} set".format(expected_data["markOut"]) + ) + + +class ValidateMarks(pyblish.api.ContextPlugin): + """Validate mark in and out are enabled.""" + + label = "Validate Marks" + order = pyblish.api.ValidatorOrder + optional = True + actions = [ValidateMarksRepair] + + def get_expected_data(self, context): + return { + "markIn": context.data["assetEntity"]["data"]["frameStart"] - 1, + "markInState": True, + "markOut": context.data["assetEntity"]["data"]["frameEnd"] - 1, + "markOutState": True + } + + def process(self, context): + # Marks return as "{frame - 1} {state} ", example "0 set". + result = lib.execute_george("tv_markin") + mark_in_frame, mark_in_state, _ = result.split(" ") + + result = lib.execute_george("tv_markout") + mark_out_frame, mark_out_state, _ = result.split(" ") + + current_data = { + "markIn": int(mark_in_frame), + "markInState": mark_in_state == "set", + "markOut": int(mark_out_frame), + "markOutState": mark_out_state == "set" + } + expected_data = self.get_expected_data(context) + invalid = {} + for k in current_data.keys(): + if current_data[k] != expected_data[k]: + invalid[k] = { + "current": current_data[k], + "expected_data": expected_data[k] + } + + if invalid: + raise AssertionError( + "Marks does not match database:\n{}".format( + json.dumps(invalid, sort_keys=True, indent=4) + ) + ) From 986ebd5fa5cc0f932359d950664ae96b063a4416 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 9 Apr 2021 11:20:34 +0100 Subject: [PATCH 156/264] Set initial project settings. --- pype/hooks/tvpaint/prelaunch.py | 3 +++ pype/hosts/tvpaint/__init__.py | 44 ++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pype/hooks/tvpaint/prelaunch.py b/pype/hooks/tvpaint/prelaunch.py index 0b3899f555d..1af079b096e 100644 --- a/pype/hooks/tvpaint/prelaunch.py +++ b/pype/hooks/tvpaint/prelaunch.py @@ -91,6 +91,9 @@ def execute(self, *args, env: dict = None) -> bool: os.path.normpath(workfile_path) ) + # Register the event in the environment for later. + env["PYPE_TVPAINT_LAUNCHED_TEMPLATE_FILE"] = "1" + self.log.info(f"Workfile to open: `{workfile_path}`") # adding compulsory environment var for openting file diff --git a/pype/hosts/tvpaint/__init__.py b/pype/hosts/tvpaint/__init__.py index 7027f0fb559..cd331ab38dd 100644 --- a/pype/hosts/tvpaint/__init__.py +++ b/pype/hosts/tvpaint/__init__.py @@ -2,8 +2,10 @@ import logging from avalon.tvpaint.communication_server import register_localization_file -from avalon.tvpaint import pipeline +from avalon.tvpaint import pipeline, lib +import pype.lib import avalon.api +import avalon.io import pyblish.api from pype import PLUGINS_DIR @@ -31,6 +33,44 @@ def on_instance_toggle(instance, old_value, new_value): pipeline._write_instances(current_instances) +def application_launch(): + # Setup project settings if its the template that's launched. + if "PYPE_TVPAINT_LAUNCHED_TEMPLATE_FILE" in os.environ: + print("Setting up project...") + + project_doc = avalon.io.find_one({"type": "project"}) + project_data = project_doc["data"] + asset_data = pype.lib.get_asset()["data"] + + framerate = asset_data.get("fps", project_data.get("fps", 25)) + + width_key = "resolutionWidth" + height_key = "resolutionHeight" + width = asset_data.get(width_key, project_data.get(width_key, 1920)) + height = asset_data.get(height_key, project_data.get(height_key, 1080)) + + lib.execute_george("tv_resizepage {} {} 0".format(width, height)) + lib.execute_george("tv_framerate {} \"timestretch\"".format(framerate)) + + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + + handles = asset_data.get("handles") or 0 + handle_start = asset_data.get("handleStart") + if handle_start is None: + handle_start = handles + + handle_end = asset_data.get("handleEnd") + if handle_end is None: + handle_end = handles + + frame_start -= int(handle_start) + frame_end += int(handle_end) + + lib.execute_george("tv_markin {} set".format(frame_start - 1)) + lib.execute_george("tv_markout {} set".format(frame_end - 1)) + + def install(): log.info("Pype - Installing TVPaint integration") current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -47,6 +87,8 @@ def install(): if on_instance_toggle not in registered_callbacks: pyblish.api.register_callback("instanceToggled", on_instance_toggle) + avalon.api.on("application.launched", application_launch) + def uninstall(): log.info("Pype - Uninstalling TVPaint integration") From 5b3c1fedc8e30965ab8e087ad18b8e37815785ac Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 9 Apr 2021 11:32:31 +0100 Subject: [PATCH 157/264] Use static method instead of initializing class. --- pype/plugins/tvpaint/publish/validate_marks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index e6fd3c05afa..e34b24d825b 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -12,7 +12,7 @@ class ValidateMarksRepair(pyblish.api.Action): on = "failed" def process(self, context, plugin): - expected_data = ValidateMarks().get_expected_data(context) + expected_data = ValidateMarks.get_expected_data(context) lib.execute_george("tv_markin {} set".format(expected_data["markIn"])) lib.execute_george( "tv_markout {} set".format(expected_data["markOut"]) @@ -27,7 +27,8 @@ class ValidateMarks(pyblish.api.ContextPlugin): optional = True actions = [ValidateMarksRepair] - def get_expected_data(self, context): + @staticmethod + def get_expected_data(context): return { "markIn": context.data["assetEntity"]["data"]["frameStart"] - 1, "markInState": True, @@ -55,7 +56,7 @@ def process(self, context): if current_data[k] != expected_data[k]: invalid[k] = { "current": current_data[k], - "expected_data": expected_data[k] + "expected": expected_data[k] } if invalid: From f2a317b3ef66a627e085fa046d4255a90a9e32e6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 9 Apr 2021 12:35:37 +0100 Subject: [PATCH 158/264] Review fixes - moved settings code to lib. - checks for framerate, resolution and frame range. --- pype/hosts/tvpaint/__init__.py | 49 ++++++++------------------------ pype/hosts/tvpaint/lib.py | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/pype/hosts/tvpaint/__init__.py b/pype/hosts/tvpaint/__init__.py index cd331ab38dd..2026917b3b7 100644 --- a/pype/hosts/tvpaint/__init__.py +++ b/pype/hosts/tvpaint/__init__.py @@ -2,7 +2,9 @@ import logging from avalon.tvpaint.communication_server import register_localization_file -from avalon.tvpaint import pipeline, lib +import avalon.tvpaint.lib +import avalon.tvpaint.pipeline +from . import lib import pype.lib import avalon.api import avalon.io @@ -19,7 +21,7 @@ def on_instance_toggle(instance, old_value, new_value): instance_id = instance.data["uuid"] found_idx = None - current_instances = pipeline.list_instances() + current_instances = avalon.tvpaint.pipeline.list_instances() for idx, workfile_instance in enumerate(current_instances): if workfile_instance["uuid"] == instance_id: found_idx = idx @@ -30,45 +32,16 @@ def on_instance_toggle(instance, old_value, new_value): if "active" in current_instances[found_idx]: current_instances[found_idx]["active"] = new_value - pipeline._write_instances(current_instances) + avalon.tvpaint.pipeline._write_instances(current_instances) -def application_launch(): +def initial_launch(): # Setup project settings if its the template that's launched. - if "PYPE_TVPAINT_LAUNCHED_TEMPLATE_FILE" in os.environ: - print("Setting up project...") - - project_doc = avalon.io.find_one({"type": "project"}) - project_data = project_doc["data"] - asset_data = pype.lib.get_asset()["data"] - - framerate = asset_data.get("fps", project_data.get("fps", 25)) - - width_key = "resolutionWidth" - height_key = "resolutionHeight" - width = asset_data.get(width_key, project_data.get(width_key, 1920)) - height = asset_data.get(height_key, project_data.get(height_key, 1080)) - - lib.execute_george("tv_resizepage {} {} 0".format(width, height)) - lib.execute_george("tv_framerate {} \"timestretch\"".format(framerate)) - - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - - handles = asset_data.get("handles") or 0 - handle_start = asset_data.get("handleStart") - if handle_start is None: - handle_start = handles - - handle_end = asset_data.get("handleEnd") - if handle_end is None: - handle_end = handles - - frame_start -= int(handle_start) - frame_end += int(handle_end) + if os.environ.get("PYPE_TVPAINT_LAUNCHED_TEMPLATE_FILE") != "1": + return - lib.execute_george("tv_markin {} set".format(frame_start - 1)) - lib.execute_george("tv_markout {} set".format(frame_end - 1)) + print("Setting up project...") + lib.set_context_settings(pype.lib.get_asset()) def install(): @@ -87,7 +60,7 @@ def install(): if on_instance_toggle not in registered_callbacks: pyblish.api.register_callback("instanceToggled", on_instance_toggle) - avalon.api.on("application.launched", application_launch) + avalon.api.on("application.launched", initial_launch) def uninstall(): diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py index 4267129fe6b..654773b0f2e 100644 --- a/pype/hosts/tvpaint/lib.py +++ b/pype/hosts/tvpaint/lib.py @@ -1,5 +1,8 @@ from PIL import Image +import avalon.io +import avalon.tvpaint.lib + def composite_images(input_image_paths, output_filepath): """Composite images in order from passed list. @@ -18,3 +21,51 @@ def composite_images(input_image_paths, output_filepath): else: img_obj.alpha_composite(_img_obj) img_obj.save(output_filepath) + + +def set_context_settings(asset): + project = avalon.io.find_one({"_id": asset["parent"]}) + + framerate = asset["data"].get("fps", project["data"].get("fps")) + if framerate: + avalon.tvpaint.lib.execute_george( + "tv_framerate {} \"timestretch\"".format(framerate) + ) + else: + print("Framerate was not found!") + + width_key = "resolutionWidth" + height_key = "resolutionHeight" + width = asset["data"].get(width_key, project["data"].get(width_key)) + height = asset["data"].get(height_key, project["data"].get(height_key)) + if width and height: + avalon.tvpaint.lib.execute_george( + "tv_resizepage {} {} 0".format(width, height) + ) + else: + print("Resolution was not found!") + + frame_start = asset["data"].get("frameStart") + frame_end = asset["data"].get("frameEnd") + + if frame_start and frame_end: + handles = asset["data"].get("handles") or 0 + handle_start = asset["data"].get("handleStart") + if handle_start is None: + handle_start = handles + + handle_end = asset["data"].get("handleEnd") + if handle_end is None: + handle_end = handles + + frame_start -= int(handle_start) + frame_end += int(handle_end) + + avalon.tvpaint.lib.execute_george( + "tv_markin {} set".format(frame_start - 1) + ) + avalon.tvpaint.lib.execute_george( + "tv_markout {} set".format(frame_end - 1) + ) + else: + print("Frame range was not found!") From c1a9c03663ffb28a2ca1d70d265530df0c2a8b85 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 9 Apr 2021 18:16:45 +0200 Subject: [PATCH 159/264] store workfile asset name to context data --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index 7965112136c..47dc1cdbd7a 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -75,6 +75,7 @@ def process(self, context): ) workfile_context = current_context.copy() + context.data["asset"] = avalon.api.Session["AVALON_ASSET"] context.data["workfile_context"] = workfile_context self.log.info("Context changed to: {}".format(workfile_context)) From 26c512e8c618b983755f771dbc2debef17cd5683 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 9 Apr 2021 18:19:37 +0200 Subject: [PATCH 160/264] implemented validator of asset name on instance and context with repair action --- .../tvpaint/publish/validate_asset_name.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/validate_asset_name.py diff --git a/pype/plugins/tvpaint/publish/validate_asset_name.py b/pype/plugins/tvpaint/publish/validate_asset_name.py new file mode 100644 index 00000000000..a920380e23c --- /dev/null +++ b/pype/plugins/tvpaint/publish/validate_asset_name.py @@ -0,0 +1,55 @@ +import pyblish.api +from avalon.tvpaint import pipeline, lib + + +class FixAssetNames(pyblish.api.Action): + """Repair the asset names. + + Change instanace metadata in the workfile. + """ + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + context_asset_name = context.data["asset"] + old_instance_items = pipeline.list_instances() + new_instance_items = [] + for instance_item in old_instance_items: + instance_asset_name = instance_item.get("asset") + if ( + instance_asset_name + and instance_asset_name != context_asset_name + ): + instance_item["asset"] = context_asset_name + new_instance_items.append(instance_item) + pipeline._write_instances(new_instance_items) + + +class ValidateMissingLayers(pyblish.api.ContextPlugin): + """Validate assset name present on instance. + + Asset name on instance should be the same as context's. + """ + + label = "Validate Asset Names" + order = pyblish.api.ValidatorOrder + hosts = ["tvpaint"] + actions = [FixAssetNames] + + def process(self, context): + context_asset_name = context.data["asset"] + for instance in context: + asset_name = instance.data.get("asset") + if asset_name and asset_name == context_asset_name: + continue + + instance_label = ( + instance.data.get("label") or instance.data["name"] + ) + raise AssertionError(( + "Different asset name on instance then context's." + " Instance \"{}\" has asset name: \"{}\"" + " Context asset name is: \"{}\"" + ).format(instance_label, asset_name, context_asset_name)) From ef3190a3d6ab923ecf36b3da1eec86df372e7d37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 9 Apr 2021 18:28:53 +0200 Subject: [PATCH 161/264] removed unused import --- pype/plugins/tvpaint/publish/validate_asset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/validate_asset_name.py b/pype/plugins/tvpaint/publish/validate_asset_name.py index a920380e23c..4ce8d5347de 100644 --- a/pype/plugins/tvpaint/publish/validate_asset_name.py +++ b/pype/plugins/tvpaint/publish/validate_asset_name.py @@ -1,5 +1,5 @@ import pyblish.api -from avalon.tvpaint import pipeline, lib +from avalon.tvpaint import pipeline class FixAssetNames(pyblish.api.Action): From de7ff953d8e67d892eed96e4d33d567234d8398c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 9 Apr 2021 18:32:13 +0200 Subject: [PATCH 162/264] create update custom attributes can ignore missing pypeclub role --- pype/modules/ftrack/actions/action_create_cust_attrs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 21c4743725b..be7d1693605 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -199,6 +199,9 @@ def prepare_global_data(self, session): for role in session.query("SecurityRole").all() } + if "pypeclub" not in self.security_roles: + self.log.info("Pypeclub role is not available. Will be skipped.") + object_types = session.query("ObjectType").all() self.object_types_per_id = { object_type["id"]: object_type for object_type in object_types @@ -752,6 +755,8 @@ def get_security_roles(self, security_roles): for role_name in security_roles_lowered: if role_name in self.security_roles: output.append(self.security_roles[role_name]) + elif role_name == "pypeclub": + continue else: raise CustAttrException(( "Securit role \"{}\" was not found in Ftrack." From da6ae55010d8fe68ee705046307fba5fa6dde222 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 Apr 2021 10:32:24 +0200 Subject: [PATCH 163/264] Nuke: viewer with correct colorspace https://pype.freshdesk.com/a/tickets/604 --- pype/hosts/nuke/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 8ad2030f702..5616d4430ce 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -585,8 +585,7 @@ def set_viewers_colorspace(self, viewer_dict): ] erased_viewers = [] - for v in [n for n in self._nodes - if "Viewer" in n.Class()]: + for v in [n for n in nuke.allNodes(filter="Viewer")]: v['viewerProcess'].setValue(str(viewer_dict["viewerProcess"])) if str(viewer_dict["viewerProcess"]) \ not in v['viewerProcess'].value(): From a370748617dadda39926f8102f136398e6c6e0b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 Apr 2021 12:30:43 +0200 Subject: [PATCH 164/264] Nuke: closing control panel during creation of write group nodes https://pype.freshdesk.com/a/tickets/605 --- pype/hosts/nuke/lib.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 5616d4430ce..864814eff71 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -321,16 +321,20 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): "inputName": input.name()}) prev_node = nuke.createNode( "Input", "name {}".format(input.name())) + prev_node.hideControlPanel() + else: # generic input node connected to nothing prev_node = nuke.createNode( "Input", "name {}".format("rgba")) + prev_node.hideControlPanel() # creating pre-write nodes `prenodes` if prenodes: for name, klass, properties, set_output_to in prenodes: # create node now_node = nuke.createNode(klass, "name {}".format(name)) + now_node.hideControlPanel() # add data to knob for k, v in properties: @@ -352,17 +356,21 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): for i, node_name in enumerate(set_output_to): input_node = nuke.createNode( "Input", "name {}".format(node_name)) + input_node.hideControlPanel() connections.append({ "node": nuke.toNode(node_name), "inputName": node_name}) now_node.setInput(1, input_node) + elif isinstance(set_output_to, str): input_node = nuke.createNode( "Input", "name {}".format(node_name)) + input_node.hideControlPanel() connections.append({ "node": nuke.toNode(set_output_to), "inputName": set_output_to}) now_node.setInput(0, input_node) + else: now_node.setInput(0, prev_node) @@ -374,6 +382,7 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): "inside_{}".format(name), **_data ) + write_node.hideControlPanel() # connect to previous node now_node.setInput(0, prev_node) @@ -382,6 +391,7 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): prev_node = now_node now_node = nuke.createNode("Output", "name Output1") + now_node.hideControlPanel() # connect to previous node now_node.setInput(0, prev_node) From cf4de238f596019ca9fcdee2e25992acca1165bf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 Apr 2021 12:32:07 +0200 Subject: [PATCH 165/264] Nuke: fixing prerender publishing also closing https://github.com/pypeclub/pype/issues/1300 --- pype/plugins/global/publish/submit_publish_job.py | 2 +- pype/plugins/nuke/publish/collect_instances.py | 12 +++++++++--- pype/plugins/nuke/publish/collect_writes.py | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index be2add4ed0c..13e65becfc4 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -130,7 +130,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] - families = ["render.farm", "prerender", + families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", "vrayscene"] aov_filter = {"maya": [r".+(?:\.|_)([Bb]eauty)(?:\.|_).*"], diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 7096437b6bf..6c5cfaedfde 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -78,19 +78,25 @@ def process(self, context): if target == "Use existing frames": # Local rendering self.log.info("flagged for no render") - families.append("render") + families.append(family) elif target == "Local": # Local rendering self.log.info("flagged for local render") - families.append("{}.local".format("render")) + families.append("{}.local".format(family)) elif target == "On farm": # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False - families.append("{}.farm".format("render")) + families.append("{}.farm".format(family)) + + # suffle family to `write` as it is main family + # this will be changed later on in process if "render" in families: families.remove("render") family = "write" + elif "prerender" in families: + families.remove("prerender") + family = "write" node.begin() for i in nuke.allNodes(): diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index d1dad9e001a..a8651066e2d 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -122,6 +122,8 @@ def process(self, instance): # Add version data to instance version_data = { + "families": [f.replace(".local", "").replace(".farm", "") + for f in families if "write" not in f], "colorspace": colorspace, } From b7b370bb89413c5dfae4f8dd4b9c9de063c5cef2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Apr 2021 14:34:51 +0200 Subject: [PATCH 166/264] implemented method to query custom attribute values in chunks --- pype/modules/ftrack/lib/avalon_sync.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index c52a45073b8..74c015f88b4 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1101,6 +1101,41 @@ def set_hierarchical_attribute(self, hier_attrs, sync_ids): self.entities_dict[child_id]["hier_attrs"].update(_hier_values) hier_down_queue.put((_hier_values, child_id)) + def _query_custom_attributes(self, session, conf_ids, entity_ids): + output = [] + # Prepare values to query + attributes_joined = ", ".join([ + "\"{}\"".format(conf_id) for conf_id in conf_ids + ]) + attributes_len = len(conf_ids) + chunk_size = int(5000 / attributes_len) + if chunk_size < 1: + chunk_size = 1 + for idx in range(0, attributes_len, chunk_size): + _entity_ids = entity_ids[idx:idx + chunk_size] + if not _entity_ids: + continue + entity_ids_joined = ", ".join([ + "\"{}\"".format(entity_id) + for entity_id in _entity_ids + ]) + + call_expr = [{ + "action": "query", + "expression": ( + "select value, entity_id from ContextCustomAttributeValue " + "where entity_id in ({}) and configuration_id in ({})" + ).format(entity_ids_joined, attributes_joined) + }] + if hasattr(self.session, "call"): + [result] = self.session.call(call_expr) + else: + [result] = self.session._call(call_expr) + + for item in result["data"]: + output.append(item) + return output + def remove_from_archived(self, mongo_id): entity = self.avalon_archived_by_id.pop(mongo_id, None) if not entity: From 1388d686df0ca35c8135c84831c09f22aaa84c09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Apr 2021 14:36:32 +0200 Subject: [PATCH 167/264] use chunked custom attribute query method to get values --- pype/modules/ftrack/lib/avalon_sync.py | 56 ++++++-------------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 74c015f88b4..23f59115170 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -944,30 +944,13 @@ def set_cutom_attributes(self): copy.deepcopy(prepared_avalon_attr_ca_id) ) - # TODO query custom attributes by entity_id - entity_ids_joined = ", ".join([ - "\"{}\"".format(id) for id in sync_ids - ]) - attributes_joined = ", ".join([ - "\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys() - ]) - - cust_attr_query = ( - "select value, entity_id from ContextCustomAttributeValue " - "where entity_id in ({}) and configuration_id in ({})" + items = self._query_custom_attributes( + self.session, + list(attribute_key_by_id.keys()), + sync_ids ) - call_expr = [{ - "action": "query", - "expression": cust_attr_query.format( - entity_ids_joined, attributes_joined - ) - }] - if hasattr(self.session, "call"): - [values] = self.session.call(call_expr) - else: - [values] = self.session._call(call_expr) - for item in values["data"]: + for item in items: entity_id = item["entity_id"] key = attribute_key_by_id[item["configuration_id"]] store_key = "custom_attributes" @@ -1021,7 +1004,7 @@ def set_hierarchical_attribute(self, hier_attrs, sync_ids): else: prepare_dict[key] = None - for id, entity_dict in self.entities_dict.items(): + for entity_dict in self.entities_dict.values(): # Skip project because has stored defaults at the moment if entity_dict["entity_type"] == "project": continue @@ -1029,27 +1012,14 @@ def set_hierarchical_attribute(self, hier_attrs, sync_ids): for key, val in prepare_dict_avalon.items(): entity_dict["avalon_attrs"][key] = val - # Prepare values to query - entity_ids_joined = ", ".join([ - "\"{}\"".format(id) for id in sync_ids - ]) - attributes_joined = ", ".join([ - "\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys() - ]) - avalon_hier = [] - call_expr = [{ - "action": "query", - "expression": ( - "select value, entity_id from ContextCustomAttributeValue " - "where entity_id in ({}) and configuration_id in ({})" - ).format(entity_ids_joined, attributes_joined) - }] - if hasattr(self.session, "call"): - [values] = self.session.call(call_expr) - else: - [values] = self.session._call(call_expr) + items = self._query_custom_attributes( + self.session, + list(attribute_key_by_id.keys()), + sync_ids + ) - for item in values["data"]: + avalon_hier = [] + for item in items: value = item["value"] # WARNING It is not possible to propage enumerate hierachical # attributes with multiselection 100% right. Unseting all values From acd597140f636a60be164110219b8b97757ec16e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Apr 2021 14:48:26 +0200 Subject: [PATCH 168/264] fix session attribute usage --- pype/modules/ftrack/lib/avalon_sync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 23f59115170..1775df834ab 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1097,10 +1097,10 @@ def _query_custom_attributes(self, session, conf_ids, entity_ids): "where entity_id in ({}) and configuration_id in ({})" ).format(entity_ids_joined, attributes_joined) }] - if hasattr(self.session, "call"): - [result] = self.session.call(call_expr) + if hasattr(session, "call"): + [result] = session.call(call_expr) else: - [result] = self.session._call(call_expr) + [result] = session._call(call_expr) for item in result["data"]: output.append(item) From 51a11b9a24a6d525b34e9737e80778b6eb2f55ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Apr 2021 16:52:26 +0200 Subject: [PATCH 169/264] fix entities range --- pype/modules/ftrack/lib/avalon_sync.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 1775df834ab..a95bc00d77b 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1079,12 +1079,8 @@ def _query_custom_attributes(self, session, conf_ids, entity_ids): ]) attributes_len = len(conf_ids) chunk_size = int(5000 / attributes_len) - if chunk_size < 1: - chunk_size = 1 - for idx in range(0, attributes_len, chunk_size): + for idx in range(0, len(entity_ids), chunk_size): _entity_ids = entity_ids[idx:idx + chunk_size] - if not _entity_ids: - continue entity_ids_joined = ", ".join([ "\"{}\"".format(entity_id) for entity_id in _entity_ids From 137fbeda4b27ce6a65ae878d45507ea6d8d06df9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 11:48:24 +0200 Subject: [PATCH 170/264] fix source of asset name in pyblish context --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index 47dc1cdbd7a..fd9e934ae72 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -75,7 +75,7 @@ def process(self, context): ) workfile_context = current_context.copy() - context.data["asset"] = avalon.api.Session["AVALON_ASSET"] + context.data["asset"] = workfile_context["AVALON_ASSET"] context.data["workfile_context"] = workfile_context self.log.info("Context changed to: {}".format(workfile_context)) From 4f7a879148b4a23620c689959aaadd0db37591f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 12:13:11 +0200 Subject: [PATCH 171/264] asset name is used based on workfile_context existence --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index fd9e934ae72..921129c75e0 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -59,6 +59,7 @@ def process(self, context): self.log.info("Collecting workfile context") workfile_context = pipeline.get_current_workfile_context() if workfile_context: + asset_name = workfile_context["asset"] # Change current context with context from workfile key_map = ( ("AVALON_ASSET", "asset"), @@ -68,6 +69,7 @@ def process(self, context): avalon.api.Session[env_key] = workfile_context[key] os.environ[env_key] = workfile_context[key] else: + asset_name = current_context["asset"] # Handle older workfiles or workfiles without metadata self.log.warning( "Workfile does not contain information about context." @@ -75,7 +77,9 @@ def process(self, context): ) workfile_context = current_context.copy() - context.data["asset"] = workfile_context["AVALON_ASSET"] + # Store context asset name + context.data["asset"] = asset_name + # Store workfile context context.data["workfile_context"] = workfile_context self.log.info("Context changed to: {}".format(workfile_context)) From f7bca423d7335e777e0fc3f05505121e43ea360b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 12:13:52 +0200 Subject: [PATCH 172/264] workfile_context is not used from current_context --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index 921129c75e0..f5cf292d764 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -68,6 +68,8 @@ def process(self, context): for env_key, key in key_map: avalon.api.Session[env_key] = workfile_context[key] os.environ[env_key] = workfile_context[key] + self.log.info("Context changed to: {}".format(workfile_context)) + else: asset_name = current_context["asset"] # Handle older workfiles or workfiles without metadata @@ -75,13 +77,11 @@ def process(self, context): "Workfile does not contain information about context." " Using current Session context." ) - workfile_context = current_context.copy() # Store context asset name context.data["asset"] = asset_name # Store workfile context context.data["workfile_context"] = workfile_context - self.log.info("Context changed to: {}".format(workfile_context)) # Collect instances self.log.info("Collecting instance data from workfile") From cbe646e41784f1d245c5178ce0d61b8d1e045d5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 12:14:05 +0200 Subject: [PATCH 173/264] fix python string formatting --- pype/plugins/tvpaint/publish/collect_workfile_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index f5cf292d764..a68f9477ab9 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -73,10 +73,10 @@ def process(self, context): else: asset_name = current_context["asset"] # Handle older workfiles or workfiles without metadata - self.log.warning( + self.log.warning(( "Workfile does not contain information about context." " Using current Session context." - ) + )) # Store context asset name context.data["asset"] = asset_name From de2745f958fa7d8a3bd4bfb6bd4ddaef7d4469a1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 12:14:32 +0200 Subject: [PATCH 174/264] project name validator won't validate project name if workfile context is not set --- .../tvpaint/publish/validate_workfile_project_name.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/validate_workfile_project_name.py b/pype/plugins/tvpaint/publish/validate_workfile_project_name.py index 7c1032fcada..cc664d8030c 100644 --- a/pype/plugins/tvpaint/publish/validate_workfile_project_name.py +++ b/pype/plugins/tvpaint/publish/validate_workfile_project_name.py @@ -13,7 +13,15 @@ class ValidateWorkfileProjectName(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder def process(self, context): - workfile_context = context.data["workfile_context"] + workfile_context = context.data.get("workfile_context") + # If workfile context is missing than project is matching to + # `AVALON_PROJECT` value for 100% + if not workfile_context: + self.log.info( + "Workfile context (\"workfile_context\") is not filled." + ) + return + workfile_project_name = workfile_context["project"] env_project_name = os.environ["AVALON_PROJECT"] if workfile_project_name == env_project_name: From bf5e6f5946244c3d094fb467d0647de002778ea1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Apr 2021 12:18:41 +0200 Subject: [PATCH 175/264] log information about context --- .../tvpaint/publish/collect_workfile_data.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index a68f9477ab9..af1dd465947 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -57,9 +57,11 @@ def process(self, context): # Collect context from workfile metadata self.log.info("Collecting workfile context") + workfile_context = pipeline.get_current_workfile_context() + # Store workfile context to pyblish context + context.data["workfile_context"] = workfile_context if workfile_context: - asset_name = workfile_context["asset"] # Change current context with context from workfile key_map = ( ("AVALON_ASSET", "asset"), @@ -70,8 +72,12 @@ def process(self, context): os.environ[env_key] = workfile_context[key] self.log.info("Context changed to: {}".format(workfile_context)) + asset_name = workfile_context["asset"] + task_name = workfile_context["task"] + else: asset_name = current_context["asset"] + task_name = current_context["task"] # Handle older workfiles or workfiles without metadata self.log.warning(( "Workfile does not contain information about context." @@ -80,8 +86,11 @@ def process(self, context): # Store context asset name context.data["asset"] = asset_name - # Store workfile context - context.data["workfile_context"] = workfile_context + self.log.info( + "Context is set to Asset: \"{}\" and Task: \"{}\"".format( + asset_name, task_name + ) + ) # Collect instances self.log.info("Collecting instance data from workfile") From e3de5174d14940f774ea33c2ca4541573780da1b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Apr 2021 09:39:49 +0100 Subject: [PATCH 176/264] Accurate frame information to user. --- pype/plugins/tvpaint/publish/validate_marks.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index e34b24d825b..e491d878354 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -13,6 +13,10 @@ class ValidateMarksRepair(pyblish.api.Action): def process(self, context, plugin): expected_data = ValidateMarks.get_expected_data(context) + + expected_data["markIn"] -= 1 + expected_data["markOut"] -= 1 + lib.execute_george("tv_markin {} set".format(expected_data["markIn"])) lib.execute_george( "tv_markout {} set".format(expected_data["markOut"]) @@ -30,9 +34,9 @@ class ValidateMarks(pyblish.api.ContextPlugin): @staticmethod def get_expected_data(context): return { - "markIn": context.data["assetEntity"]["data"]["frameStart"] - 1, + "markIn": int(context.data["frameStart"]), "markInState": True, - "markOut": context.data["assetEntity"]["data"]["frameEnd"] - 1, + "markOut": int(context.data["frameEnd"]), "markOutState": True } @@ -45,9 +49,9 @@ def process(self, context): mark_out_frame, mark_out_state, _ = result.split(" ") current_data = { - "markIn": int(mark_in_frame), + "markIn": int(mark_in_frame) + 1, "markInState": mark_in_state == "set", - "markOut": int(mark_out_frame), + "markOut": int(mark_out_frame) + 1, "markOutState": mark_out_state == "set" } expected_data = self.get_expected_data(context) From 967faee039c1e508fc026358e79d59946d1aa7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 14 Apr 2021 10:43:11 +0200 Subject: [PATCH 177/264] make tx configurable with presets --- pype/plugins/maya/create/create_look.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/create/create_look.py b/pype/plugins/maya/create/create_look.py index 04f5603d4d1..77673416985 100644 --- a/pype/plugins/maya/create/create_look.py +++ b/pype/plugins/maya/create/create_look.py @@ -12,6 +12,7 @@ class CreateLook(plugin.Creator): family = "look" icon = "paint-brush" defaults = ['Main'] + make_tx = True def __init__(self, *args, **kwargs): super(CreateLook, self).__init__(*args, **kwargs) @@ -19,7 +20,7 @@ def __init__(self, *args, **kwargs): self.data["renderlayer"] = lib.get_current_renderlayer() # Whether to automatically convert the textures to .tx upon publish. - self.data["maketx"] = True + self.data["maketx"] = self.make_tx # Enable users to force a copy. self.data["forceCopy"] = False From 32b1fa4f13b52b2b4b5e1be9dd5eea590f9a15e9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Apr 2021 09:43:14 +0100 Subject: [PATCH 178/264] Remove frame range validation from project settings validator. --- pype/plugins/tvpaint/publish/validate_project_settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/validate_project_settings.py b/pype/plugins/tvpaint/publish/validate_project_settings.py index fead3393ae9..84c03a98573 100644 --- a/pype/plugins/tvpaint/publish/validate_project_settings.py +++ b/pype/plugins/tvpaint/publish/validate_project_settings.py @@ -13,8 +13,6 @@ class ValidateProjectSettings(pyblish.api.ContextPlugin): def process(self, context): scene_data = { - "frameStart": context.data.get("sceneFrameStart"), - "frameEnd": context.data.get("sceneFrameEnd"), "fps": context.data.get("sceneFps"), "resolutionWidth": context.data.get("sceneWidth"), "resolutionHeight": context.data.get("sceneHeight"), From 0b1f8b2c557c2ead8abf65c868c2d9e20a950efd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Apr 2021 10:33:02 +0100 Subject: [PATCH 179/264] Collect and use mark in/out frame and state. --- .../tvpaint/publish/collect_instances.py | 8 ++--- .../tvpaint/publish/collect_workfile_data.py | 34 +++++++------------ .../plugins/tvpaint/publish/validate_marks.py | 15 +++----- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index 5456f24dfa2..cc236734e5f 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -34,8 +34,8 @@ def process(self, context): instance_data["name"] = name instance_data["label"] = "{} [{}-{}]".format( name, - context.data["sceneFrameStart"], - context.data["sceneFrameEnd"] + context.data["sceneMarkIn"] + 1, + context.data["sceneMarkOut"] + 1 ) active = instance_data.get("active", True) @@ -86,8 +86,8 @@ def process(self, context): instance.data["publish"] = any_visible - instance.data["frameStart"] = context.data["sceneFrameStart"] - instance.data["frameEnd"] = context.data["sceneFrameEnd"] + instance.data["frameStart"] = context.data["sceneMarkIn"] + 1 + instance.data["frameEnd"] = context.data["sceneMarkOut"] + 1 self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) diff --git a/pype/plugins/tvpaint/publish/collect_workfile_data.py b/pype/plugins/tvpaint/publish/collect_workfile_data.py index e683c66ea9d..4409413ff65 100644 --- a/pype/plugins/tvpaint/publish/collect_workfile_data.py +++ b/pype/plugins/tvpaint/publish/collect_workfile_data.py @@ -122,36 +122,26 @@ def process(self, context): width = int(workfile_info_parts.pop(-1)) workfile_path = " ".join(workfile_info_parts).replace("\"", "") - frame_start, frame_end = self.collect_clip_frames() + # Marks return as "{frame - 1} {state} ", example "0 set". + result = lib.execute_george("tv_markin") + mark_in_frame, mark_in_state, _ = result.split(" ") + + result = lib.execute_george("tv_markout") + mark_out_frame, mark_out_state, _ = result.split(" ") + scene_data = { "currentFile": workfile_path, "sceneWidth": width, "sceneHeight": height, "scenePixelAspect": pixel_apsect, - "sceneFrameStart": frame_start, - "sceneFrameEnd": frame_end, "sceneFps": frame_rate, - "sceneFieldOrder": field_order + "sceneFieldOrder": field_order, + "sceneMarkIn": int(mark_in_frame), + "sceneMarkInState": mark_in_state == "set", + "sceneMarkOut": int(mark_out_frame), + "sceneMarkOutState": mark_out_state == "set" } self.log.debug( "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) - - def collect_clip_frames(self): - clip_info_str = lib.execute_george("tv_clipinfo") - self.log.debug("Clip info: {}".format(clip_info_str)) - clip_info_items = clip_info_str.split(" ") - # Color index - not used - clip_info_items.pop(-1) - clip_info_items.pop(-1) - - mark_out = int(clip_info_items.pop(-1)) - frame_end = mark_out + 1 - clip_info_items.pop(-1) - - mark_in = int(clip_info_items.pop(-1)) - frame_start = mark_in + 1 - clip_info_items.pop(-1) - - return frame_start, frame_end diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index e491d878354..73486d10059 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -41,18 +41,11 @@ def get_expected_data(context): } def process(self, context): - # Marks return as "{frame - 1} {state} ", example "0 set". - result = lib.execute_george("tv_markin") - mark_in_frame, mark_in_state, _ = result.split(" ") - - result = lib.execute_george("tv_markout") - mark_out_frame, mark_out_state, _ = result.split(" ") - current_data = { - "markIn": int(mark_in_frame) + 1, - "markInState": mark_in_state == "set", - "markOut": int(mark_out_frame) + 1, - "markOutState": mark_out_state == "set" + "markIn": context.data["sceneMarkIn"] + 1, + "markInState": context.data["sceneMarkInState"], + "markOut": context.data["sceneMarkOut"] + 1, + "markOutState": context.data["sceneMarkOutState"] } expected_data = self.get_expected_data(context) invalid = {} From 7cb9bb300726092ce28e7ff5c0941cc04b09537b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 Apr 2021 13:37:10 +0200 Subject: [PATCH 180/264] Nuke: families issue final fix --- pype/plugins/nuke/publish/collect_instances.py | 13 ++----------- pype/plugins/nuke/publish/collect_writes.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 6c5cfaedfde..9617f8023d6 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -57,8 +57,6 @@ def process(self, context): if families_ak: families.append(families_ak) - families.append(family) - # except disabled nodes but exclude backdrops in test if ("nukenodes" not in family) and (node["disable"].value()): continue @@ -78,24 +76,16 @@ def process(self, context): if target == "Use existing frames": # Local rendering self.log.info("flagged for no render") - families.append(family) elif target == "Local": # Local rendering self.log.info("flagged for local render") families.append("{}.local".format(family)) + family = "write" elif target == "On farm": # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False families.append("{}.farm".format(family)) - - # suffle family to `write` as it is main family - # this will be changed later on in process - if "render" in families: - families.remove("render") - family = "write" - elif "prerender" in families: - families.remove("prerender") family = "write" node.begin() @@ -103,6 +93,7 @@ def process(self, context): instance.append(i) node.end() + self.log.debug("__ family: `{}`".format(family)) self.log.debug("__ families: `{}`".format(families)) # Get format diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index a8651066e2d..dd0111c1e46 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -16,7 +16,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): sync_workfile_version = True def process(self, instance): - families = instance.data["families"] + families = _families_test = instance.data["families"] + _families_test = [instance.data["family"]] + _families_test node = None for x in instance: @@ -54,7 +55,10 @@ def process(self, instance): output_dir = os.path.dirname(path) self.log.debug('output dir: {}'.format(output_dir)) - if not next((f for f in families + self.log.info(">> _families_test: `{}`".format(_families_test)) + # synchronize version if it is set in presets + # and not prerender in _families_test + if not next((f for f in _families_test if "prerender" in f), None) and self.sync_workfile_version: # get version to instance for integration @@ -71,7 +75,7 @@ def process(self, instance): int(last_frame) ) - if [fm for fm in families + if [fm for fm in _families_test if fm in ["render", "prerender"]]: if "representations" not in instance.data: instance.data["representations"] = list() @@ -98,9 +102,9 @@ def process(self, instance): collected_frames_len)) # this will only run if slate frame is not already # rendered from previews publishes - if "slate" in instance.data["families"] \ + if "slate" in _families_test \ and (frame_length == collected_frames_len) \ - and ("prerender" not in instance.data["families"]): + and ("prerender" not in _families_test): frame_slate_str = "%0{}d".format( len(str(last_frame))) % (first_frame - 1) slate_frame = collected_frames[0].replace( @@ -123,7 +127,7 @@ def process(self, instance): # Add version data to instance version_data = { "families": [f.replace(".local", "").replace(".farm", "") - for f in families if "write" not in f], + for f in _families_test if "write" not in f], "colorspace": colorspace, } @@ -149,7 +153,6 @@ def process(self, instance): "frameStartHandle": first_frame, "frameEndHandle": last_frame, "outputType": output_type, - "families": families, "colorspace": colorspace, "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority From c097d6d71619388227411924c34fa5a0b504266d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 Apr 2021 13:37:42 +0200 Subject: [PATCH 181/264] Nuke: validator was not compatible with new ui --- .../nuke/publish/validate_rendered_frames.py | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 425789f18ac..3e7f1591160 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -5,8 +5,8 @@ @pyblish.api.log -class RepairCollectionAction(pyblish.api.Action): - label = "Repair" +class RepairCollectionActionToLocal(pyblish.api.Action): + label = "Repair > rerender with `Local` machine" on = "failed" icon = "wrench" @@ -20,8 +20,27 @@ def process(self, context, plugin): for f in files_remove: os.remove(f) self.log.debug("removing file: {}".format(f)) - context[0][0]["render"].setValue(True) - self.log.info("Rendering toggled ON") + context[0][0]["render"].setValue("Local") + self.log.info("Rendering toggled to `Local`") + +@pyblish.api.log +class RepairCollectionActionToFarm(pyblish.api.Action): + label = "Repair > rerender `On farm` with remote machines" + on = "failed" + icon = "wrench" + + def process(self, context, plugin): + self.log.info(context[0][0]) + files_remove = [os.path.join(context[0].data["outputDir"], f) + for r in context[0].data.get("representations", []) + for f in r.get("files", []) + ] + self.log.info("Files to be removed: {}".format(files_remove)) + for f in files_remove: + os.remove(f) + self.log.debug("removing file: {}".format(f)) + context[0][0]["render"].setValue("On farm") + self.log.info("Rendering toggled to `On farm`") class ValidateRenderedFrames(pyblish.api.InstancePlugin): @@ -32,26 +51,28 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): label = "Validate rendered frame" hosts = ["nuke", "nukestudio"] - actions = [RepairCollectionAction] + actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm] + def process(self, instance): - for repre in instance.data.get('representations'): + for repre in instance.data["representations"]: - if not repre.get('files'): + if not repre.get("files"): msg = ("no frames were collected, " "you need to render them") self.log.error(msg) raise ValidationException(msg) collections, remainder = clique.assemble(repre["files"]) - self.log.info('collections: {}'.format(str(collections))) - self.log.info('remainder: {}'.format(str(remainder))) + self.log.info("collections: {}".format(str(collections))) + self.log.info("remainder: {}".format(str(remainder))) collection = collections[0] frame_length = int( - instance.data["frameEndHandle"] - instance.data["frameStartHandle"] + 1 + instance.data["frameEndHandle"] + - instance.data["frameStartHandle"] + 1 ) if frame_length != 1: @@ -65,15 +86,10 @@ def process(self, instance): self.log.error(msg) raise ValidationException(msg) - # if len(remainder) != 0: - # msg = "There are some extra files in folder" - # self.log.error(msg) - # raise ValidationException(msg) - collected_frames_len = int(len(collection.indexes)) - self.log.info('frame_length: {}'.format(frame_length)) + self.log.info("frame_length: {}".format(frame_length)) self.log.info( - 'len(collection.indexes): {}'.format(collected_frames_len) + "len(collection.indexes): {}".format(collected_frames_len) ) if ("slate" in instance.data["families"]) \ @@ -84,6 +100,6 @@ def process(self, instance): "{} missing frames. Use repair to render all frames" ).format(__name__) - instance.data['collection'] = collection + instance.data["collection"] = collection return From a9398231c185c053d3efec45df6cf95ae226349f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 Apr 2021 14:06:25 +0200 Subject: [PATCH 182/264] nuke: hound fix --- pype/plugins/nuke/publish/validate_rendered_frames.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 3e7f1591160..1e6c25d16fb 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -23,6 +23,7 @@ def process(self, context, plugin): context[0][0]["render"].setValue("Local") self.log.info("Rendering toggled to `Local`") + @pyblish.api.log class RepairCollectionActionToFarm(pyblish.api.Action): label = "Repair > rerender `On farm` with remote machines" From 5c61c1b385827cf24348402246429aa1abc39485 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 16 Apr 2021 17:41:28 +0200 Subject: [PATCH 183/264] backport of #1358 for pype 2 --- .../maya/create/create_redshift_proxy.py | 23 +++ pype/plugins/maya/load/load_redshift_proxy.py | 147 ++++++++++++++++++ .../maya/publish/extract_redshift_proxy.py | 80 ++++++++++ 3 files changed, 250 insertions(+) create mode 100644 pype/plugins/maya/create/create_redshift_proxy.py create mode 100644 pype/plugins/maya/load/load_redshift_proxy.py create mode 100644 pype/plugins/maya/publish/extract_redshift_proxy.py diff --git a/pype/plugins/maya/create/create_redshift_proxy.py b/pype/plugins/maya/create/create_redshift_proxy.py new file mode 100644 index 00000000000..8713022aac8 --- /dev/null +++ b/pype/plugins/maya/create/create_redshift_proxy.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Creator of Redshift proxy subset types.""" + +from pype.hosts.maya import plugin, lib + + +class CreateRedshiftProxy(plugin.Creator): + """Create instance of Redshift Proxy subset.""" + + name = "redshiftproxy" + label = "Redshift Proxy" + family = "redshiftproxy" + icon = "gears" + + def __init__(self, *args, **kwargs): + super(CreateRedshiftProxy, self).__init__(*args, **kwargs) + + animation_data = lib.collect_animation_data() + + self.data["animation"] = False + self.data["proxyFrameStart"] = animation_data["frameStart"] + self.data["proxyFrameEnd"] = animation_data["frameEnd"] + self.data["proxyFrameStep"] = animation_data["step"] diff --git a/pype/plugins/maya/load/load_redshift_proxy.py b/pype/plugins/maya/load/load_redshift_proxy.py new file mode 100644 index 00000000000..b4f54cd8a2e --- /dev/null +++ b/pype/plugins/maya/load/load_redshift_proxy.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +"""Loader for Redshift proxy.""" +import os +from avalon.maya import lib +from avalon import api +from pype.api import config + +import maya.cmds as cmds +import clique + + +class RedshiftProxyLoader(api.Loader): + """Load Redshift proxy""" + + families = ["redshiftproxy"] + representations = ["rs"] + + label = "Import Redshift Proxy" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, options=None): + """Plugin entry point.""" + + from avalon.maya.pipeline import containerise + from pype.hosts.maya.lib import namespaced + + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "redshiftproxy" + + asset_name = context['asset']["name"] + namespace = namespace or lib.unique_namespace( + asset_name + "_", + prefix="_" if asset_name[0].isdigit() else "", + suffix="_", + ) + + # Ensure Redshift for Maya is loaded. + cmds.loadPlugin("redshift4maya", quiet=True) + + with lib.maintained_selection(): + cmds.namespace(addNamespace=namespace) + with namespaced(namespace, new=False): + nodes, group_node = self.create_rs_proxy( + name, self.fname) + + self[:] = nodes + if not nodes: + return + + # colour the group node + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + colors = presets['plugins']['maya']['load']['colors'] + c = colors.get(family) + if c is not None: + cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) + cmds.setAttr("{0}.outlinerColor".format(group_node), + c[0], c[1], c[2]) + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + + node = container['objectName'] + assert cmds.objExists(node), "Missing container" + + members = cmds.sets(node, query=True) or [] + rs_meshes = cmds.ls(members, type="RedshiftProxyMesh") + assert rs_meshes, "Cannot find RedshiftProxyMesh in container" + + filename = api.get_representation_path(representation) + + for rs_mesh in rs_meshes: + cmds.setAttr("{}.fileName".format(rs_mesh), + filename, + type="string") + + # Update metadata + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + + def remove(self, container): + + # Delete container and its contents + if cmds.objExists(container['objectName']): + members = cmds.sets(container['objectName'], query=True) or [] + cmds.delete([container['objectName']] + members) + + # Remove the namespace, if empty + namespace = container['namespace'] + if cmds.namespace(exists=namespace): + members = cmds.namespaceInfo(namespace, listNamespace=True) + if not members: + cmds.namespace(removeNamespace=namespace) + else: + self.log.warning("Namespace not deleted because it " + "still has members: %s", namespace) + + def switch(self, container, representation): + self.update(container, representation) + + def create_rs_proxy(self, name, path): + """Creates Redshift Proxies showing a proxy object. + + Args: + name (str): Proxy name. + path (str): Path to proxy file. + + Returns: + (str, str): Name of mesh with Redshift proxy and its parent + transform. + + """ + rs_mesh = cmds.createNode( + 'RedshiftProxyMesh', name="{}_RS".format(name)) + mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) + + cmds.setAttr("{}.fileName".format(rs_mesh), + path, + type="string") + + cmds.connectAttr("{}.outMesh".format(rs_mesh), + "{}.inMesh".format(mesh_shape)) + + group_node = cmds.group(empty=True, name="{}_GRP".format(name)) + mesh_transform = cmds.listRelatives(mesh_shape, + parent=True, fullPath=True) + cmds.parent(mesh_transform, group_node) + nodes = [rs_mesh, mesh_shape, group_node] + + # determine if we need to enable animation support + files_in_folder = os.listdir(os.path.dirname(path)) + collections, remainder = clique.assemble(files_in_folder) + + if collections: + cmds.setAttr("{}.useFrameExtension".format(rs_mesh), 1) + + return nodes, group_node diff --git a/pype/plugins/maya/publish/extract_redshift_proxy.py b/pype/plugins/maya/publish/extract_redshift_proxy.py new file mode 100644 index 00000000000..56255eaad43 --- /dev/null +++ b/pype/plugins/maya/publish/extract_redshift_proxy.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""Redshift Proxy extractor.""" +import os +import math + +import avalon.maya +import pype.api + +from maya import cmds + + +class ExtractRedshiftProxy(pype.api.Extractor): + """Extract the content of the instance to a redshift proxy file.""" + + label = "Redshift Proxy (.rs)" + hosts = ["maya"] + families = ["redshiftproxy"] + + def process(self, instance): + """Extractor entry point.""" + + staging_dir = self.staging_dir(instance) + file_name = "{}.rs".format(instance.name) + file_path = os.path.join(staging_dir, file_name) + + anim_on = instance.data["animation"] + rs_options = "exportConnectivity=0;enableCompression=1;keepUnused=0;" + repr_files = file_name + + if not anim_on: + # Remove animation information because it is not required for + # non-animated subsets + instance.data.pop("proxyFrameStart", None) + instance.data.pop("proxyFrameEnd", None) + + else: + start_frame = instance.data["proxyFrameStart"] + end_frame = instance.data["proxyFrameEnd"] + rs_options = "{}startFrame={};endFrame={};frameStep={};".format( + rs_options, start_frame, + end_frame, instance.data["proxyFrameStep"] + ) + + root, ext = os.path.splitext(file_path) + # Padding is taken from number of digits of the end_frame. + # Not sure where Redshift is taking it. + repr_files = [ + "{}.{}{}".format(root, str(frame).rjust(int(math.log10(int(end_frame)) + 1), "0"), ext) # noqa: E501 + for frame in range( + int(start_frame), + int(end_frame) + 1, + int(instance.data["proxyFrameStep"]))] + # vertex_colors = instance.data.get("vertexColors", False) + + # Write out rs file + self.log.info("Writing: '%s'" % file_path) + with avalon.maya.maintained_selection(): + cmds.select(instance.data["setMembers"], noExpand=True) + cmds.file(file_path, + pr=False, + force=True, + type="Redshift Proxy", + exportSelected=True, + options=rs_options) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + self.log.debug("Files: {}".format(repr_files)) + + representation = { + 'name': 'rs', + 'ext': 'rs', + 'files': repr_files, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s" + % (instance.name, staging_dir)) From 9b67498eb46c0567a5cf0efe1bb7c1245fb299ea Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 16 Apr 2021 19:00:27 +0200 Subject: [PATCH 184/264] Hiero: fixing source frame from correct object also review wrong sequence definition --- .../hiero/publish/collect_calculate_retime.py | 21 ++++++++++++++----- pype/plugins/hiero/publish/collect_clips.py | 6 +++--- .../hiero/publish/collect_frame_ranges.py | 9 +++++--- pype/plugins/hiero/publish/collect_plates.py | 7 ++----- pype/plugins/hiero/publish/collect_review.py | 8 +++---- .../hiero/publish/collect_tag_framestart.py | 6 +----- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/pype/plugins/hiero/publish/collect_calculate_retime.py b/pype/plugins/hiero/publish/collect_calculate_retime.py index 1b2f047da24..820a13c0d32 100644 --- a/pype/plugins/hiero/publish/collect_calculate_retime.py +++ b/pype/plugins/hiero/publish/collect_calculate_retime.py @@ -20,12 +20,16 @@ def process(self, instance): handle_end = instance.data["handleEnd"] track_item = instance.data["item"] + clip = track_item.source() + mediaSource = clip.mediaSource() + file_info = mediaSource.fileinfos().pop() + start_frame = file_info.startFrame() # define basic clip frame range variables timeline_in = int(track_item.timelineIn()) timeline_out = int(track_item.timelineOut()) - source_in = int(track_item.sourceIn()) - source_out = int(track_item.sourceOut()) + source_in = int(clip.sourceIn()) + source_out = int(clip.sourceOut()) speed = track_item.playbackSpeed() self.log.debug("_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\ \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n handle_start: `{5}`,\n handle_end: `{6}`".format( @@ -102,8 +106,16 @@ def process(self, instance): source_in += int(source_in_change) source_out += int(source_out_change * speed) - handle_start += (margin_in) - handle_end += (margin_out) + + # make sure there are frames for margin in + if (track_item.handleInLength() >= margin_in) and ( + source_in - (handle_start + margin_in >= start_frame)): + handle_start += (margin_in) + # make sure there are frames for margin out + if (track_item.handleOutLength() >= margin_out) and ( + (source_out + handle_end + margin_out) <= (start_frame + track_item.sourceDuration() - 1)): + handle_end += (margin_out) + self.log.debug("margin: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end)) # add all data to Instance @@ -115,7 +127,6 @@ def process(self, instance): (handle_end * 1000) / 1000.0)) instance.data["speed"] = speed - self.log.debug("timeWarpNodes: {}".format(instance.data["timeWarpNodes"])) self.log.debug("sourceIn: {}".format(instance.data["sourceIn"])) self.log.debug("sourceOut: {}".format(instance.data["sourceOut"])) self.log.debug("speed: {}".format(instance.data["speed"])) diff --git a/pype/plugins/hiero/publish/collect_clips.py b/pype/plugins/hiero/publish/collect_clips.py index 44de68224d5..c8e5bf71e2e 100644 --- a/pype/plugins/hiero/publish/collect_clips.py +++ b/pype/plugins/hiero/publish/collect_clips.py @@ -45,6 +45,7 @@ def process(self, context): asset = item.name() track = item.parent() + clip = item.source() source = item.source().mediaSource() source_path = source.firstpath() clip_in = int(item.timelineIn()) @@ -130,10 +131,9 @@ def process(self, context): "isSequence": is_sequence, "track": track.name(), "trackIndex": track_index, - "sourceFirst": source_first_frame, "effects": effects, - "sourceIn": int(item.sourceIn()), - "sourceOut": int(item.sourceOut()), + "sourceIn": int(clip.sourceIn()), + "sourceOut": int(clip.sourceOut()), "mediaDuration": int(source.duration()), "clipIn": clip_in, "clipOut": clip_out, diff --git a/pype/plugins/hiero/publish/collect_frame_ranges.py b/pype/plugins/hiero/publish/collect_frame_ranges.py index 19a46d80b15..9dba48bbd7c 100644 --- a/pype/plugins/hiero/publish/collect_frame_ranges.py +++ b/pype/plugins/hiero/publish/collect_frame_ranges.py @@ -11,15 +11,18 @@ class CollectClipFrameRanges(pyblish.api.InstancePlugin): def process(self, instance): data = dict() - + track_item = instance.data["item"] + item_handle_in = int(track_item.handleInLength()) + item_handle_out = int(track_item.handleOutLength()) + # Timeline data. handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] source_in_h = instance.data("sourceInH", - instance.data("sourceIn") - handle_start) + (instance.data("sourceIn") + item_handle_in) - handle_start) source_out_h = instance.data("sourceOutH", - instance.data("sourceOut") + handle_end) + (instance.data("sourceOut") - item_handle_out) + handle_end) timeline_in = instance.data["clipIn"] timeline_out = instance.data["clipOut"] diff --git a/pype/plugins/hiero/publish/collect_plates.py b/pype/plugins/hiero/publish/collect_plates.py index 7b671ef718b..b3fe5fd7e72 100644 --- a/pype/plugins/hiero/publish/collect_plates.py +++ b/pype/plugins/hiero/publish/collect_plates.py @@ -144,9 +144,7 @@ def process(self, instance): "version": version }) - source_first_frame = instance.data.get("sourceFirst") source_file_head = instance.data.get("sourceFileHead") - self.log.debug("source_first_frame: `{}`".format(source_first_frame)) if instance.data.get("isSequence", False): self.log.info("Is sequence of files") @@ -154,7 +152,7 @@ def process(self, instance): ext = os.path.splitext(file)[-1][1:] self.log.debug("source_file_head: `{}`".format(source_file_head)) head = source_file_head[:-1] - start_frame = int(source_first_frame + instance.data["sourceInH"]) + start_frame = int(instance.data["sourceInH"]) duration = int( instance.data["sourceOutH"] - instance.data["sourceInH"]) end_frame = start_frame + duration @@ -192,8 +190,7 @@ def process(self, instance): instance.data["representations"].append( plates_mov_representation) - thumb_frame = instance.data["sourceInH"] + ( - (instance.data["sourceOutH"] - instance.data["sourceInH"]) / 2) + thumb_frame = item.sourceIn() + (item.sourceDuration() / 2) thumb_file = "{}_{}{}".format(head, thumb_frame, ".png") thumb_path = os.path.join(staging_dir, thumb_file) self.log.debug("__ thumb_path: `{}`, frame: `{}`".format( diff --git a/pype/plugins/hiero/publish/collect_review.py b/pype/plugins/hiero/publish/collect_review.py index f1767b2a681..7c7de8750b6 100644 --- a/pype/plugins/hiero/publish/collect_review.py +++ b/pype/plugins/hiero/publish/collect_review.py @@ -21,7 +21,6 @@ class CollectReview(api.InstancePlugin): families = ["plate"] def process(self, instance): - is_sequence = instance.data["isSequence"] # Exclude non-tagged instances. tagged = False @@ -90,20 +89,19 @@ def process(self, instance): ext = os.path.splitext(file)[-1] # detect if sequence - if not is_sequence: + if not self.detect_sequence(file)[-1]: # is video file files = file else: files = list() - source_first = instance.data["sourceFirst"] self.log.debug("_ file: {}".format(file)) spliter, padding = self.detect_sequence(file) self.log.debug("_ spliter, padding: {}, {}".format( spliter, padding)) base_name = file.split(spliter)[0] collection = clique.Collection(base_name, ext, padding, set(range( - int(source_first + rev_inst.data.get("sourceInH")), - int(source_first + rev_inst.data.get("sourceOutH") + 1)))) + int(rev_inst.data.get("sourceInH")), + int(rev_inst.data.get("sourceOutH") + 1)))) self.log.debug("_ collection: {}".format(collection)) real_files = os.listdir(file_dir) for item in collection: diff --git a/pype/plugins/hiero/publish/collect_tag_framestart.py b/pype/plugins/hiero/publish/collect_tag_framestart.py index 0d14271aa5a..e1ea98de011 100644 --- a/pype/plugins/hiero/publish/collect_tag_framestart.py +++ b/pype/plugins/hiero/publish/collect_tag_framestart.py @@ -29,13 +29,9 @@ def process(self, instance): start_frame = int(start_frame) except ValueError: if "source" in t_value: - source_first = instance.data["sourceFirst"] - if source_first == 0: - source_first = 1 - self.log.info("Start frame on `{0}`".format(source_first)) source_in = instance.data["sourceIn"] self.log.info("Start frame on `{0}`".format(source_in)) - start_frame = source_first + source_in + start_frame = source_in instance.data["startingFrame"] = start_frame self.log.info("Start frame on `{0}` set to `{1}`".format( From bb1e4980d24a4d289a70f1d28d59e08165a6e5a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 16 Apr 2021 19:42:55 +0200 Subject: [PATCH 185/264] AE added validation for frames duration --- pype/hosts/aftereffects/__init__.py | 85 +++++++++++++ .../aftereffects/publish/collect_render.py | 17 ++- .../publish/validate_scene_settings.py | 115 ++++++++++++++++++ 3 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 pype/plugins/aftereffects/publish/validate_scene_settings.py diff --git a/pype/hosts/aftereffects/__init__.py b/pype/hosts/aftereffects/__init__.py index 184a9a59d6b..74203fe4b29 100644 --- a/pype/hosts/aftereffects/__init__.py +++ b/pype/hosts/aftereffects/__init__.py @@ -5,6 +5,7 @@ from avalon.vendor import Qt from pype import lib import pyblish.api +from pype.api import config def check_inventory(): @@ -72,3 +73,87 @@ def install(): def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle layer visibility on instance toggles.""" instance[0].Visible = new_value + + +def get_asset_settings(): + """Get settings on current asset from database. + + Returns: + dict: Scene data. + + """ + asset_data = lib.get_asset()["data"] + fps = asset_data.get("fps") + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + handle_start = asset_data.get("handleStart") + handle_end = asset_data.get("handleEnd") + resolution_width = asset_data.get("resolutionWidth") + resolution_height = asset_data.get("resolutionHeight") + duration = frame_end + handle_end - min(frame_start - handle_start, 0) + entity_type = asset_data.get("entityType") + + scene_data = { + "fps": fps, + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, + "resolutionWidth": resolution_width, + "resolutionHeight": resolution_height, + "duration": duration + } + + + try: + # temporary, in pype3 replace with api.get_current_project_settings + skip_resolution_check = ( + get_current_project_settings() + ["plugins"] + ["aftereffects"] + ["publish"] + ["ValidateSceneSettings"] + ["skip_resolution_check"] + ) + skip_timelines_check = ( + get_current_project_settings() + ["plugins"] + ["aftereffects"] + ["publish"] + ["ValidateSceneSettings"] + ["skip_timelines_check"] + ) + except KeyError: + skip_resolution_check = ['*'] + skip_timelines_check = ['*'] + + if os.getenv('AVALON_TASK') in skip_resolution_check or \ + '*' in skip_timelines_check: + scene_data.pop("resolutionWidth") + scene_data.pop("resolutionHeight") + + if entity_type in skip_timelines_check or '*' in skip_timelines_check: + scene_data.pop('fps', None) + scene_data.pop('frameStart', None) + scene_data.pop('frameEnd', None) + scene_data.pop('handleStart', None) + scene_data.pop('handleEnd', None) + + return scene_data + +# temporary, in pype3 replace with api.get_current_project_settings +def get_current_project_settings(): + """Project settings for current context project. + + Project name should be stored in environment variable `AVALON_PROJECT`. + This function should be used only in host context where environment + variable must be set and should not happen that any part of process will + change the value of the enviornment variable. + """ + project_name = os.environ.get("AVALON_PROJECT") + if not project_name: + raise ValueError( + "Missing context project in environemt variable `AVALON_PROJECT`." + ) + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) + return presets diff --git a/pype/plugins/aftereffects/publish/collect_render.py b/pype/plugins/aftereffects/publish/collect_render.py index 8c80ab928ea..24b1c7a89a8 100644 --- a/pype/plugins/aftereffects/publish/collect_render.py +++ b/pype/plugins/aftereffects/publish/collect_render.py @@ -12,6 +12,7 @@ class AERenderInstance(RenderInstance): # extend generic, composition name is needed comp_name = attr.ib(default=None) comp_id = attr.ib(default=None) + fps = attr.ib(default=None) class CollectAERender(abstract_collect_render.AbstractCollectRender): @@ -23,6 +24,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): padding_width = 6 rendered_extension = 'png' + stub = aftereffects.stub() + def get_instances(self, context): instances = [] @@ -31,9 +34,9 @@ def get_instances(self, context): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - compositions = aftereffects.stub().get_items(True) + compositions = self.stub.get_items(True) compositions_by_id = {item.id: item for item in compositions} - for inst in aftereffects.stub().get_metadata(): + for inst in self.stub.get_metadata(): schema = inst.get('schema') # loaded asset container skip it if schema and 'container' in schema: @@ -43,7 +46,8 @@ def get_instances(self, context): raise ValueError("Couldn't find id, unable to publish. " + "Please recreate instance.") item_id = inst["members"][0] - work_area_info = aftereffects.stub().get_work_area(int(item_id)) + + work_area_info = self.stub.get_work_area(int(item_id)) if not work_area_info: self.log.warning("Orphaned instance, deleting metadata") @@ -55,6 +59,8 @@ def get_instances(self, context): frameEnd = round(work_area_info.workAreaStart + float(work_area_info.workAreaDuration) * float(work_area_info.frameRate)) - 1 + fps = work_area_info.frameRate + # TODO add resolution when supported by extension if inst["family"] == "render" and inst["active"]: instance = AERenderInstance( @@ -84,7 +90,8 @@ def get_instances(self, context): frameStart=frameStart, frameEnd=frameEnd, frameStep=1, - toBeRenderedOn='deadline' + toBeRenderedOn='deadline', + fps=fps ) comp = compositions_by_id.get(int(item_id)) @@ -119,7 +126,7 @@ def get_expected_files(self, render_instance): end = render_instance.frameEnd # pull file name from Render Queue Output module - render_q = aftereffects.stub().get_render_info() + render_q = self.stub.get_render_info() if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) diff --git a/pype/plugins/aftereffects/publish/validate_scene_settings.py b/pype/plugins/aftereffects/publish/validate_scene_settings.py new file mode 100644 index 00000000000..acaef89be22 --- /dev/null +++ b/pype/plugins/aftereffects/publish/validate_scene_settings.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""Validate scene settings.""" +import os +import json + +import pyblish.api + +from avalon import harmony +from avalon import aftereffects + +import pype.hosts.aftereffects + +stub = aftereffects.stub() + +from pype.api import config + + +class ValidateSceneSettings(pyblish.api.InstancePlugin): + """ + Ensures that Composition Settings (right mouse on comp) are same as + in FTrack on task. + + By default checks only duration - how many frames should be rendered. + Compares: + Frame start - Frame end + 1 from FTrack + against + Duration in Composition Settings. + + If this complains: + Check error message where is discrepancy. + Check FTrack task 'pype' section of task attributes for expected + values. + Check/modify rendered Composition Settings. + + If you know what you are doing run publishing again, uncheck this + validation before Validation phase. + """ + + """ + Dev docu: + Could be configured by 'presets/plugins/aftereffects/publish' + + skip_timelines_check - fill task name for which skip validation of + frameStart + frameEnd + fps + handleStart + handleEnd + skip_resolution_check - fill entity type ('asset') to skip validation + resolutionWidth + resolutionHeight + TODO support in extension is missing for now + + By defaults validates duration (how many frames should be published) + + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Scene Settings" + families = ["render.farm"] + hosts = ["aftereffects"] + optional = True + + skip_timelines_check = ["*"] # * >> skip for all + skip_resolution_check = ["*"] + + def process(self, instance): + """Plugin entry point.""" + expected_settings = pype.hosts.aftereffects.get_asset_settings() + self.log.info("expected_settings::{}".format(expected_settings)) + + # handle case where ftrack uses only two decimal places + # 23.976023976023978 vs. 23.98 + fps = instance.data.get("fps") + if fps: + if isinstance(fps, float): + fps = float( + "{:.2f}".format(fps)) + expected_settings["fps"] = fps + + duration = instance.data.get("frameEndHandle") - \ + instance.data.get("frameStartHandle") + 1 + + current_settings = { + "fps": fps, + "frameStartHandle": instance.data.get("frameStartHandle"), + "frameEndHandle": instance.data.get("frameEndHandle"), + "resolutionWidth": instance.data.get("resolutionWidth"), + "resolutionHeight": instance.data.get("resolutionHeight"), + "duration": duration + } + self.log.info("current_settings:: {}".format(current_settings)) + + invalid_settings = [] + for key, value in expected_settings.items(): + if value != current_settings[key]: + invalid_settings.append( + "{} expected: {} found: {}".format(key, value, + current_settings[key]) + ) + + if ((expected_settings.get("handleStart") + or expected_settings.get("handleEnd")) + and invalid_settings): + msg = "Handles included in calculation. Remove handles in DB " +\ + "or extend frame range in Composition Setting." + invalid_settings[-1]["reason"] = msg + + msg = "Found invalid settings:\n{}".format( + "\n".join(invalid_settings) + ) + assert not invalid_settings, msg + assert os.path.exists(instance.data.get("source")), ( + "Scene file not found (saved under wrong name)" + ) From 68fcbf24cd64bdc49a29472f9e5e2b1d72a42e32 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 16 Apr 2021 19:49:39 +0200 Subject: [PATCH 186/264] Hound --- pype/hosts/aftereffects/__init__.py | 2 +- .../aftereffects/publish/validate_scene_settings.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pype/hosts/aftereffects/__init__.py b/pype/hosts/aftereffects/__init__.py index 74203fe4b29..0f813190cbb 100644 --- a/pype/hosts/aftereffects/__init__.py +++ b/pype/hosts/aftereffects/__init__.py @@ -104,7 +104,6 @@ def get_asset_settings(): "duration": duration } - try: # temporary, in pype3 replace with api.get_current_project_settings skip_resolution_check = ( @@ -141,6 +140,7 @@ def get_asset_settings(): return scene_data + # temporary, in pype3 replace with api.get_current_project_settings def get_current_project_settings(): """Project settings for current context project. diff --git a/pype/plugins/aftereffects/publish/validate_scene_settings.py b/pype/plugins/aftereffects/publish/validate_scene_settings.py index acaef89be22..944691aabe2 100644 --- a/pype/plugins/aftereffects/publish/validate_scene_settings.py +++ b/pype/plugins/aftereffects/publish/validate_scene_settings.py @@ -1,19 +1,15 @@ # -*- coding: utf-8 -*- """Validate scene settings.""" import os -import json import pyblish.api -from avalon import harmony from avalon import aftereffects import pype.hosts.aftereffects stub = aftereffects.stub() -from pype.api import config - class ValidateSceneSettings(pyblish.api.InstancePlugin): """ @@ -50,9 +46,8 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): resolutionWidth resolutionHeight TODO support in extension is missing for now - + By defaults validates duration (how many frames should be published) - """ order = pyblish.api.ValidatorOrder From 77b4b1c192bfec7582042b391be1f2e72b76d3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 19 Apr 2021 13:02:10 +0200 Subject: [PATCH 187/264] update integrate for redshiftproxy family type --- pype/plugins/global/publish/integrate_new.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index fd18802f419..880fd804b79 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -92,7 +92,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "harmony.palette", "editorial", "background", - "camerarig" + "camerarig", + "redshiftproxy" ] exclude_families = ["clip"] db_representation_context_keys = [ From 726886758e291042b1540e1bb9df1a3cda157327 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 Apr 2021 11:29:50 +0200 Subject: [PATCH 188/264] Hound: suggestions --- .../hiero/publish/collect_calculate_retime.py | 26 +++++++++++-------- .../hiero/publish/collect_frame_ranges.py | 20 +++++++++----- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pype/plugins/hiero/publish/collect_calculate_retime.py b/pype/plugins/hiero/publish/collect_calculate_retime.py index 820a13c0d32..1615222e233 100644 --- a/pype/plugins/hiero/publish/collect_calculate_retime.py +++ b/pype/plugins/hiero/publish/collect_calculate_retime.py @@ -14,7 +14,8 @@ class CollectCalculateRetime(api.InstancePlugin): def process(self, instance): margin_in = instance.data["retimeMarginIn"] margin_out = instance.data["retimeMarginOut"] - self.log.debug("margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out)) + self.log.debug( + "margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out)) handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] @@ -31,16 +32,19 @@ def process(self, instance): source_in = int(clip.sourceIn()) source_out = int(clip.sourceOut()) speed = track_item.playbackSpeed() - self.log.debug("_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\ - \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n handle_start: `{5}`,\n handle_end: `{6}`".format( - timeline_in, - timeline_out, - source_in, - source_out, - speed, - handle_start, - handle_end - )) + self.log.debug( + "_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\ + \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`, \ + \n handle_start: `{5}`,\n handle_end: `{6}`".format( + timeline_in, + timeline_out, + source_in, + source_out, + speed, + handle_start, + handle_end + ) + ) # loop withing subtrack items source_in_change = 0 diff --git a/pype/plugins/hiero/publish/collect_frame_ranges.py b/pype/plugins/hiero/publish/collect_frame_ranges.py index 9dba48bbd7c..333aa9f6c41 100644 --- a/pype/plugins/hiero/publish/collect_frame_ranges.py +++ b/pype/plugins/hiero/publish/collect_frame_ranges.py @@ -2,26 +2,34 @@ class CollectClipFrameRanges(pyblish.api.InstancePlugin): - """Collect all frame range data: source(In,Out), timeline(In,Out), edit_(in, out), f(start, end)""" + """Collect all frame range data. + + Collecting data: + sourceIn, sourceOut with handles, + clipIn, clipOut with handles, + clipDuration with handles + frameStart, frameEnd + """ order = pyblish.api.CollectorOrder + 0.101 label = "Collect Frame Ranges" hosts = ["hiero"] def process(self, instance): - - data = dict() + data = {} track_item = instance.data["item"] item_handle_in = int(track_item.handleInLength()) item_handle_out = int(track_item.handleOutLength()) - + # Timeline data. handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - source_in_h = instance.data("sourceInH", + source_in_h = instance.data( + "sourceInH", (instance.data("sourceIn") + item_handle_in) - handle_start) - source_out_h = instance.data("sourceOutH", + source_out_h = instance.data( + "sourceOutH", (instance.data("sourceOut") - item_handle_out) + handle_end) timeline_in = instance.data["clipIn"] From 7fe401ea13df080e53852360f2ce89f525fe8f85 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 Apr 2021 11:38:56 +0200 Subject: [PATCH 189/264] Hound: suggestion --- pype/plugins/hiero/publish/collect_calculate_retime.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/hiero/publish/collect_calculate_retime.py b/pype/plugins/hiero/publish/collect_calculate_retime.py index 1615222e233..77da76d801a 100644 --- a/pype/plugins/hiero/publish/collect_calculate_retime.py +++ b/pype/plugins/hiero/publish/collect_calculate_retime.py @@ -15,7 +15,8 @@ def process(self, instance): margin_in = instance.data["retimeMarginIn"] margin_out = instance.data["retimeMarginOut"] self.log.debug( - "margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out)) + "margin_in: '{0}', margin_out: '{1}'".format( + margin_in, margin_out)) handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] From 2a5a1431c5525894ef90fadbb4a0cd87dd4edd4f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 20 Apr 2021 11:38:38 +0100 Subject: [PATCH 190/264] Enhance review letterbox feature. - backward compatible with single float for config. - support for pillar boxes. - expose thickness of boxes to config. - expose color of boxes to config. --- pype/plugins/global/publish/extract_review.py | 75 +++++++++++++++---- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 085d23dc80f..6c7965ba9ba 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -747,6 +747,18 @@ def audio_args(self, instance, temp_data, duration_seconds): return audio_in_args, audio_filters, audio_out_args + def get_letterbox_filters(self, ratio, thickness="fill", color="black"): + top_box = ( + "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t={}:c={}" + ).format(ratio, thickness, color) + + bottom_box = ( + "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" + ":iw:round((ih-(iw*(1/{0})))/2):t={1}:c={2}" + ).format(ratio, thickness, color) + + return [top_box, bottom_box] + def rescaling_filters(self, temp_data, output_def, new_repre): """Prepare vieo filters based on tags in new representation. @@ -880,28 +892,61 @@ def rescaling_filters(self, temp_data, output_def, new_repre): # letter_box if letter_box: + letter_box_ratio = letter_box + if isinstance(letter_box, dict): + letter_box_ratio = letter_box["ratio"] + if input_res_ratio == output_res_ratio: - letter_box /= pixel_aspect + letter_box_ratio /= pixel_aspect elif input_res_ratio < output_res_ratio: - letter_box /= scale_factor_by_width + letter_box_ratio /= scale_factor_by_width else: - letter_box /= scale_factor_by_height + letter_box_ratio /= scale_factor_by_height - scale_filter = "scale={}x{}:flags=lanczos".format( - output_width, output_height - ) + filters.extend([ + "scale={}x{}:flags=lanczos".format( + output_width, output_height + ), + "setsar=1" + ]) - top_box = ( - "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t=fill:c=black" - ).format(letter_box) + if letter_box and isinstance(letter_box, float): + filters.extend(self.get_letterbox_filters(letter_box)) - bottom_box = ( - "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" - ":iw:round((ih-(iw*(1/{0})))/2):t=fill:c=black" - ).format(letter_box) + if letter_box and isinstance(letter_box, dict): + if letter_box["state"] == "letterbox": + filters.extend( + self.get_letterbox_filters( + letter_box["ratio"], + letter_box["thickness"], + letter_box["color"] + ) + ) + elif letter_box["state"] == "pillar": + right_box = ( + "drawbox=0:0:round((iw-(ih*{}))/2):ih:t={}:c={}" + ).format( + letter_box["ratio"], + letter_box["thickness"], + letter_box["color"] + ) + + left_box = ( + "drawbox=(round(ih*{0})+round((iw-(ih*{0}))/2))" + ":0:round((iw-(ih*{0}))/2):ih:t={1}:c={2}" + ).format( + letter_box["ratio"], + letter_box["thickness"], + letter_box["color"] + ) - # Add letter box filters - filters.extend([scale_filter, "setsar=1", top_box, bottom_box]) + filters.extend([right_box, left_box]) + else: + raise ValueError( + "Letterbox state \"{}\" is not recognized".format( + letter_box["state"] + ) + ) # scaling none square pixels and 1920 width if ( From 960250cf807f8e5f34e00014c78397a157cb66ed Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 20 Apr 2021 12:49:01 +0200 Subject: [PATCH 191/264] add default shader and mesh preview option --- pype/plugins/maya/load/load_redshift_proxy.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/load/load_redshift_proxy.py b/pype/plugins/maya/load/load_redshift_proxy.py index b4f54cd8a2e..b01aff066b4 100644 --- a/pype/plugins/maya/load/load_redshift_proxy.py +++ b/pype/plugins/maya/load/load_redshift_proxy.py @@ -108,7 +108,7 @@ def remove(self, container): def switch(self, container, representation): self.update(container, representation) - def create_rs_proxy(self, name, path): + def create_rs_proxy(self, name, path): """Creates Redshift Proxies showing a proxy object. Args: @@ -123,14 +123,28 @@ def create_rs_proxy(self, name, path): rs_mesh = cmds.createNode( 'RedshiftProxyMesh', name="{}_RS".format(name)) mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) - + # Create a new shader RedshiftMaterial shader + rs_shader = cmds.shadingNode('RedshiftMaterial', asShader=True) + cmds.setAttr( + "{}.diffuse_color".format(rs_shader), + 0.35, 0.35, 0.35, type='double3') + # Create shading group for it + rs_shading_group = cmds.sets( + renderable=True, noSurfaceShader=True, empty=True, name='rsSG') + cmds.connectAttr("{}.outColor".format(rs_shader), + "{}.surfaceShader".format(rs_shading_group), + force=True) + + # add path to proxy cmds.setAttr("{}.fileName".format(rs_mesh), path, type="string") + # connect nodes cmds.connectAttr("{}.outMesh".format(rs_mesh), "{}.inMesh".format(mesh_shape)) + # put proxy under group node group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, parent=True, fullPath=True) @@ -139,9 +153,16 @@ def create_rs_proxy(self, name, path): # determine if we need to enable animation support files_in_folder = os.listdir(os.path.dirname(path)) - collections, remainder = clique.assemble(files_in_folder) + collections, _ = clique.assemble(files_in_folder) + # set Preview Mesh on proxy if collections: cmds.setAttr("{}.useFrameExtension".format(rs_mesh), 1) + cmds.setAttr("{}.displayMode".format(rs_mesh), 1) + cmds.refresh() + + # add mesh to new shading group + cmds.sets([mesh_shape], addElement=rs_shading_group) + return nodes, group_node From 49ecf395947bc8d64d49fddbce200f250d93dad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 20 Apr 2021 16:03:36 +0200 Subject: [PATCH 192/264] fix indent --- pype/plugins/maya/load/load_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_redshift_proxy.py b/pype/plugins/maya/load/load_redshift_proxy.py index b01aff066b4..96941b25e7d 100644 --- a/pype/plugins/maya/load/load_redshift_proxy.py +++ b/pype/plugins/maya/load/load_redshift_proxy.py @@ -108,7 +108,7 @@ def remove(self, container): def switch(self, container, representation): self.update(container, representation) - def create_rs_proxy(self, name, path): + def create_rs_proxy(self, name, path): """Creates Redshift Proxies showing a proxy object. Args: From 30be5cae117133b367da9a9b3bb2fcdb5a37c6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:14:58 +0200 Subject: [PATCH 193/264] fix sequence padding --- pype/plugins/maya/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/extract_redshift_proxy.py b/pype/plugins/maya/publish/extract_redshift_proxy.py index 56255eaad43..9dab4701826 100644 --- a/pype/plugins/maya/publish/extract_redshift_proxy.py +++ b/pype/plugins/maya/publish/extract_redshift_proxy.py @@ -45,7 +45,7 @@ def process(self, instance): # Padding is taken from number of digits of the end_frame. # Not sure where Redshift is taking it. repr_files = [ - "{}.{}{}".format(root, str(frame).rjust(int(math.log10(int(end_frame)) + 1), "0"), ext) # noqa: E501 + "{}.{}{}".format(root, str(frame).rjust(4, "0"), ext) # noqa: E501 for frame in range( int(start_frame), int(end_frame) + 1, From ff4d86d2bebd3df778590446b518a6749599e4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:17:34 +0200 Subject: [PATCH 194/264] remove unused import --- pype/plugins/maya/publish/extract_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/maya/publish/extract_redshift_proxy.py b/pype/plugins/maya/publish/extract_redshift_proxy.py index 9dab4701826..4774ec4584d 100644 --- a/pype/plugins/maya/publish/extract_redshift_proxy.py +++ b/pype/plugins/maya/publish/extract_redshift_proxy.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Redshift Proxy extractor.""" import os -import math import avalon.maya import pype.api From 205362f681500d38d5fe59100e9fab8c0ea20f1e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 20 Apr 2021 19:46:04 +0200 Subject: [PATCH 195/264] bump version and add changelog --- .github_changelog_generator | 4 +- CHANGELOG.md | 37 +++++ HISTORY.md | 270 ++++++++++++++++++++++++++++++++++++ pype/version.py | 2 +- 4 files changed, 310 insertions(+), 3 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index fa87c93ccfe..a899afb1808 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -2,9 +2,9 @@ pr-wo-labels=False exclude-labels=duplicate,question,invalid,wontfix,weekly-digest author=False unreleased=True -since-tag=2.13.6 +since-tag=2.16.0 enhancement-label=**Enhancements:** release-branch=2.x/develop issues=False exclude-tags-regex=3.\d.\d.* -future-release=2.16.0 \ No newline at end of file +future-release=2.16.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f9808d327..eef3020c51a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-20) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1) + +**Enhancements:** + +- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/pype/pull/1328) +- TVPaint asset name validation [\#1302](https://github.com/pypeclub/pype/pull/1302) +- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/pype/pull/1299) +- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/pype/pull/1298) +- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297) +- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243) +- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234) +- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206) + +**Fixed bugs:** + +- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312) +- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/pype/pull/1308) +- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303) +- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282) +- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/pype/pull/1275) +- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242) +- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226) +- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217) +- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214) +- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204) +- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202) +- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199) +- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194) +- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178) + + + ## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22) [Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0) @@ -1059,4 +1093,7 @@ A large cleanup release. Most of the change are under the hood. \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* + + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/HISTORY.md b/HISTORY.md index b8b96fb4c38..43681520285 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,270 @@ + + +## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0) + +**Enhancements:** + +- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167) +- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156) +- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152) +- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146) +- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142) +- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138) +- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127) +- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124) +- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117) +- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106) +- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101) +- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) +- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088) +- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080) +- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078) +- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072) +- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008) +- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073) + + +**Fixed bugs:** + +- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174) +- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166) +- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163) +- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125) +- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090) +- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082) +- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067) +- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064) + +## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3) + +**Enhancements:** + +- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) +- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) + +**Fixed bugs:** + +- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085) +- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059) +- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057) +- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046) + +## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2) + +**Enhancements:** + +- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013) + +**Fixed bugs:** + +- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040) +- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036) +- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031) + +## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1) + +**Enhancements:** + +- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011) +- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445) + +**Fixed bugs:** + +- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006) +- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) + +## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0) + +**Enhancements:** + +- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932) +- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926) +- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) +- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993) +- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986) +- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981) +- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965) +- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944) +- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941) +- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938) +- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903) +- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893) +- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891) +- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884) +- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881) +- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840) +- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824) +- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795) +- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695) + +**Fixed bugs:** + +- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995) +- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982) +- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960) +- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839) +- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990) +- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988) +- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984) +- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979) +- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972) +- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971) +- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967) +- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964) +- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959) +- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956) +- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954) +- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953) +- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933) +- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920) +- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909) +- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896) +- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889) +- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831) +- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822) + +**Merged pull requests:** + +- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934) + +## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6) + +**Fixed bugs:** + +- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885) + +**Merged pull requests:** + +- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892) +- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869) + +## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5) + +**Merged pull requests:** + +- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866) + +## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4) + +**Merged pull requests:** + +- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837) + +## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3) + +**Fixed bugs:** + +- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809) +- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807) +- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806) + +**Merged pull requests:** + +- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823) + +## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2) + +**Enhancements:** + +- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767) + +**Fixed bugs:** + +- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768) +- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766) +- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763) + +**Merged pull requests:** + +- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781) +- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769) + +## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1) + +**Enhancements:** + +- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770) +- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761) + +**Fixed bugs:** + +- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760) +- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754) +- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752) +- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748) +- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744) +- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742) + +## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0) + +**Enhancements:** + +- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687) +- Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736) +- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721) +- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719) +- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716) +- 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699) +- Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678) +- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675) +- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674) +- After Effects: base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667) +- Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666) + +**Fixed bugs:** + +- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743) +- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726) +- TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740) +- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738) +- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722) +- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682) +- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639) + +**Deprecated:** + +- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717) +- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715) + +**Merged pull requests:** + +- Application manager [\#728](https://github.com/pypeclub/pype/pull/728) +- Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706) +- Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700) +- 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697) + +## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7) + +**Fixed bugs:** + +- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729) + # Changelog ## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15) @@ -789,4 +1056,7 @@ A large cleanup release. Most of the change are under the hood. - _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* + + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/pype/version.py b/pype/version.py index 8f4a351703b..dc9511ccbf6 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.16.0" +__version__ = "2.16.1" From ee4af8fe3ad171bca9c1c1747fa3274c09e6b078 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 20 Apr 2021 20:32:02 +0200 Subject: [PATCH 196/264] bump version --- CHANGELOG.md | 7 ++++--- pype/version.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef3020c51a..e278c9b82e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-20) +## [2.17.0](https://github.com/pypeclub/pype/tree/2.17.0) (2021-04-20) -[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1) +[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.17.0) **Enhancements:** @@ -11,12 +11,13 @@ - TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/pype/pull/1299) - TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/pype/pull/1298) - Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297) -- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243) +- 3.0 Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243) - After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234) - Show error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206) **Fixed bugs:** +- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/pype/pull/1362) - Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312) - Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/pype/pull/1308) - Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303) diff --git a/pype/version.py b/pype/version.py index dc9511ccbf6..a6b62ff3b29 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.16.1" +__version__ = "2.17.0" From c33d971f9bdf9f141a6773925f6fcade5f7558cd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Apr 2021 09:58:57 +0100 Subject: [PATCH 197/264] Add task name to context pop up. --- pype/hosts/maya/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/hosts/maya/__init__.py b/pype/hosts/maya/__init__.py index 6eeed8cef01..124ecc744f4 100644 --- a/pype/hosts/maya/__init__.py +++ b/pype/hosts/maya/__init__.py @@ -226,6 +226,9 @@ def on_task_changed(*args): lib.set_context_settings() lib.update_content_on_context_change() - lib.show_message("Context was changed", - ("Context was changed to {}".format( - avalon.Session["AVALON_ASSET"]))) + lib.show_message( + "Context was changed", + "Context was changed to {}/{}".format( + avalon.Session["AVALON_ASSET"], avalon.Session["AVALON_TASK"] + ) + ) From 4c18199b2325e13dbda64797530b81f6ad41b3cb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 11:41:00 +0200 Subject: [PATCH 198/264] Fix max instead of min Minimum value must be 0, former solution took always 0 value --- pype/hosts/aftereffects/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/aftereffects/__init__.py b/pype/hosts/aftereffects/__init__.py index 0f813190cbb..f0e94addb8d 100644 --- a/pype/hosts/aftereffects/__init__.py +++ b/pype/hosts/aftereffects/__init__.py @@ -90,7 +90,7 @@ def get_asset_settings(): handle_end = asset_data.get("handleEnd") resolution_width = asset_data.get("resolutionWidth") resolution_height = asset_data.get("resolutionHeight") - duration = frame_end + handle_end - min(frame_start - handle_start, 0) + duration = frame_end + handle_end - max(frame_start - handle_start, 0) entity_type = asset_data.get("entityType") scene_data = { From eb785fbc7d9f2303007ef42ba56f729e5415f5bf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Apr 2021 12:44:06 +0100 Subject: [PATCH 199/264] Move all letterbox code into class method. --- pype/plugins/global/publish/extract_review.py | 120 +++++++++--------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 6c7965ba9ba..a112150db8b 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -747,17 +747,61 @@ def audio_args(self, instance, temp_data, duration_seconds): return audio_in_args, audio_filters, audio_out_args - def get_letterbox_filters(self, ratio, thickness="fill", color="black"): - top_box = ( - "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t={}:c={}" - ).format(ratio, thickness, color) - - bottom_box = ( - "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" - ":iw:round((ih-(iw*(1/{0})))/2):t={1}:c={2}" - ).format(ratio, thickness, color) + def get_letterbox_filters(self, + letter_box_def, + input_res_ratio, + output_res_ratio, + pixel_aspect, + scale_factor_by_width, + scale_factor_by_height): + output = [] + + ratio = letter_box_def + state = "letterbox" + thickness = "fill" + color = "black" + + if isinstance(letter_box_def, dict): + ratio = letter_box_def["ratio"] + state = letter_box_def["state"] + thickness = letter_box_def["thickness"] + color = letter_box_def["color"] + + if input_res_ratio == output_res_ratio: + ratio /= pixel_aspect + elif input_res_ratio < output_res_ratio: + ratio /= scale_factor_by_width + else: + ratio /= scale_factor_by_height + + if state == "letterbox": + top_box = ( + "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t={}:c={}" + ).format(ratio, thickness, color) + + bottom_box = ( + "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" + ":iw:round((ih-(iw*(1/{0})))/2):t={1}:c={2}" + ).format(ratio, thickness, color) + + output.extend([top_box, bottom_box]) + elif state == "pillar": + right_box = ( + "drawbox=0:0:round((iw-(ih*{}))/2):ih:t={}:c={}" + ).format(ratio, thickness, color) + + left_box = ( + "drawbox=(round(ih*{0})+round((iw-(ih*{0}))/2))" + ":0:round((iw-(ih*{0}))/2):ih:t={1}:c={2}" + ).format(ratio, thickness, color) + + output.extend([right_box, left_box]) + else: + raise ValueError( + "Letterbox state \"{}\" is not recognized".format(state) + ) - return [top_box, bottom_box] + return output def rescaling_filters(self, temp_data, output_def, new_repre): """Prepare vieo filters based on tags in new representation. @@ -892,17 +936,6 @@ def rescaling_filters(self, temp_data, output_def, new_repre): # letter_box if letter_box: - letter_box_ratio = letter_box - if isinstance(letter_box, dict): - letter_box_ratio = letter_box["ratio"] - - if input_res_ratio == output_res_ratio: - letter_box_ratio /= pixel_aspect - elif input_res_ratio < output_res_ratio: - letter_box_ratio /= scale_factor_by_width - else: - letter_box_ratio /= scale_factor_by_height - filters.extend([ "scale={}x{}:flags=lanczos".format( output_width, output_height @@ -910,43 +943,16 @@ def rescaling_filters(self, temp_data, output_def, new_repre): "setsar=1" ]) - if letter_box and isinstance(letter_box, float): - filters.extend(self.get_letterbox_filters(letter_box)) - - if letter_box and isinstance(letter_box, dict): - if letter_box["state"] == "letterbox": - filters.extend( - self.get_letterbox_filters( - letter_box["ratio"], - letter_box["thickness"], - letter_box["color"] - ) - ) - elif letter_box["state"] == "pillar": - right_box = ( - "drawbox=0:0:round((iw-(ih*{}))/2):ih:t={}:c={}" - ).format( - letter_box["ratio"], - letter_box["thickness"], - letter_box["color"] - ) - - left_box = ( - "drawbox=(round(ih*{0})+round((iw-(ih*{0}))/2))" - ":0:round((iw-(ih*{0}))/2):ih:t={1}:c={2}" - ).format( - letter_box["ratio"], - letter_box["thickness"], - letter_box["color"] - ) - - filters.extend([right_box, left_box]) - else: - raise ValueError( - "Letterbox state \"{}\" is not recognized".format( - letter_box["state"] - ) + filters.extend( + self.get_letterbox_filters( + letter_box, + input_res_ratio, + output_res_ratio, + pixel_aspect, + scale_factor_by_width, + scale_factor_by_height ) + ) # scaling none square pixels and 1920 width if ( From 13614eba3fb740be15615b2ea0c354ef85bf9f8d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 22 Apr 2021 16:13:07 +0200 Subject: [PATCH 200/264] specifically list history of VrayPluginNodeMtl --- pype/plugins/maya/publish/collect_look.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index 7df85e4ba7a..297509dfabd 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -295,6 +295,13 @@ def collect(self, instance): history = list() for material in materials: history.extend(cmds.listHistory(material)) + + # handle VrayPluginNodeMtl node - see #1397 + vray_plugin_nodes = cmds.ls( + history, type="VRayPluginNodeMtl", long=True) + for vray_node in vray_plugin_nodes: + history.extend(cmds.listHistory(vray_node)) + files = cmds.ls(history, type="file", long=True) files.extend(cmds.ls(history, type="aiImage", long=True)) From f092a93938e443ac2551d77eb3282b72a28bab0c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Apr 2021 17:11:23 +0200 Subject: [PATCH 201/264] Nuke: gpu enabled rendering implementation of https://github.com/pypeclub/OpenPype/pull/1394 --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 65c41ca2ab2..1f630d75768 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -34,10 +34,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_group = "" deadline_department = "" deadline_limit_groups = {} + deadline_use_gpu = False env_allowed_keys = [] env_search_replace_values = {} - def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" families = instance.data["families"] @@ -208,6 +208,10 @@ def payload_submit(self, # Resolve relative references "ProjectPath": script_path, "AWSAssetFile0": render_path, + + # using GPU by default + "UseGpu": self.deadline_use_gpu, + # Only the specific write node is rendered. "WriteNode": exe_node_name }, From 27edde38a2b83abeb28a4540ffffa6ad3c834d00 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Apr 2021 17:30:12 +0200 Subject: [PATCH 202/264] PS - group all published instances, add them Task name Grouped instances will show up together in Loader --- pype/plugins/photoshop/publish/collect_instances.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 5390df768b3..1c260f92ae3 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -1,5 +1,6 @@ import pyblish.api +import avalon.api from avalon import photoshop @@ -19,6 +20,8 @@ class CollectInstances(pyblish.api.ContextPlugin): families_mapping = { "image": [] } + # True will add all instances to same group in Loader + group_by_task_name = False def process(self, context): stub = photoshop.stub() @@ -49,6 +52,12 @@ def process(self, context): layer_data["family"] ] instance.data["publish"] = layer.visible + + if self.group_by_task_name: + task = avalon.api.Session["AVALON_TASK"] + sanitized_task_name = task[0].upper() + task[1:] + instance.data["subsetGroup"] = sanitized_task_name + instance_names.append(layer_data["subset"]) # Produce diagnostic message for any graphical From 67d397f9176a6ca0fe4d0a2472a94b19b1c21dec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 18:22:10 +0200 Subject: [PATCH 203/264] remove XML because can't be reanamed --- setup/houdini/MainMenuCommon.XML | 79 -------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 setup/houdini/MainMenuCommon.XML diff --git a/setup/houdini/MainMenuCommon.XML b/setup/houdini/MainMenuCommon.XML deleted file mode 100644 index 16e92be6883..00000000000 --- a/setup/houdini/MainMenuCommon.XML +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 0943d4d9869437191637dc69ac6e21f6f03da058 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 18:22:30 +0200 Subject: [PATCH 204/264] added back xml houdini menu with lovered extension --- setup/houdini/MainMenuCommon.xml | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 setup/houdini/MainMenuCommon.xml diff --git a/setup/houdini/MainMenuCommon.xml b/setup/houdini/MainMenuCommon.xml new file mode 100644 index 00000000000..16e92be6883 --- /dev/null +++ b/setup/houdini/MainMenuCommon.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 352e053946d2e66b66eb9d22bba7d5446c2d19e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Apr 2021 18:51:44 +0200 Subject: [PATCH 205/264] PS - task name used for subset group is untouched (no capitalization) --- pype/plugins/photoshop/publish/collect_instances.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 1c260f92ae3..085ed26a4a7 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -54,9 +54,8 @@ def process(self, context): instance.data["publish"] = layer.visible if self.group_by_task_name: - task = avalon.api.Session["AVALON_TASK"] - sanitized_task_name = task[0].upper() + task[1:] - instance.data["subsetGroup"] = sanitized_task_name + task_name = avalon.api.Session["AVALON_TASK"] + instance.data["subsetGroup"] = task_name instance_names.append(layer_data["subset"]) From 589aa0c293ed4f6bc09211ee80ff35e872f7d983 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:32:53 +0200 Subject: [PATCH 206/264] set always mark in to 0 and mark out to mark in + duration - 1 --- pype/hosts/tvpaint/lib.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/pype/hosts/tvpaint/lib.py b/pype/hosts/tvpaint/lib.py index 654773b0f2e..405c90191f2 100644 --- a/pype/hosts/tvpaint/lib.py +++ b/pype/hosts/tvpaint/lib.py @@ -48,24 +48,19 @@ def set_context_settings(asset): frame_start = asset["data"].get("frameStart") frame_end = asset["data"].get("frameEnd") - if frame_start and frame_end: - handles = asset["data"].get("handles") or 0 - handle_start = asset["data"].get("handleStart") - if handle_start is None: - handle_start = handles + if frame_start is None or frame_end is None: + print("Frame range was not found!") + return - handle_end = asset["data"].get("handleEnd") - if handle_end is None: - handle_end = handles + handles = asset["data"].get("handles") or 0 + handle_start = asset["data"].get("handleStart") + handle_end = asset["data"].get("handleEnd") + if handle_start is None or handle_end is None: + handle_start = handle_end = handles - frame_start -= int(handle_start) - frame_end += int(handle_end) + # Always start from 0 Mark In and set only Mark Out + mark_in = 0 + mark_out = mark_in + (frame_end - frame_start) + handle_start + handle_end - avalon.tvpaint.lib.execute_george( - "tv_markin {} set".format(frame_start - 1) - ) - avalon.tvpaint.lib.execute_george( - "tv_markout {} set".format(frame_end - 1) - ) - else: - print("Frame range was not found!") + avalon.tvpaint.lib.execute_george("tv_markin {} set".format(mark_in)) + avalon.tvpaint.lib.execute_george("tv_markout {} set".format(mark_out)) From f00e18d404bad6a83b61d034acbe35a754d1e1c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:33:20 +0200 Subject: [PATCH 207/264] set frameStart and frameEnd on instance to context frameStart and frameEnd --- pype/plugins/tvpaint/publish/collect_instances.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index cc236734e5f..cf0949debe8 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -86,8 +86,9 @@ def process(self, context): instance.data["publish"] = any_visible - instance.data["frameStart"] = context.data["sceneMarkIn"] + 1 - instance.data["frameEnd"] = context.data["sceneMarkOut"] + 1 + # Output frame range X not rendered output from TVPaint + instance.data["frameStart"] = context.data["sceneFrameStart"] + instance.data["frameEnd"] = context.data["sceneFrameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) From 5bb42803741d103023dc1683824c89292b2a79b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:34:42 +0200 Subject: [PATCH 208/264] validate only duration in validate marks and repair does not change Mark In value --- .../plugins/tvpaint/publish/validate_marks.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index 73486d10059..f8974f19bba 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -14,10 +14,9 @@ class ValidateMarksRepair(pyblish.api.Action): def process(self, context, plugin): expected_data = ValidateMarks.get_expected_data(context) - expected_data["markIn"] -= 1 - expected_data["markOut"] -= 1 - - lib.execute_george("tv_markin {} set".format(expected_data["markIn"])) + lib.execute_george( + "tv_markin {} set".format(expected_data["markIn"]) + ) lib.execute_george( "tv_markout {} set".format(expected_data["markOut"]) ) @@ -33,18 +32,32 @@ class ValidateMarks(pyblish.api.ContextPlugin): @staticmethod def get_expected_data(context): + scene_mark_in = context.data["sceneMarkIn"] + + # Data collected in `CollectAvalonEntities` + frame_end = context.data["frameEnd"] + frame_start = context.data["frameStart"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + + # Calculate expeted Mark out (Mark In + duration - 1) + expected_mark_out = ( + scene_mark_in + + (frame_end - frame_start) + + handle_start + handle_end + ) return { - "markIn": int(context.data["frameStart"]), + "markIn": scene_mark_in, "markInState": True, - "markOut": int(context.data["frameEnd"]), + "markOut": expected_mark_out, "markOutState": True } def process(self, context): current_data = { - "markIn": context.data["sceneMarkIn"] + 1, + "markIn": context.data["sceneMarkIn"], "markInState": context.data["sceneMarkInState"], - "markOut": context.data["sceneMarkOut"] + 1, + "markOut": context.data["sceneMarkOut"], "markOutState": context.data["sceneMarkOutState"] } expected_data = self.get_expected_data(context) From 44bc83b32d74e62de0271c870c43e7582253c66a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:34:54 +0200 Subject: [PATCH 209/264] changed label of validator --- pype/plugins/tvpaint/publish/validate_marks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index f8974f19bba..853ca973ce0 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -25,7 +25,7 @@ def process(self, context, plugin): class ValidateMarks(pyblish.api.ContextPlugin): """Validate mark in and out are enabled.""" - label = "Validate Marks" + label = "Validate Mark In/Out" order = pyblish.api.ValidatorOrder optional = True actions = [ValidateMarksRepair] From 49e70b77d0efc49cd9bdf0619a8e66f0296d9442 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:35:00 +0200 Subject: [PATCH 210/264] added small docstring --- pype/plugins/tvpaint/publish/validate_marks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/validate_marks.py b/pype/plugins/tvpaint/publish/validate_marks.py index 853ca973ce0..e2ef81e4a44 100644 --- a/pype/plugins/tvpaint/publish/validate_marks.py +++ b/pype/plugins/tvpaint/publish/validate_marks.py @@ -23,7 +23,11 @@ def process(self, context, plugin): class ValidateMarks(pyblish.api.ContextPlugin): - """Validate mark in and out are enabled.""" + """Validate mark in and out are enabled and it's duration. + + Mark In/Out does not have to match frameStart and frameEnd but duration is + important. + """ label = "Validate Mark In/Out" order = pyblish.api.ValidatorOrder From 8530ad779542ea9efc9a25dde7484f4052e0d42f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:50:33 +0200 Subject: [PATCH 211/264] pass mark in/out to render methods --- .../plugins/tvpaint/publish/extract_sequence.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index cec3e2edbca..1489fa67aec 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -48,6 +48,9 @@ def process(self, instance): frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] + mark_in = instance.context.data["sceneMarkIn"] + mark_out = instance.data["sceneMarkOut"] + filename_template = self._get_filename_template(frame_end) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -66,12 +69,16 @@ def process(self, instance): if instance.data["family"] == "review": repre_files, thumbnail_fullpath = self.render_review( - filename_template, output_dir, frame_start, frame_end + filename_template, output_dir, + frame_start, frame_end, + mark_in, mark_out ) else: # Render output repre_files, thumbnail_fullpath = self.render( - filename_template, output_dir, frame_start, frame_end, + filename_template, output_dir, + frame_start, frame_end, + mark_in, mark_out, filtered_layers ) @@ -134,7 +141,8 @@ def _get_filename_template(self, frame_end): return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( - self, filename_template, output_dir, frame_start, frame_end + self, filename_template, output_dir, + frame_start, frame_end, mark_in, mark_out ): """ Export images from TVPaint using `tv_savesequence` command. @@ -187,7 +195,8 @@ def render_review( return output, thumbnail_filepath def render( - self, filename_template, output_dir, frame_start, frame_end, layers + self, filename_template, output_dir, + frame_start, frame_end, mark_in, mark_out, layers ): """ Export images from TVPaint. From c314d600eaedb3376f22046f41a92e756571b7e8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 13:53:02 +0200 Subject: [PATCH 212/264] use proper mark in/out --- pype/plugins/tvpaint/publish/extract_sequence.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 1489fa67aec..c0d0b4be738 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -164,8 +164,6 @@ def render_review( output_dir, filename_template.format(frame=frame_start) ) - mark_in = frame_start - 1 - mark_out = frame_end - 1 george_script_lines = [ "tv_SaveMode \"PNG\"", @@ -233,9 +231,6 @@ def render( self.log.debug("Collecting pre/post behavior of individual layers.") behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) - mark_in_index = frame_start - 1 - mark_out_index = frame_end - 1 - tmp_filename_template = "pos_{pos}." + filename_template files_by_position = {} @@ -248,15 +243,15 @@ def render( tmp_filename_template, output_dir, behavior, - mark_in_index, - mark_out_index + mark_in, + mark_out ) files_by_position[position] = files_by_frames output_filepaths = self._composite_files( files_by_position, - mark_in_index, - mark_out_index, + mark_in, + mark_out, filename_template, output_dir ) From 1917504e4e190f2ba0578aa50c00ae0fa40e4d5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:08:25 +0200 Subject: [PATCH 213/264] don't use frame start/end --- pype/plugins/tvpaint/publish/extract_sequence.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index c0d0b4be738..5720c25071a 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -45,13 +45,10 @@ def process(self, instance): ) family_lowered = instance.data["family"].lower() - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.data["sceneMarkOut"] - filename_template = self._get_filename_template(frame_end) + filename_template = self._get_filename_template(mark_out) ext = os.path.splitext(filename_template)[1].replace(".", "") self.log.debug("Using file template \"{}\"".format(filename_template)) @@ -70,14 +67,12 @@ def process(self, instance): if instance.data["family"] == "review": repre_files, thumbnail_fullpath = self.render_review( filename_template, output_dir, - frame_start, frame_end, mark_in, mark_out ) else: # Render output repre_files, thumbnail_fullpath = self.render( filename_template, output_dir, - frame_start, frame_end, mark_in, mark_out, filtered_layers ) @@ -141,8 +136,7 @@ def _get_filename_template(self, frame_end): return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( - self, filename_template, output_dir, - frame_start, frame_end, mark_in, mark_out + self, filename_template, output_dir, mark_in, mark_out ): """ Export images from TVPaint using `tv_savesequence` command. @@ -162,7 +156,7 @@ def render_review( self.log.debug("Preparing data for rendering.") first_frame_filepath = os.path.join( output_dir, - filename_template.format(frame=frame_start) + filename_template.format(frame=mark_in) ) george_script_lines = [ @@ -178,7 +172,7 @@ def render_review( output = [] first_frame_filepath = None - for frame in range(frame_start, frame_end + 1): + for frame in range(mark_in, mark_out + 1): filename = filename_template.format(frame=frame) output.append(filename) if first_frame_filepath is None: @@ -194,7 +188,7 @@ def render_review( def render( self, filename_template, output_dir, - frame_start, frame_end, mark_in, mark_out, layers + mark_in, mark_out, layers ): """ Export images from TVPaint. From 55c3e370527be906b04150d5e5fdbfd1d891fbb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:08:46 +0200 Subject: [PATCH 214/264] add frameStart and frameEnd only if representation is sequence --- pype/plugins/tvpaint/publish/extract_sequence.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 5720c25071a..2c87e94e799 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -83,7 +83,8 @@ def process(self, instance): tags.append("review") # Sequence of one frame - if len(repre_files) == 1: + single_file = len(repre_files) == 1 + if single_file: repre_files = repre_files[0] new_repre = { @@ -91,10 +92,15 @@ def process(self, instance): "ext": ext, "files": repre_files, "stagingDir": output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, "tags": tags } + + if not single_file: + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + new_repre["frameStart"] = frame_start + new_repre["frameEnd"] = frame_end + self.log.debug("Creating new representation: {}".format(new_repre)) instance.data["representations"].append(new_repre) From d79bb829615d21a144a5f638ff9eeb5967e0c288 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:09:01 +0200 Subject: [PATCH 215/264] skipe representation creation if nothing was rendered --- pype/plugins/tvpaint/publish/extract_sequence.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 2c87e94e799..c89e0dc48a5 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -77,6 +77,11 @@ def process(self, instance): filtered_layers ) + # Sequence of one frame + if not repre_files: + self.log.warning("Extractor did not create any output.") + return + # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): From 5cfa9c63fe7199bf6ce0dd03d225674ec9e9b698 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:09:15 +0200 Subject: [PATCH 216/264] fix return value --- pype/plugins/tvpaint/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index c89e0dc48a5..dace9c66100 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -231,7 +231,7 @@ def render( # Sort layer positions in reverse order sorted_positions = list(reversed(sorted(layers_by_position.keys()))) if not sorted_positions: - return + return [], None self.log.debug("Collecting pre/post behavior of individual layers.") behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) From c0951d984999005b82b551b6dd176161bc279ec5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:09:49 +0200 Subject: [PATCH 217/264] add more check for rendered output --- .../tvpaint/publish/extract_sequence.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index dace9c66100..1be20f8e108 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -251,7 +251,24 @@ def render( mark_in, mark_out ) - files_by_position[position] = files_by_frames + if files_by_frames: + files_by_position[position] = files_by_frames + else: + self.log.warning(( + "Skipped layer \"{}\". Probably out of Mark In/Out range." + ).format(layer["name"])) + + if not files_by_position: + layer_names = set(layer["name"] for layer in layers) + joined_names = ", ".join( + ["\"{}\"".format(name) for name in layer_names] + ) + self.log.warning( + "Layers {} do not have content in range {} - {}".format( + joined_names, mark_in, mark_out + ) + ) + return [], None output_filepaths = self._composite_files( files_by_position, From 45ec2eaec5e43370e3ef046a882c2972b0220ab4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:10:42 +0200 Subject: [PATCH 218/264] skip layers without content in mark in/out range --- .../tvpaint/publish/extract_sequence.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 1be20f8e108..7929334f1fb 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -309,6 +309,22 @@ def _render_layer( layer_id = layer["layer_id"] frame_start_index = layer["frame_start"] frame_end_index = layer["frame_end"] + + pre_behavior = behavior["pre"] + post_behavior = behavior["post"] + + # Check if layer is before mark in + if frame_end_index < mark_in_index: + # Skip layer if post behavior is "none" + if post_behavior == "none": + return {} + + # Check if layer is after mark out + elif frame_start_index > mark_out_index: + # Skip layer if pre behavior is "none" + if pre_behavior == "none": + return {} + exposure_frames = lib.get_exposure_frames( layer_id, frame_start_index, frame_end_index ) @@ -367,8 +383,6 @@ def _render_layer( self.log.debug("Filled frames {}".format(str(_debug_filled_frames))) # Fill frames by pre/post behavior of layer - pre_behavior = behavior["pre"] - post_behavior = behavior["post"] self.log.debug(( "Completing image sequence of layer by pre/post behavior." " PRE: {} | POST: {}" From 4516c694a4c5a07c8e1f2314c09c8ad06c123371 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:13:54 +0200 Subject: [PATCH 219/264] formatting changes --- pype/plugins/tvpaint/publish/extract_sequence.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 7929334f1fb..16452882ff6 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -66,8 +66,7 @@ def process(self, instance): if instance.data["family"] == "review": repre_files, thumbnail_fullpath = self.render_review( - filename_template, output_dir, - mark_in, mark_out + filename_template, output_dir, mark_in, mark_out ) else: # Render output @@ -198,8 +197,7 @@ def render_review( return output, thumbnail_filepath def render( - self, filename_template, output_dir, - mark_in, mark_out, layers + self, filename_template, output_dir, mark_in, mark_out, layers ): """ Export images from TVPaint. From d0eab719e4b5a0b691bc76aea96df402fc5604b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 14:55:42 +0200 Subject: [PATCH 220/264] fixed keys used for frame start/end --- pype/plugins/tvpaint/publish/collect_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index cf0949debe8..d27246a9935 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -87,8 +87,8 @@ def process(self, context): instance.data["publish"] = any_visible # Output frame range X not rendered output from TVPaint - instance.data["frameStart"] = context.data["sceneFrameStart"] - instance.data["frameEnd"] = context.data["sceneFrameEnd"] + instance.data["frameStart"] = context.data["frameStart"] + instance.data["frameEnd"] = context.data["frameEnd"] self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) From 6e1d3fd48b5778e53eadd6cd66637b4e31a725d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 15:49:04 +0200 Subject: [PATCH 221/264] added frame collector which happens after avalon context collector --- .../publish/collect_instance_frames.py | 31 +++++++++++++++++++ .../tvpaint/publish/collect_instances.py | 4 --- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 pype/plugins/tvpaint/publish/collect_instance_frames.py diff --git a/pype/plugins/tvpaint/publish/collect_instance_frames.py b/pype/plugins/tvpaint/publish/collect_instance_frames.py new file mode 100644 index 00000000000..b3488c29fee --- /dev/null +++ b/pype/plugins/tvpaint/publish/collect_instance_frames.py @@ -0,0 +1,31 @@ +import pyblish.api + + +class CollectOutputFrameRange(pyblish.api.ContextPlugin): + label = "Collect output frame range" + order = pyblish.api.CollectorOrder + hosts = ["tvpaint"] + + def process(self, context): + for instance in context: + frame_start = instance.data.get("frameStart") + frame_end = instance.data.get("frameEnd") + if frame_start is not None and frame_end is not None: + self.log.debug( + "Instance {} already has set frames {}-{}".format( + str(instance), frame_start, frame_end + ) + ) + return + + frame_start = context.data.get("frameStart") + frame_end = context.data.get("frameEnd") + + instance.data["frameStart"] = frame_start + instance.data["frameEnd"] = frame_end + + self.log.info( + "Set frames {}-{} on instance {} ".format( + frame_start, frame_end, str(instance) + ) + ) diff --git a/pype/plugins/tvpaint/publish/collect_instances.py b/pype/plugins/tvpaint/publish/collect_instances.py index d27246a9935..27bd8e9edef 100644 --- a/pype/plugins/tvpaint/publish/collect_instances.py +++ b/pype/plugins/tvpaint/publish/collect_instances.py @@ -86,10 +86,6 @@ def process(self, context): instance.data["publish"] = any_visible - # Output frame range X not rendered output from TVPaint - instance.data["frameStart"] = context.data["frameStart"] - instance.data["frameEnd"] = context.data["frameEnd"] - self.log.debug("Created instance: {}\n{}".format( instance, json.dumps(instance.data, indent=4) )) From 217106d3c4d564ce26736e3d50a6a5e4657b5bdc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 15:49:40 +0200 Subject: [PATCH 222/264] rendered frames are renamed to right sequence frames --- .../tvpaint/publish/extract_sequence.py | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 16452882ff6..5949288e3e0 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -46,7 +46,10 @@ def process(self, instance): family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] - mark_out = instance.data["sceneMarkOut"] + mark_out = instance.context.data["sceneMarkOut"] + # Frame start/end may be stored as float + frame_start = int(instance.data["frameStart"]) + frame_end = int(instance.data["frameEnd"]) filename_template = self._get_filename_template(mark_out) ext = os.path.splitext(filename_template)[1].replace(".", "") @@ -66,13 +69,16 @@ def process(self, instance): if instance.data["family"] == "review": repre_files, thumbnail_fullpath = self.render_review( - filename_template, output_dir, mark_in, mark_out + filename_template, output_dir, + mark_in, mark_out, + frame_start, frame_end ) else: # Render output repre_files, thumbnail_fullpath = self.render( filename_template, output_dir, mark_in, mark_out, + frame_start, frame_end, filtered_layers ) @@ -100,8 +106,6 @@ def process(self, instance): } if not single_file: - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] new_repre["frameStart"] = frame_start new_repre["frameEnd"] = frame_end @@ -146,7 +150,8 @@ def _get_filename_template(self, frame_end): return "{{frame:0>{}}}".format(frame_padding) + ".png" def render_review( - self, filename_template, output_dir, mark_in, mark_out + self, filename_template, output_dir, mark_in, mark_out, + frame_start, frame_end ): """ Export images from TVPaint using `tv_savesequence` command. @@ -180,13 +185,28 @@ def render_review( ] lib.execute_george_through_file("\n".join(george_script_lines)) - output = [] + reversed_repre_filepaths = [] + marks_range = range(mark_out, mark_in - 1, -1) + frames_range = range(frame_end, frame_start - 1, -1) + for mark, frame in zip(marks_range, frames_range): + new_filename = filename_template.format(frame=frame) + new_filepath = os.path.join(output_dir, new_filename) + reversed_repre_filepaths.append(new_filepath) + + if mark != frame: + old_filename = filename_template.format(frame=mark) + old_filepath = os.path.join(output_dir, old_filename) + os.rename(old_filepath, new_filepath) + + repre_filepaths = list(reversed(reversed_repre_filepaths)) + repre_files = [ + os.path.basename(path) + for path in repre_filepaths + ] + first_frame_filepath = None - for frame in range(mark_in, mark_out + 1): - filename = filename_template.format(frame=frame) - output.append(filename) - if first_frame_filepath is None: - first_frame_filepath = os.path.join(output_dir, filename) + if repre_filepaths: + first_frame_filepath = repre_filepaths[0] thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") if first_frame_filepath and os.path.exists(first_frame_filepath): @@ -194,10 +214,11 @@ def render_review( thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return output, thumbnail_filepath + return repre_files, thumbnail_filepath def render( - self, filename_template, output_dir, mark_in, mark_out, layers + self, filename_template, output_dir, mark_in, mark_out, + frame_start, frame_end, layers ): """ Export images from TVPaint. @@ -268,7 +289,7 @@ def render( ) return [], None - output_filepaths = self._composite_files( + output_filepaths_by_frame = self._composite_files( files_by_position, mark_in, mark_out, @@ -277,11 +298,29 @@ def render( ) self._cleanup_tmp_files(files_by_position) + reversed_repre_filepaths = [] + marks_range = range(mark_out, mark_in - 1, -1) + frames_range = range(frame_end, frame_start - 1, -1) + for mark, frame in zip(marks_range, frames_range): + new_filename = filename_template.format(frame=frame) + new_filepath = os.path.join(output_dir, new_filename) + reversed_repre_filepaths.append(new_filepath) + + if mark != frame: + old_filepath = output_filepaths_by_frame[mark] + os.rename(old_filepath, new_filepath) + + repre_filepaths = list(reversed(reversed_repre_filepaths)) + repre_files = [ + os.path.basename(path) + for path in repre_filepaths + ] + thumbnail_src_filepath = None - thumbnail_filepath = None - if output_filepaths: - thumbnail_src_filepath = tuple(sorted(output_filepaths))[0] + if repre_filepaths: + thumbnail_src_filepath = repre_filepaths[0] + thumbnail_filepath = None 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") @@ -289,10 +328,6 @@ def render( thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - repre_files = [ - os.path.basename(path) - for path in output_filepaths - ] return repre_files, thumbnail_filepath def _render_layer( @@ -573,14 +608,14 @@ def _composite_files( process_count -= 1 processes = {} - output_filepaths = [] + output_filepaths_by_frame = {} missing_frame_paths = [] random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] output_filename = filename_template.format(frame=frame_idx + 1) output_filepath = os.path.join(output_dir, output_filename) - output_filepaths.append(output_filepath) + output_filepaths_by_frame[frame_idx] = output_filepath # Store information about missing frame and skip if not image_filepaths: @@ -604,7 +639,7 @@ def _composite_files( random_frame_path = output_filepath self.log.info( - "Running {} compositing processes - this mey take a while.".format( + "Running {} compositing processes - this may take a while.".format( len(processes) ) ) @@ -646,7 +681,7 @@ def _composite_files( transparent_filepath = filepath else: self._copy_image(transparent_filepath, filepath) - return output_filepaths + return output_filepaths_by_frame def _cleanup_tmp_files(self, files_by_position): """Remove temporary files that were used for compositing.""" From 74f763da70d3c43f49d937002aa9b816d88de4c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 15:52:02 +0200 Subject: [PATCH 223/264] use bigger value of mark out of frame end to prepare template --- pype/plugins/tvpaint/publish/extract_sequence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 5949288e3e0..fa4aebbbe7d 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -51,7 +51,10 @@ def process(self, instance): frame_start = int(instance.data["frameStart"]) frame_end = int(instance.data["frameEnd"]) - filename_template = self._get_filename_template(mark_out) + filename_template = self._get_filename_template( + # Use the biggest number + max(mark_out, frame_end) + ) ext = os.path.splitext(filename_template)[1].replace(".", "") self.log.debug("Using file template \"{}\"".format(filename_template)) From 2d2fb573ec81c9e8e7781ab402cf3afb0e109c3a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Apr 2021 16:00:30 +0200 Subject: [PATCH 224/264] added docstring to new collector --- pype/plugins/tvpaint/publish/collect_instance_frames.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/tvpaint/publish/collect_instance_frames.py b/pype/plugins/tvpaint/publish/collect_instance_frames.py index b3488c29fee..f291c363b8e 100644 --- a/pype/plugins/tvpaint/publish/collect_instance_frames.py +++ b/pype/plugins/tvpaint/publish/collect_instance_frames.py @@ -2,6 +2,12 @@ class CollectOutputFrameRange(pyblish.api.ContextPlugin): + """Collect frame start/end from context. + + When instances are collected context does not contain `frameStart` and + `frameEnd` keys yet. They are collected in global plugin + `CollectAvalonEntities`. + """ label = "Collect output frame range" order = pyblish.api.CollectorOrder hosts = ["tvpaint"] From 16bd6e64ef314fa813deacc5c5e477a118854603 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 28 Apr 2021 18:09:38 +0200 Subject: [PATCH 225/264] AE - validation for duration was 1 frame shorter --- pype/hosts/aftereffects/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/aftereffects/__init__.py b/pype/hosts/aftereffects/__init__.py index f0e94addb8d..5d837a5c481 100644 --- a/pype/hosts/aftereffects/__init__.py +++ b/pype/hosts/aftereffects/__init__.py @@ -90,7 +90,7 @@ def get_asset_settings(): handle_end = asset_data.get("handleEnd") resolution_width = asset_data.get("resolutionWidth") resolution_height = asset_data.get("resolutionHeight") - duration = frame_end + handle_end - max(frame_start - handle_start, 0) + duration = (frame_end - frame_start + 1) + handle_start + handle_end entity_type = asset_data.get("entityType") scene_data = { From 8edd552f4db8f4cbbd7f6224e4b1d9f244185ca9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Apr 2021 11:56:26 +0200 Subject: [PATCH 226/264] render methods do not rename output filenames only render mark in->out --- .../tvpaint/publish/extract_sequence.py | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index fa4aebbbe7d..c2fcf771cb7 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -71,22 +71,19 @@ def process(self, instance): ) if instance.data["family"] == "review": - repre_files, thumbnail_fullpath = self.render_review( - filename_template, output_dir, - mark_in, mark_out, - frame_start, frame_end + output_filenames, thumbnail_fullpath = self.render_review( + filename_template, output_dir, mark_in, mark_out ) else: # Render output - repre_files, thumbnail_fullpath = self.render( + output_filenames, thumbnail_fullpath = self.render( filename_template, output_dir, mark_in, mark_out, - frame_start, frame_end, filtered_layers ) # Sequence of one frame - if not repre_files: + if not output_filenames: self.log.warning("Extractor did not create any output.") return @@ -152,10 +149,7 @@ def _get_filename_template(self, frame_end): return "{{frame:0>{}}}".format(frame_padding) + ".png" - def render_review( - self, filename_template, output_dir, mark_in, mark_out, - frame_start, frame_end - ): + def render_review(self, filename_template, output_dir, mark_in, mark_out): """ Export images from TVPaint using `tv_savesequence` command. Args: @@ -164,8 +158,8 @@ def render_review( keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. output_dir (str): Directory where files will be stored. - first_frame (int): Starting frame from which export will begin. - last_frame (int): On which frame export will end. + mark_in (int): Starting frame index from which export will begin. + mark_out (int): On which frame index export will end. Retruns: tuple: With 2 items first is list of filenames second is path to @@ -188,28 +182,22 @@ def render_review( ] lib.execute_george_through_file("\n".join(george_script_lines)) - reversed_repre_filepaths = [] - marks_range = range(mark_out, mark_in - 1, -1) - frames_range = range(frame_end, frame_start - 1, -1) - for mark, frame in zip(marks_range, frames_range): - new_filename = filename_template.format(frame=frame) - new_filepath = os.path.join(output_dir, new_filename) - reversed_repre_filepaths.append(new_filepath) - - if mark != frame: - old_filename = filename_template.format(frame=mark) - old_filepath = os.path.join(output_dir, old_filename) - os.rename(old_filepath, new_filepath) - - repre_filepaths = list(reversed(reversed_repre_filepaths)) - repre_files = [ - os.path.basename(path) - for path in repre_filepaths - ] - first_frame_filepath = None - if repre_filepaths: - first_frame_filepath = repre_filepaths[0] + output_filenames = [] + for frame in range(mark_in, mark_out + 1): + filename = filename_template.format(frame=frame) + output_filenames.append(filename) + + filepath = os.path.join(output_dir, filename) + if not os.path.exists(filepath): + raise AssertionError( + "Output was not rendered. File was not found {}".format( + filepath + ) + ) + + if first_frame_filepath is None: + first_frame_filepath = filepath thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") if first_frame_filepath and os.path.exists(first_frame_filepath): @@ -217,12 +205,10 @@ def render_review( thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return repre_files, thumbnail_filepath - def render( - self, filename_template, output_dir, mark_in, mark_out, - frame_start, frame_end, layers - ): + return output_filenames, thumbnail_filepath + + def render(self, filename_template, output_dir, mark_in, mark_out, layers): """ Export images from TVPaint. Args: @@ -231,8 +217,8 @@ def render( keyword argument `{frame}` or index argument (for same value). Extension in template must match `save_mode`. output_dir (str): Directory where files will be stored. - first_frame (int): Starting frame from which export will begin. - last_frame (int): On which frame export will end. + mark_in (int): Starting frame index from which export will begin. + mark_out (int): On which frame index export will end. layers (list): List of layers to be exported. Retruns: @@ -292,7 +278,7 @@ def render( ) return [], None - output_filepaths_by_frame = self._composite_files( + output_filepaths = self._composite_files( files_by_position, mark_in, mark_out, @@ -301,27 +287,14 @@ def render( ) self._cleanup_tmp_files(files_by_position) - reversed_repre_filepaths = [] - marks_range = range(mark_out, mark_in - 1, -1) - frames_range = range(frame_end, frame_start - 1, -1) - for mark, frame in zip(marks_range, frames_range): - new_filename = filename_template.format(frame=frame) - new_filepath = os.path.join(output_dir, new_filename) - reversed_repre_filepaths.append(new_filepath) - - if mark != frame: - old_filepath = output_filepaths_by_frame[mark] - os.rename(old_filepath, new_filepath) - - repre_filepaths = list(reversed(reversed_repre_filepaths)) - repre_files = [ - os.path.basename(path) - for path in repre_filepaths + output_filenames = [ + os.path.basename(filepath) + for filepath in output_filepaths ] thumbnail_src_filepath = None - if repre_filepaths: - thumbnail_src_filepath = repre_filepaths[0] + if output_filepaths: + thumbnail_src_filepath = output_filepaths[0] thumbnail_filepath = None if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): @@ -331,7 +304,7 @@ def render( thumbnail_obj.paste(source_img) thumbnail_obj.save(thumbnail_filepath) - return repre_files, thumbnail_filepath + return output_filenames, thumbnail_filepath def _render_layer( self, @@ -611,14 +584,14 @@ def _composite_files( process_count -= 1 processes = {} - output_filepaths_by_frame = {} + output_filepaths = [] missing_frame_paths = [] random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] output_filename = filename_template.format(frame=frame_idx + 1) output_filepath = os.path.join(output_dir, output_filename) - output_filepaths_by_frame[frame_idx] = output_filepath + output_filepaths.append(output_filepath) # Store information about missing frame and skip if not image_filepaths: @@ -684,7 +657,7 @@ def _composite_files( transparent_filepath = filepath else: self._copy_image(transparent_filepath, filepath) - return output_filepaths_by_frame + return output_filepaths def _cleanup_tmp_files(self, files_by_position): """Remove temporary files that were used for compositing.""" From f213372e821c0b8867081d93aae56b393121b3e2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Apr 2021 11:56:58 +0200 Subject: [PATCH 227/264] prepare and check all frames before rendering --- .../tvpaint/publish/extract_sequence.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index c2fcf771cb7..719c88dab65 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -51,6 +51,54 @@ def process(self, instance): frame_start = int(instance.data["frameStart"]) frame_end = int(instance.data["frameEnd"]) + # Handles are not stored per instance but on Context + handle_start = instance.context.data["handleStart"] + handle_end = instance.context.data["handleEnd"] + + # --- Fallbacks ---------------------------------------------------- + # This is required if validations of ranges are ignored. + # - all of this code won't change processing if range to render + # match to range of expected output + + # Prepare output frames + output_frame_start = frame_start - handle_start + output_frame_end = frame_end + handle_end + + # Change output frame start to 0 if handles cause it's negative number + if output_frame_start < 0: + self.log.warning(( + "Frame start with handles has negative value." + " Changed to \"0\". Frames start: {}, Handle Start: {}" + ).format(frame_start, handle_start)) + output_frame_start = 0 + + # Check Marks range and output range + output_range = output_frame_end - output_frame_start + marks_range = mark_out - mark_in + + # Lower Mark Out if mark range is bigger than output + # - do not rendered not used frames + if output_range < marks_range: + new_mark_out = mark_out - (marks_range - output_range) + self.log.warning(( + "Lowering render range to {} frames. Changed Mark Out {} -> {}" + ).format(marks_range + 1, mark_out, new_mark_out)) + # Assign new mark out to variable + mark_out = new_mark_out + + # Lower output frame end so representation has right `frameEnd` value + elif output_range > marks_range: + new_output_frame_end = ( + output_frame_end - (output_range - marks_range) + ) + self.log.warning(( + "Lowering representation range to {} frames." + " Changed frame end {} -> {}" + ).format(output_range + 1, mark_out, new_mark_out)) + output_frame_end = new_output_frame_end + + # ------------------------------------------------------------------- + filename_template = self._get_filename_template( # Use the biggest number max(mark_out, frame_end) From 3806166c269b4ea3703120e3cae82afa732c7899 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Apr 2021 11:58:40 +0200 Subject: [PATCH 228/264] rendered frames are renamed to proper sequence frames in single method --- .../tvpaint/publish/extract_sequence.py | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index 719c88dab65..e00ff7e8520 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -135,6 +135,12 @@ def process(self, instance): self.log.warning("Extractor did not create any output.") return + repre_files = self._rename_output_files( + filename_template, output_dir, + mark_in, mark_out, + output_frame_start, output_frame_end + ) + # Fill tags and new families tags = [] if family_lowered in ("review", "renderlayer"): @@ -154,8 +160,8 @@ def process(self, instance): } if not single_file: - new_repre["frameStart"] = frame_start - new_repre["frameEnd"] = frame_end + new_repre["frameStart"] = output_frame_start + new_repre["frameEnd"] = output_frame_end self.log.debug("Creating new representation: {}".format(new_repre)) @@ -197,6 +203,44 @@ def _get_filename_template(self, frame_end): return "{{frame:0>{}}}".format(frame_padding) + ".png" + def _rename_output_files( + self, filename_template, output_dir, + mark_in, mark_out, output_frame_start, output_frame_end + ): + # Use differnet ranges based on Mark In and output Frame Start values + # - this is to make sure that filename renaming won't affect files that + # are not renamed yet + mark_start_is_less = bool(mark_in < output_frame_start) + if mark_start_is_less: + marks_range = range(mark_out, mark_in - 1, -1) + frames_range = range(output_frame_end, output_frame_start - 1, -1) + else: + # This is less possible situation as frame start will be in most + # cases higher than Mark In. + marks_range = range(mark_in, mark_out + 1) + frames_range = range(output_frame_start, output_frame_end + 1) + + repre_filepaths = [] + for mark, frame in zip(marks_range, frames_range): + new_filename = filename_template.format(frame=frame) + new_filepath = os.path.join(output_dir, new_filename) + + repre_filepaths.append(new_filepath) + + if mark != frame: + old_filename = filename_template.format(frame=frame) + old_filepath = os.path.join(output_dir, old_filename) + os.rename(old_filepath, new_filepath) + + # Reverse repre files order if output + if mark_start_is_less: + repre_filepaths = list(reversed(repre_filepaths)) + + return [ + os.path.basename(path) + for path in repre_filepaths + ] + def render_review(self, filename_template, output_dir, mark_in, mark_out): """ Export images from TVPaint using `tv_savesequence` command. From 17406ac2dc4a6d60be13fcb0804145986c34bea4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Apr 2021 12:24:49 +0200 Subject: [PATCH 229/264] fix used frames --- pype/plugins/tvpaint/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index e00ff7e8520..ac8be0e64e6 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -228,7 +228,7 @@ def _rename_output_files( repre_filepaths.append(new_filepath) if mark != frame: - old_filename = filename_template.format(frame=frame) + old_filename = filename_template.format(frame=mark) old_filepath = os.path.join(output_dir, old_filename) os.rename(old_filepath, new_filepath) @@ -681,7 +681,7 @@ def _composite_files( random_frame_path = None for frame_idx in sorted(images_by_frame.keys()): image_filepaths = images_by_frame[frame_idx] - output_filename = filename_template.format(frame=frame_idx + 1) + output_filename = filename_template.format(frame=frame_idx) output_filepath = os.path.join(output_dir, output_filename) output_filepaths.append(output_filepath) From 09b86bda531ba47cd69037ff4ca0722e5acc168a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Apr 2021 12:25:04 +0200 Subject: [PATCH 230/264] added prefix to temp directory --- pype/plugins/tvpaint/publish/extract_sequence.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/tvpaint/publish/extract_sequence.py b/pype/plugins/tvpaint/publish/extract_sequence.py index ac8be0e64e6..a50f1a91f21 100644 --- a/pype/plugins/tvpaint/publish/extract_sequence.py +++ b/pype/plugins/tvpaint/publish/extract_sequence.py @@ -111,7 +111,9 @@ def process(self, instance): output_dir = instance.data.get("stagingDir") if not output_dir: # Create temp folder if staging dir is not set - output_dir = tempfile.mkdtemp().replace("\\", "/") + output_dir = ( + tempfile.mkdtemp(prefix="tvpaint_render_") + ).replace("\\", "/") instance.data["stagingDir"] = output_dir self.log.debug( From 37ba4dacbc0dc40f5828bcb984e1e6a634fc85cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Apr 2021 15:05:39 +0200 Subject: [PATCH 231/264] Nuke: addressing https://github.com/pypeclub/client/issues/57 --- pype/plugins/nuke/load/load_mov.py | 9 ++++----- pype/plugins/nuke/load/load_sequence.py | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pype/plugins/nuke/load/load_mov.py b/pype/plugins/nuke/load/load_mov.py index 68a92060d80..429b63cba01 100644 --- a/pype/plugins/nuke/load/load_mov.py +++ b/pype/plugins/nuke/load/load_mov.py @@ -165,13 +165,12 @@ def load(self, context, name, namespace, data): } read_name = self.name_expression.format(**name_data) - + read_node = nuke.createNode( + "Read", + "name {}".format(read_name) + ) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): - read_node = nuke.createNode( - "Read", - "name {}".format(read_name) - ) read_node["file"].setValue(file) self.loader_shift(read_node, first) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 1648dc8b630..4428dc54158 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -151,12 +151,12 @@ def load(self, context, name, namespace, data): read_name = self.name_expression.format(**name_data) + r = nuke.createNode( + "Read", + "name {}".format(read_name)) + # Create the Loader with the filename path set with viewer_update_and_undo_stop(): - # TODO: it might be universal read to img/geo/camera - r = nuke.createNode( - "Read", - "name {}".format(read_name)) r["file"].setValue(file) # Set colorspace defined in version data From e491ed84985689f64e312e22410beb5d022ab4e4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 30 Apr 2021 18:18:37 +0200 Subject: [PATCH 232/264] bump version --- .github_changelog_generator | 2 +- CHANGELOG.md | 71 ++++++++++++++++++++++++------------- pype/version.py | 2 +- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index a899afb1808..60b6b8e8116 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -7,4 +7,4 @@ enhancement-label=**Enhancements:** release-branch=2.x/develop issues=False exclude-tags-regex=3.\d.\d.* -future-release=2.16.1 \ No newline at end of file +future-release=2.17.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e278c9b82e9..c7a3930d2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,37 +1,58 @@ # Changelog -## [2.17.0](https://github.com/pypeclub/pype/tree/2.17.0) (2021-04-20) +## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30) -[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.17.0) +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1) **Enhancements:** -- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/pype/pull/1328) -- TVPaint asset name validation [\#1302](https://github.com/pypeclub/pype/pull/1302) -- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/pype/pull/1299) -- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/pype/pull/1298) -- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297) -- 3.0 Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243) -- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234) -- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206) +- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) +- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) +- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) +- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) +- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) +- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) **Fixed bugs:** -- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/pype/pull/1362) -- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312) -- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/pype/pull/1308) -- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303) -- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282) -- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/pype/pull/1275) -- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242) -- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226) -- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217) -- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214) -- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204) -- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202) -- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199) -- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194) -- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178) +- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) +- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) +- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) +- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) + + +## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/3.0.0-beta2...2.17.0) + +**Enhancements:** + +- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) +- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) +- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) +- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) +- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298) +- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297) +- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234) +- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206) + +**Fixed bugs:** + +- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362) +- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308) +- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282) +- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194) +- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312) +- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303) +- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275) +- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242) +- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) +- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) +- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) +- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204) +- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) +- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) +- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178) diff --git a/pype/version.py b/pype/version.py index a6b62ff3b29..19768736314 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.17.0" +__version__ = "2.17.1" From 647246360230a748c4df1343df1817e61d36562d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 4 May 2021 16:01:11 +0200 Subject: [PATCH 233/264] replace slash with underscore in app name --- pype/modules/ftrack/lib/avalon_sync.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index a95bc00d77b..cba98251f8f 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -192,23 +192,25 @@ def get_project_apps(in_app_list): "Unexpected error happend during preparation of application" ) warnings = collections.defaultdict(list) - for app in in_app_list: + for app_name in in_app_list: + # Forwards ompatibility + toml_filename = app_name.replace("/", "_") try: - toml_path = avalon.lib.which_app(app) + toml_path = avalon.lib.which_app(toml_filename) if not toml_path: - log.warning(missing_toml_msg + ' "{}"'.format(app)) - warnings[missing_toml_msg].append(app) + log.warning(missing_toml_msg + ' "{}"'.format(toml_filename)) + warnings[missing_toml_msg].append(toml_filename) continue apps.append({ - "name": app, + "name": app_name, "label": toml.load(toml_path)["label"] }) except Exception: - warnings[error_msg].append(app) + warnings[error_msg].append(toml_filename) log.warning(( "Error has happened during preparing application \"{}\"" - ).format(app), exc_info=True) + ).format(toml_filename), exc_info=True) return apps, warnings From 8ec839f3378c244d1764ee3c6862e332b599cee4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 4 May 2021 16:02:18 +0200 Subject: [PATCH 234/264] custom attribute applications are filled with slashed app names --- pype/modules/ftrack/actions/action_create_cust_attrs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index be7d1693605..dee4d8fa519 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -393,6 +393,13 @@ def application_definitions(self): loaded_data = toml.load(os.path.join(launchers_path, file)) + new_app_name = app_name + name_parts = app_name.split("_") + if len(name_parts) > 1: + name_start = name_parts.pop(0) + name_end = "_".join(name_parts) + new_app_name = "/".join((name_start, name_end)) + ftrack_label = loaded_data.get("ftrack_label") if ftrack_label: parts = app_name.split("_") @@ -401,7 +408,7 @@ def application_definitions(self): else: ftrack_label = loaded_data.get("label", app_name) - app_definitions.append({app_name: ftrack_label}) + app_definitions.append({new_app_name: ftrack_label}) if missing_app_names: self.log.warning( From 7f01f79801f865462754872751ec2998e62c4550 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 4 May 2021 16:13:03 +0200 Subject: [PATCH 235/264] applications in ftrack are created with slashes --- .../ftrack/actions/action_create_cust_attrs.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index dee4d8fa519..752c63d0d91 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -443,8 +443,19 @@ def tools_attribute(self, event): tool_usages = self.presets.get("global", {}).get("tools") or {} tools_data = [] for tool_name, usage in tool_usages.items(): - if usage: - tools_data.append({tool_name: tool_name}) + if not usage or not tool_name: + continue + # Forward compatibility with Pype 3 + # - tools have group and variant joined with slash `/` + parts = tool_name.split("_") + if len(parts) == 1: + # This will cause incompatible tool name + new_name = parts[0] + else: + tool_group = parts.pop(0) + remainder = "_".join(parts) + new_name = "/".join([tool_group, remainder]) + tools_data.append({new_name: new_name}) # Make sure there is at least one item if not tools_data: From bbcc1e9d71c9e29b464460000d4543ee877f68d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 6 May 2021 14:05:19 +0200 Subject: [PATCH 236/264] Nuke: workfile version synced to db version always --- pype/plugins/nuke/publish/collect_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/collect_workfile.py b/pype/plugins/nuke/publish/collect_workfile.py index b95edf0a93a..26186fd027e 100644 --- a/pype/plugins/nuke/publish/collect_workfile.py +++ b/pype/plugins/nuke/publish/collect_workfile.py @@ -73,7 +73,8 @@ def process(self, context): "publish": root.knob('publish').value(), "family": family, "families": [family], - "representations": list() + "representations": list(), + "version": instance.context.data["version"] }) # adding basic script data From 2587925d021b0374b2398a811534871c4ac37380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 6 May 2021 15:13:23 +0200 Subject: [PATCH 237/264] set proper start frame on redshift proxy --- pype/plugins/maya/publish/extract_redshift_proxy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/maya/publish/extract_redshift_proxy.py b/pype/plugins/maya/publish/extract_redshift_proxy.py index 4774ec4584d..a13c5b57c88 100644 --- a/pype/plugins/maya/publish/extract_redshift_proxy.py +++ b/pype/plugins/maya/publish/extract_redshift_proxy.py @@ -73,6 +73,10 @@ def process(self, instance): 'files': repr_files, "stagingDir": staging_dir, } + + if anim_on: + representation['frameStart'] = instance.data["proxyFrameStart"] + instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" From 6a236cc57212ea9cf812597935d8a4c2c5820034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 6 May 2021 15:15:56 +0200 Subject: [PATCH 238/264] remove blanks --- pype/plugins/maya/publish/extract_redshift_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/publish/extract_redshift_proxy.py b/pype/plugins/maya/publish/extract_redshift_proxy.py index a13c5b57c88..f4bdf56281b 100644 --- a/pype/plugins/maya/publish/extract_redshift_proxy.py +++ b/pype/plugins/maya/publish/extract_redshift_proxy.py @@ -73,10 +73,10 @@ def process(self, instance): 'files': repr_files, "stagingDir": staging_dir, } - + if anim_on: representation['frameStart'] = instance.data["proxyFrameStart"] - + instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" From b2559f6aa44571a64347a8c27a1024f9581d097e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 6 May 2021 18:42:13 +0200 Subject: [PATCH 239/264] make use of multiple deadline servers --- .../global/publish/submit_publish_job.py | 2 + pype/plugins/maya/create/create_render.py | 142 +++++++++++++----- pype/plugins/maya/publish/collect_render.py | 21 +++ .../maya/publish/submit_maya_deadline.py | 2 + .../publish/validate_deadline_connection.py | 21 ++- 5 files changed, 136 insertions(+), 52 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 13e65becfc4..3d8c9b719a6 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -928,6 +928,8 @@ def process(self, instance): self.DEADLINE_REST_URL = os.environ.get( "DEADLINE_REST_URL", "http://localhost:8082" ) + if instance.data.get("deadlineUrl"): + self.DEADLINE_REST_URL = instance.data.get("deadlineUrl") assert self.DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" self._submit_deadline_post_job(instance, render_job, instances) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index d63dd75fe41..0d0fd36efb2 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -4,6 +4,9 @@ import json import appdirs import requests +from collections import OrderedDict + +import six from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -79,6 +82,8 @@ class CreateRender(plugin.Creator): 'redshift': 'maya///_' } + deadline_servers = {} + def __init__(self, *args, **kwargs): """Constructor.""" super(CreateRender, self).__init__(*args, **kwargs) @@ -93,10 +98,10 @@ def process(self): use_selection = self.options.get("useSelection") with lib.undo_chunk(): self._create_render_settings() - instance = super(CreateRender, self).process() + self.instance = super(CreateRender, self).process() # create namespace with instance index = 1 - namespace_name = "_{}".format(str(instance)) + namespace_name = "_{}".format(str(self.instance)) try: cmds.namespace(rm=namespace_name) except RuntimeError: @@ -104,12 +109,19 @@ def process(self): pass while(cmds.namespace(exists=namespace_name)): - namespace_name = "_{}{}".format(str(instance), index) + namespace_name = "_{}{}".format(str(self.instance), index) index += 1 namespace = cmds.namespace(add=namespace_name) - cmds.setAttr("{}.machineList".format(instance), lock=True) + # add Deadline server selection list + cmds.scriptJob( + attributeChange=[ + "{}.deadlineServers".format(self.instance), + self._deadline_webservice_changed + ]) + + cmds.setAttr("{}.machineList".format(self.instance), lock=True) self._rs = renderSetup.instance() layers = self._rs.getRenderLayers() if use_selection: @@ -121,7 +133,7 @@ def process(self): render_set = cmds.sets( n="{}:{}".format(namespace, layer.name())) sets.append(render_set) - cmds.sets(sets, forceElement=instance) + cmds.sets(sets, forceElement=self.instance) # if no render layers are present, create default one with # asterix selector @@ -139,12 +151,63 @@ def process(self): cmds.setAttr(self._image_prefix_nodes[renderer], self._image_prefixes[renderer], type="string") + return self.instance + + def _deadline_webservice_changed(self): + """Refresh Deadline served dependent options.""" + # get selected server + webservice = self.deadline_servers[ + self.server_aliases[ + cmds.getAttr("{}.deadlineServers".format(self.instance)) + ] + ] + pools = self._get_deadline_pools(webservice) + cmds.deleteAttr("{}.primaryPool".format(self.instance)) + cmds.deleteAttr("{}.secondaryPool".format(self.instance)) + cmds.addAttr(self.instance, longName="primaryPool", + attributeType="enum", + enumName=":".join(pools)) + cmds.addAttr(self.instance, longName="secondaryPool", + attributeType="enum", + enumName=":".join(["-"] + pools)) + + # cmds.setAttr("{}.secondaryPool".format(self.instance), ["-"] + pools) def _create_render_settings(self): # get pools pools = [] + deadline_url = os.getenv("DEADLINE_REST_URL") + if self.deadline_servers: + for key, server in self.deadline_servers.items(): + if server == "DEADLINE_REST_URL": + self.deadline_servers[key] = deadline_url + + self.server_aliases = self.deadline_servers.keys() + deadline_url = self.deadline_servers[self.server_aliases[0]] + print("deadline servers: {}".format(self.deadline_servers)) + self.data["deadlineServers"] = self.server_aliases + else: + self.data["deadlineServers"] = [deadline_url] + + self.data["suspendPublishJob"] = False + self.data["review"] = True + self.data["extendFrames"] = False + self.data["overrideExistingFrame"] = True + # self.data["useLegacyRenderLayers"] = True + self.data["priority"] = 50 + self.data["framesPerTask"] = 1 + self.data["whitelist"] = False + self.data["machineList"] = "" + self.data["useMayaBatch"] = False + self.data["tileRendering"] = False + self.data["tilesX"] = 2 + self.data["tilesY"] = 2 + self.data["convertToScanline"] = False + self.data["useReferencedAovs"] = False + # Disable for now as this feature is not working yet + # self.data["assScene"] = False - deadline_url = os.environ.get("DEADLINE_REST_URL", None) + self.options = {"useSelection": False} # Force no content muster_url = os.environ.get("MUSTER_REST_URL", None) if deadline_url and muster_url: self.log.error( @@ -155,21 +218,11 @@ def _create_render_settings(self): if deadline_url is None: self.log.warning("Deadline REST API url not found.") else: - argument = "{}/api/pools?NamesOnly=true".format(deadline_url) - try: - response = self._requests_get(argument) - except requests.exceptions.ConnectionError as e: - msg = 'Cannot connect to deadline web service' - self.log.error(msg) - raise RuntimeError('{} - {}'.format(msg, e)) - if not response.ok: - self.log.warning("No pools retrieved") - else: - pools = response.json() - self.data["primaryPool"] = pools - # We add a string "-" to allow the user to not - # set any secondary pools - self.data["secondaryPool"] = ["-"] + pools + pools = self._get_deadline_pools(deadline_url) + self.data["primaryPool"] = pools + # We add a string "-" to allow the user to not + # set any secondary pools + self.data["secondaryPool"] = ["-"] + pools if muster_url is None: self.log.warning("Muster REST API URL not found.") @@ -194,26 +247,6 @@ def _create_render_settings(self): self.data["primaryPool"] = pool_names - self.data["suspendPublishJob"] = False - self.data["review"] = True - self.data["extendFrames"] = False - self.data["overrideExistingFrame"] = True - # self.data["useLegacyRenderLayers"] = True - self.data["priority"] = 50 - self.data["framesPerTask"] = 1 - self.data["whitelist"] = False - self.data["machineList"] = "" - self.data["useMayaBatch"] = False - self.data["tileRendering"] = False - self.data["tilesX"] = 2 - self.data["tilesY"] = 2 - self.data["convertToScanline"] = False - self.data["useReferencedAovs"] = False - # Disable for now as this feature is not working yet - # self.data["assScene"] = False - - self.options = {"useSelection": False} # Force no content - def _load_credentials(self): """Load Muster credentials. @@ -268,6 +301,33 @@ def _get_muster_pools(self): return pools + def _get_deadline_pools(self, webservice): + # type: (str) -> list + """Get pools from Deadline. + + Args: + webservice (str): Server url. + + Returns: + list: Pools. + + Throws: + RuntimeError: If deadline webservice is unreachable. + + """ + argument = "{}/api/pools?NamesOnly=true".format(webservice) + try: + response = self._requests_get(argument) + except requests.exceptions.ConnectionError as exc: + msg = 'Cannot connect to deadline web service' + self.log.error(msg) + six.reraise(exc, RuntimeError('{} - {}'.format(msg, exc))) + if not response.ok: + self.log.warning("No pools retrieved") + return [] + + return response.json() + def _show_login(self): # authentication token expired so we need to login to Muster # again to get it. We use Pype API call to show login window. diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 304d47b0c0b..a45c6e2c8ae 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -19,6 +19,7 @@ Provides: instance -> label + instance -> deadline_server instance -> subset instance -> attachTo instance -> setMembers @@ -64,6 +65,14 @@ class CollectMayaRender(pyblish.api.ContextPlugin): def process(self, context): """Entry point to collector.""" render_instance = None + + # load deadline servers from preset + deadline_servers = [] + create_plugin_preset = context.data["presets"]["plugins"]["maya"].get("create") # noqa: E501 + if create_plugin_preset.get("CreateRender"): + deadline_servers = create_plugin_preset["CreateRender"].get( + "deadline_servers") + for instance in context: if "rendering" in instance.data["families"]: render_instance = instance @@ -80,6 +89,16 @@ def process(self, context): ) return + if deadline_servers: + + deadline_url = deadline_servers[ + deadline_servers.keys()[ + int(render_instance.data.get("deadlineServers")) + ] + ] + else: + deadline_url = os.getenv("DEADLINE_REST_URL", "") + render_globals = render_instance collected_render_layers = render_instance.data["setMembers"] filepath = context.data["currentFile"].replace("\\", "/") @@ -262,6 +281,8 @@ def process(self, context): "useReferencedAovs") or render_instance.data.get( "vrayUseReferencedAovs") or False # noqa: E501 } + if deadline_url: + data["deadlineUrl"] = deadline_url if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index c4f7578cfd8..6d30554890b 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -273,6 +273,8 @@ def process(self, instance): self.payload_skeleton = copy.deepcopy(payload_skeleton_template) self._deadline_url = os.environ.get( "DEADLINE_REST_URL", "http://localhost:8082") + if instance.data.get("deadlineUrl"): + self._deadline_url = instance.data.get("deadlineUrl") assert self._deadline_url, "Requires DEADLINE_REST_URL" context = instance.context diff --git a/pype/plugins/maya/publish/validate_deadline_connection.py b/pype/plugins/maya/publish/validate_deadline_connection.py index f9c11620bae..c775fbf0860 100644 --- a/pype/plugins/maya/publish/validate_deadline_connection.py +++ b/pype/plugins/maya/publish/validate_deadline_connection.py @@ -5,7 +5,7 @@ import os -class ValidateDeadlineConnection(pyblish.api.ContextPlugin): +class ValidateDeadlineConnection(pyblish.api.InstancePlugin): """Validate Deadline Web Service is running""" label = "Validate Deadline Web Service" @@ -15,17 +15,16 @@ class ValidateDeadlineConnection(pyblish.api.ContextPlugin): if not os.environ.get("DEADLINE_REST_URL"): active = False - def process(self, context): + def process(self, instance): - # Workaround bug pyblish-base#250 - if not contextplugin_should_run(self, context): - return - - try: - DEADLINE_REST_URL = os.environ["DEADLINE_REST_URL"] - except KeyError: - self.log.error("Deadline REST API url not found.") - raise ValueError("Deadline REST API url not found.") + if instance.data.get("deadlineUrl"): + DEADLINE_REST_URL = instance.data.get("deadlineUrl") + else: + try: + DEADLINE_REST_URL = os.environ["DEADLINE_REST_URL"] + except KeyError: + self.log.error("Deadline REST API url not found.") + raise ValueError("Deadline REST API url not found.") # Check response response = self._requests_get(DEADLINE_REST_URL) From 86a2857a961d268180df747a155965ed198e1908 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 6 May 2021 18:54:41 +0200 Subject: [PATCH 240/264] remove unused import --- pype/plugins/maya/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 0d0fd36efb2..755f1cb0b2b 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -4,7 +4,6 @@ import json import appdirs import requests -from collections import OrderedDict import six From d20722a8b257c56f10a5dd434993e50f44ebaee2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 6 May 2021 19:33:25 +0100 Subject: [PATCH 241/264] Use SubsetLoader and multiple contexts for delete_old_versions - Only appears once in the right click menu. - Size user feedback is only displayed once at the end of the operation. I have a progress indicator printed atm, but would like to have a progress bar, but I'm shit at Qt stuff. Tried showing a progress bar but could not get it to show before the operation was completed. --- .../global/load/delete_old_versions.py | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/pype/plugins/global/load/delete_old_versions.py b/pype/plugins/global/load/delete_old_versions.py index b00d2474259..b5044b6bac4 100644 --- a/pype/plugins/global/load/delete_old_versions.py +++ b/pype/plugins/global/load/delete_old_versions.py @@ -14,7 +14,10 @@ from pype.api import Anatomy -class DeleteOldVersions(api.Loader): +class DeleteOldVersions(api.SubsetLoader): + + is_multiple_contexts_compatible = True + sequence_splitter = "__sequence_splitter__" representations = ["*"] families = ["*"] @@ -258,9 +261,11 @@ def sort_func(ent): ) if not version_ids: - msg = "Skipping processing. Nothing to delete." + msg = "Skipping processing. Nothing to delete on {}/{}".format( + asset["name"], subset["name"] + ) self.log.info(msg) - self.message(msg) + print(msg) return repres = list(self.dbcon.find({ @@ -396,25 +401,30 @@ def main(self, data, remove_publish_folder): self.log.error(msg) self.message(msg) - msg = "Total size of files: " + self.sizeof_fmt(size) - self.log.info(msg) - self.message(msg) + return size - def load(self, context, name=None, namespace=None, options=None): + def load(self, contexts, name=None, namespace=None, options=None): try: - versions_to_keep = 2 - remove_publish_folder = False - if options: - versions_to_keep = options.get( - "versions_to_keep", versions_to_keep - ) - remove_publish_folder = options.get( - "remove_publish_folder", remove_publish_folder - ) + size = 0 + for count, context in enumerate(contexts): + versions_to_keep = 2 + remove_publish_folder = False + if options: + versions_to_keep = options.get( + "versions_to_keep", versions_to_keep + ) + remove_publish_folder = options.get( + "remove_publish_folder", remove_publish_folder + ) + + data = self.get_data(context, versions_to_keep) - data = self.get_data(context, versions_to_keep) + size += self.main(data, remove_publish_folder) + print("Progressing {}/{}".format(count + 1, len(contexts))) - self.main(data, remove_publish_folder) + msg = "Total size of files: " + self.sizeof_fmt(size) + self.log.info(msg) + self.message(msg) except Exception: self.log.error("Failed to delete versions.", exc_info=True) @@ -436,6 +446,9 @@ class CalculateOldVersions(DeleteOldVersions): def main(self, data, remove_publish_folder): size = 0 + if not data: + return size + if remove_publish_folder: size = self.delete_whole_dir_paths( data["dir_paths"].values(), delete=False @@ -445,6 +458,4 @@ def main(self, data, remove_publish_folder): data["dir_paths"], data["file_paths_by_dir"], delete=False ) - msg = "Total size of files: " + self.sizeof_fmt(size) - self.log.info(msg) - self.message(msg) + return size From eff98f0612ecab149920dd769ea81dc7dec9581d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 May 2021 09:45:53 +0100 Subject: [PATCH 242/264] Use instance frame start instead of timeline. --- pype/plugins/maya/publish/extract_thumbnail.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/publish/extract_thumbnail.py b/pype/plugins/maya/publish/extract_thumbnail.py index 524fc1e17c8..33604d988e3 100644 --- a/pype/plugins/maya/publish/extract_thumbnail.py +++ b/pype/plugins/maya/publish/extract_thumbnail.py @@ -26,10 +26,6 @@ class ExtractThumbnail(pype.api.Extractor): def process(self, instance): self.log.info("Extracting capture..") - start = cmds.currentTime(query=True) - end = cmds.currentTime(query=True) - self.log.info("start: {}, end: {}".format(start, end)) - camera = instance.data['review_camera'] capture_preset = "" @@ -47,8 +43,8 @@ def process(self, instance): # preset['compression'] = "qt" preset['quality'] = 50 preset['compression'] = "jpg" - preset['start_frame'] = start - preset['end_frame'] = end + preset['start_frame'] = instance.data["frameStart"] + preset['end_frame'] = instance.data["frameStart"] preset['camera_options'] = { "displayGateMask": False, "displayResolution": False, From b967c226a482c4ae0eaf8502908c12c79ea10eed Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 May 2021 11:36:35 +0100 Subject: [PATCH 243/264] Increment workfile version on successfull publish. --- .../publish/increment_workfile_version.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pype/plugins/tvpaint/publish/increment_workfile_version.py diff --git a/pype/plugins/tvpaint/publish/increment_workfile_version.py b/pype/plugins/tvpaint/publish/increment_workfile_version.py new file mode 100644 index 00000000000..e34ba733511 --- /dev/null +++ b/pype/plugins/tvpaint/publish/increment_workfile_version.py @@ -0,0 +1,22 @@ +import pyblish.api + +from avalon.tvpaint import workio + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 0.9 + label = "Increment Workfile Version" + optional = True + hosts = ["tvpaint"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not succesfull so version is not increased.") + + from pype.lib import version_up + path = context.data["currentFile"] + workio.save_file(version_up(path)) + self.log.info('Incrementing workfile version') From defc20bfd30271012bd8f4ffa2ff4c9407c1783d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 7 May 2021 15:48:12 +0200 Subject: [PATCH 244/264] fix default deadline url --- pype/plugins/maya/publish/collect_render.py | 2 ++ pype/plugins/maya/publish/validate_deadline_connection.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index a45c6e2c8ae..d4aebdf8b5a 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -96,6 +96,8 @@ def process(self, context): int(render_instance.data.get("deadlineServers")) ] ] + if deadline_url == "DEADLINE_REST_URL": + deadline_url = os.getenv("DEADLINE_REST_URL", "") else: deadline_url = os.getenv("DEADLINE_REST_URL", "") diff --git a/pype/plugins/maya/publish/validate_deadline_connection.py b/pype/plugins/maya/publish/validate_deadline_connection.py index c775fbf0860..792c625f2e7 100644 --- a/pype/plugins/maya/publish/validate_deadline_connection.py +++ b/pype/plugins/maya/publish/validate_deadline_connection.py @@ -19,6 +19,9 @@ def process(self, instance): if instance.data.get("deadlineUrl"): DEADLINE_REST_URL = instance.data.get("deadlineUrl") + self.log.info( + "We have deadline URL on instance {}".format( + DEADLINE_REST_URL)) else: try: DEADLINE_REST_URL = os.environ["DEADLINE_REST_URL"] From 698ca59a448dd15c158e8cb081cacfc7c8d9ad17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 7 May 2021 17:25:13 +0200 Subject: [PATCH 245/264] nuke: space in node name breaking process --- pype/hosts/nuke/lib.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 864814eff71..cdeb414b2da 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -313,14 +313,11 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): prev_node = None with GN: - connections = list() if input: # if connected input node was defined - connections.append({ - "node": input, - "inputName": input.name()}) + input_name = str(input.name()).replace(" ", "") prev_node = nuke.createNode( - "Input", "name {}".format(input.name())) + "Input", "name {}".format(input_name)) prev_node.hideControlPanel() else: @@ -357,18 +354,12 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): input_node = nuke.createNode( "Input", "name {}".format(node_name)) input_node.hideControlPanel() - connections.append({ - "node": nuke.toNode(node_name), - "inputName": node_name}) now_node.setInput(1, input_node) elif isinstance(set_output_to, str): input_node = nuke.createNode( "Input", "name {}".format(node_name)) input_node.hideControlPanel() - connections.append({ - "node": nuke.toNode(set_output_to), - "inputName": set_output_to}) now_node.setInput(0, input_node) else: From 8b4b770b3296874f17225d2298810d3735c2ae3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 17:17:21 +0200 Subject: [PATCH 246/264] ignore case sensitivity on user input text replacement --- pype/tools/standalonepublish/widgets/widget_family.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 077371030e8..93d1e4b1bbb 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -254,9 +254,9 @@ def _on_data_changed(self): defaults = list(plugin.defaults) # Replace - compare_regex = re.compile( - subset_name.replace(user_input_text, "(.+)") - ) + compare_regex = re.compile(re.sub( + user_input_text, "(.+)", subset_name, flags=re.IGNORECASE + )) subset_hints = set() if user_input_text: for _name in existing_subset_names: From fbb9aa2a051efeaaac8102f5df0356ea5a274214 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 May 2021 14:37:24 +0200 Subject: [PATCH 247/264] nuke: fixing start_at with option gui --- pype/plugins/nuke/load/load_mov.py | 142 +++++++-------- pype/plugins/nuke/load/load_sequence.py | 226 ++++++++++-------------- 2 files changed, 157 insertions(+), 211 deletions(-) diff --git a/pype/plugins/nuke/load/load_mov.py b/pype/plugins/nuke/load/load_mov.py index 429b63cba01..e8d5d3c1369 100644 --- a/pype/plugins/nuke/load/load_mov.py +++ b/pype/plugins/nuke/load/load_mov.py @@ -1,47 +1,12 @@ import re import nuke -import contextlib +from avalon.vendor import qargparse from avalon import api, io from pype.hosts.nuke import presets from pype.api import config -@contextlib.contextmanager -def preserve_trim(node): - """Preserve the relative trim of the Loader tool. - - This tries to preserve the loader's trim (trim in and trim out) after - the context by reapplying the "amount" it trims on the clip's length at - start and end. - - """ - # working script frame range - script_start = nuke.root()["first_frame"].value() - - start_at_frame = None - offset_frame = None - if node['frame_mode'].value() == "start at": - start_at_frame = node['frame'].value() - if node['frame_mode'].value() == "offset": - offset_frame = node['frame'].value() - - try: - yield - finally: - if start_at_frame: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(script_start)) - print("start frame of Read was set to" - "{}".format(script_start)) - - if offset_frame: - node['frame_mode'].setValue("offset") - node['frame'].setValue(str((script_start + offset_frame))) - print("start frame of Read was set to" - "{}".format(script_start)) - - def add_review_presets_config(): returning = { "families": list(), @@ -73,49 +38,59 @@ class LoadMov(api.Loader): "mov", "preview", "review", - "mp4"] + presets["representations"] + "mp4", + "h264"] + presets["representations"] label = "Load mov" order = -10 icon = "code-fork" color = "orange" + defaults = { + "start_at_workfile": True + } + + options = [ + qargparse.Boolean( + "start_at_workfile", + help="Load at workfile start frame", + default=True + ) + ] + # presets name_expression = "{class_name}_{ext}" - def loader_shift(self, node, frame, relative=True): - """Shift global in time by i preserving duration - - This moves the loader by i frames preserving global duration. When relative - is False it will shift the global in to the start frame. + def loader_shift(self, read_node, frame, workfile_start=True): + """ Set start frame of read read_node to a workfile start Args: - loader (tool): The fusion loader tool. - frame (int): The amount of frames to move. - relative (bool): When True the shift is relative, else the shift will - change the global in to frame. - - Returns: - int: The resulting relative frame change (how much it moved) + read_node (nuke.Node): The nuke's read node + frame (int): start frame number + workfile_start (bool): set workfile start frame if true """ # working script frame range script_start = nuke.root()["first_frame"].value() - if relative: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(script_start)) + if workfile_start: + read_node['frame_mode'].setValue("start at") + read_node['frame'].setValue(str(script_start)) else: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(frame)) + read_node['frame_mode'].setValue("start at") + read_node['frame'].setValue(str(frame)) return int(script_start) - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): from avalon.nuke import ( containerise, viewer_update_and_undo_stop ) + + start_at_workfile = options.get( + "start_at_workfile", self.defaults["start_at_workfile"]) + version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] @@ -139,8 +114,6 @@ def load(self, context, name, namespace, data): context["representation"]["_id"] # create handles offset (only to last, because of mov) last += handle_start + handle_end - # offset should be with handles so it match orig frame range - offset_frame = orig_first - handle_start # Fallback to asset name when namespace is None if namespace is None: @@ -173,7 +146,7 @@ def load(self, context, name, namespace, data): with viewer_update_and_undo_stop(): read_node["file"].setValue(file) - self.loader_shift(read_node, first) + self.loader_shift(read_node, orig_first, start_at_workfile) read_node["origfirst"].setValue(first) read_node["first"].setValue(first) read_node["origlast"].setValue(last) @@ -240,9 +213,9 @@ def update(self, container, representation): update_container ) - node = nuke.toNode(container['objectName']) + read_node = nuke.toNode(container['objectName']) - assert node.Class() == "Read", "Must be Read" + assert read_node.Class() == "Read", "Must be Read" file = api.get_representation_path(representation) @@ -283,10 +256,10 @@ def update(self, container, representation): colorspace = version_data.get("colorspace") if first is None: - self.log.warning("Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format( - node['name'].value(), representation)) + self.log.warning( + "Missing start frame for updated version" + "assuming starts at frame 0 for: " + "{} ({})".format(read_node['name'].value(), representation)) first = 0 # fix handle start and end if none are available @@ -296,23 +269,26 @@ def update(self, container, representation): # create handles offset (only to last, because of mov) last += handle_start + handle_end - # offset should be with handles so it match orig frame range - offset_frame = orig_first - handle_start # Update the loader's path whilst preserving some values - with preserve_trim(node): - node["file"].setValue(file) - self.log.info("__ node['file']: {}".format(node["file"].value())) + + read_node["file"].setValue(file) + self.log.info( + "__ read_node['file']: {}".format(read_node["file"].value())) # Set the global in to the start frame of the sequence - self.loader_shift(node, first, relative=True) - node["origfirst"].setValue(first) - node["first"].setValue(first) - node["origlast"].setValue(last) - node["last"].setValue(last) + self.loader_shift( + read_node, orig_first, + bool(int( + nuke.root()["first_frame"].value()) == int( + read_node['frame'].value()))) + read_node["origfirst"].setValue(first) + read_node["first"].setValue(first) + read_node["origlast"].setValue(last) + read_node["last"].setValue(last) if colorspace: - node["colorspace"].setValue(str(colorspace)) + read_node["colorspace"].setValue(str(colorspace)) # load nuke presets for Read's colorspace read_clrs_presets = presets.get_colorspace_preset().get( @@ -324,7 +300,7 @@ def update(self, container, representation): if bool(re.search(k, file))), None) if preset_clrsp is not None: - node["colorspace"].setValue(str(preset_clrsp)) + read_node["colorspace"].setValue(str(preset_clrsp)) updated_dict = {} updated_dict.update({ @@ -341,15 +317,15 @@ def update(self, container, representation): "outputDir": version_data.get("outputDir") }) - # change color of node + # change color of read_node if version.get("name") not in [max_version]: - node["tile_color"].setValue(int("0xd84f20ff", 16)) + read_node["tile_color"].setValue(int("0xd84f20ff", 16)) else: - node["tile_color"].setValue(int("0x4ecd25ff", 16)) + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) # Update the imprinted representation update_container( - node, updated_dict + read_node, updated_dict ) self.log.info("udated to version: {}".format(version.get("name"))) @@ -357,8 +333,8 @@ def remove(self, container): from avalon.nuke import viewer_update_and_undo_stop - node = nuke.toNode(container['objectName']) - assert node.Class() == "Read", "Must be Read" + read_node = nuke.toNode(container['objectName']) + assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): - nuke.delete(node) + nuke.delete(read_node) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 4428dc54158..429614d3022 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -1,74 +1,12 @@ import os import re import nuke -import contextlib +from avalon.vendor import qargparse from avalon import api, io from pype.hosts.nuke import presets -@contextlib.contextmanager -def preserve_trim(node): - """Preserve the relative trim of the Loader tool. - - This tries to preserve the loader's trim (trim in and trim out) after - the context by reapplying the "amount" it trims on the clip's length at - start and end. - - """ - # working script frame range - script_start = nuke.root()["first_frame"].value() - - start_at_frame = None - offset_frame = None - if node['frame_mode'].value() == "start at": - start_at_frame = node['frame'].value() - if node['frame_mode'].value() == "offset": - offset_frame = node['frame'].value() - - try: - yield - finally: - if start_at_frame: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(script_start)) - print("start frame of Read was set to" - "{}".format(script_start)) - - if offset_frame: - node['frame_mode'].setValue("offset") - node['frame'].setValue(str((script_start + offset_frame))) - print("start frame of Read was set to" - "{}".format(script_start)) - - -def loader_shift(node, frame, relative=False): - """Shift global in time by i preserving duration - - This moves the loader by i frames preserving global duration. When relative - is False it will shift the global in to the start frame. - - Args: - loader (tool): The fusion loader tool. - frame (int): The amount of frames to move. - relative (bool): When True the shift is relative, else the shift will - change the global in to frame. - - Returns: - int: The resulting relative frame change (how much it moved) - - """ - # working script frame range - script_start = nuke.root()["first_frame"].value() - - if relative: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(script_start)) - else: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(frame)) - - class LoadSequence(api.Loader): """Load image sequence into Nuke""" @@ -80,6 +18,18 @@ class LoadSequence(api.Loader): icon = "file-video-o" color = "white" + defaults = { + "start_at_workfile": True + } + + options = [ + qargparse.Boolean( + "start_at_workfile", + help="Load at workfile start frame", + default=True + ) + ] + # presets name_expression = "{class_name}_{ext}" @@ -100,11 +50,13 @@ def fix_hashes_in_path(file, repr_cont): file = os.path.join(dirname, new_basename).replace("\\", "/") return file - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): from avalon.nuke import ( containerise, viewer_update_and_undo_stop ) + start_at_workfile = options.get( + "start_at_workfile", self.defaults["start_at_workfile"]) version = context['version'] version_data = version.get("data", {}) @@ -151,18 +103,18 @@ def load(self, context, name, namespace, data): read_name = self.name_expression.format(**name_data) - r = nuke.createNode( + read_node = nuke.createNode( "Read", "name {}".format(read_name)) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): - r["file"].setValue(file) + read_node["file"].setValue(file) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace") if colorspace: - r["colorspace"].setValue(str(colorspace)) + read_node["colorspace"].setValue(str(colorspace)) # load nuke presets for Read's colorspace read_clrs_presets = presets.get_colorspace_preset().get( @@ -174,13 +126,13 @@ def load(self, context, name, namespace, data): if bool(re.search(k, file))), None) if preset_clrsp is not None: - r["colorspace"].setValue(str(preset_clrsp)) + read_node["colorspace"].setValue(str(preset_clrsp)) - loader_shift(r, first, relative=True) - r["origfirst"].setValue(int(first)) - r["first"].setValue(int(first)) - r["origlast"].setValue(int(last)) - r["last"].setValue(int(last)) + loader_shift(read_node, start_at_workfile) + read_node["origfirst"].setValue(int(first)) + read_node["first"].setValue(int(first)) + read_node["origlast"].setValue(int(last)) + read_node["last"].setValue(int(last)) # add additional metadata from the version to imprint Avalon knob add_keys = ["frameStart", "frameEnd", @@ -197,53 +149,22 @@ def load(self, context, name, namespace, data): data_imprint.update({"objectName": read_name}) - r["tile_color"].setValue(int("0x4ecd25ff", 16)) + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) if version_data.get("retime", None): speed = version_data.get("speed", 1) time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(r, speed, time_warp_nodes) + self.make_retimes(read_node, speed, time_warp_nodes) - return containerise(r, + return containerise(read_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) - def make_retimes(self, node, speed, time_warp_nodes): - ''' Create all retime and timewarping nodes with coppied animation ''' - if speed != 1: - rtn = nuke.createNode( - "Retime", - "speed {}".format(speed)) - rtn["before"].setValue("continue") - rtn["after"].setValue("continue") - rtn["input.first_lock"].setValue(True) - rtn["input.first"].setValue( - self.handle_start + self.first_frame - ) - - if time_warp_nodes != []: - for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) - if isinstance(timewarp["lookup"], list): - # if array for animation - twn["lookup"].setAnimated() - for i, value in enumerate(timewarp["lookup"]): - twn["lookup"].setValueAt( - (self.first_frame + i) + value, - (self.first_frame + i)) - else: - # if static value `int` - twn["lookup"].setValue(timewarp["lookup"]) - - def switch(self, container, representation): - self.update(container, representation) - def update(self, container, representation): - """Update the Loader's path + """ Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its @@ -255,9 +176,9 @@ def update(self, container, representation): update_container ) - node = nuke.toNode(container['objectName']) + read_node = nuke.toNode(container['objectName']) - assert node.Class() == "Read", "Must be Read" + assert read_node.Class() == "Read", "Must be Read" repr_cont = representation["context"] @@ -298,23 +219,25 @@ def update(self, container, representation): self.log.warning( "Missing start frame for updated version" "assuming starts at frame 0 for: " - "{} ({})".format(node['name'].value(), representation)) + "{} ({})".format(read_node['name'].value(), representation)) first = 0 first -= self.handle_start last += self.handle_end - # Update the loader's path whilst preserving some values - with preserve_trim(node): - node["file"].setValue(file) - self.log.info("__ node['file']: {}".format(node["file"].value())) + read_node["file"].setValue(file) + self.log.info( + "__ read_node['file']: {}".format(read_node["file"].value())) # Set the global in to the start frame of the sequence - loader_shift(node, first, relative=True) - node["origfirst"].setValue(int(first)) - node["first"].setValue(int(first)) - node["origlast"].setValue(int(last)) - node["last"].setValue(int(last)) + + loader_shift( + read_node, + bool("start at" in read_node['frame_mode'].value())) + read_node["origfirst"].setValue(int(first)) + read_node["first"].setValue(int(first)) + read_node["origlast"].setValue(int(last)) + read_node["last"].setValue(int(last)) updated_dict = {} updated_dict.update({ @@ -331,20 +254,20 @@ def update(self, container, representation): "outputDir": version_data.get("outputDir"), }) - # change color of node + # change color of read_node if version.get("name") not in [max_version]: - node["tile_color"].setValue(int("0xd84f20ff", 16)) + read_node["tile_color"].setValue(int("0xd84f20ff", 16)) else: - node["tile_color"].setValue(int("0x4ecd25ff", 16)) + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) if version_data.get("retime", None): speed = version_data.get("speed", 1) time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(node, speed, time_warp_nodes) + self.make_retimes(read_node, speed, time_warp_nodes) # Update the imprinted representation update_container( - node, + read_node, updated_dict ) self.log.info("udated to version: {}".format(version.get("name"))) @@ -353,8 +276,55 @@ def remove(self, container): from avalon.nuke import viewer_update_and_undo_stop - node = nuke.toNode(container['objectName']) - assert node.Class() == "Read", "Must be Read" + read_node = nuke.toNode(container['objectName']) + assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): - nuke.delete(node) + nuke.delete(read_node) + + def switch(self, container, representation): + self.update(container, representation) + + def make_retimes(self, speed, time_warp_nodes): + ''' Create all retime and timewarping nodes with coppied animation ''' + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.handle_start + self.first_frame + ) + + if time_warp_nodes != []: + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (self.first_frame + i) + value, + (self.first_frame + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) + + +def loader_shift(read_node, workfile_start=False): + """ Set start frame of read node to a workfile start + + Args: + read_node (nuke.Node): The nuke's read node + workfile_start (bool): set workfile start frame if true + + """ + # working script frame range + script_start = nuke.root()["first_frame"].value() + + if workfile_start: + read_node['frame_mode'].setValue("start at") + read_node['frame'].setValue(str(script_start)) From da474e4ed0b6ad71459e69b7109744da514d6486 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 14 May 2021 13:07:33 +0200 Subject: [PATCH 248/264] make sure we are grabbing only playblasted files --- .../plugins/maya/publish/extract_playblast.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index d14cd580789..a00d5946c71 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -102,13 +102,22 @@ def process(self, instance): path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) - self.log.info("file list {}".format(playblast)) + self.log.debug("playblast path {}".format(playblast)) - collected_frames = os.listdir(stagingdir) - collections, remainder = clique.assemble(collected_frames) + collected_files = os.listdir(stagingdir) + collections, remainder = clique.assemble(collected_files) input_path = os.path.join( stagingdir, collections[0].format('{head}{padding}{tail}')) - self.log.info("input {}".format(input_path)) + + self.log.debug("filename {}".format(filename)) + frame_collection = None + for collection in collections: + filebase = collection.format('{head}').rstrip(".") + self.log.debug("collection head {}".format(filebase)) + if filebase in filename: + frame_collection = collection + self.log.info("we found collection of interest {}".format(str(frame_collection))) + if "representations" not in instance.data: instance.data["representations"] = [] @@ -123,7 +132,7 @@ def process(self, instance): representation = { 'name': 'png', 'ext': 'png', - 'files': collected_frames, + 'files': list(frame_collection), "stagingDir": stagingdir, "frameStart": start, "frameEnd": end, From debf5b417b66e7018397d1457377b49d506e388e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 14 May 2021 13:07:53 +0200 Subject: [PATCH 249/264] cast representations log into string to prevent false output --- pype/plugins/global/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index a112150db8b..af4d4241c24 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -58,7 +58,7 @@ class ExtractReview(pyblish.api.InstancePlugin): to_height = 1080 def process(self, instance): - self.log.debug(instance.data["representations"]) + self.log.debug(str(instance.data["representations"])) # Skip review when requested. if not instance.data.get("review", True): return From 5e507ae9d4807d00660559e95b2b08e9dd06ef54 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 14 May 2021 13:30:03 +0200 Subject: [PATCH 250/264] remove obsolte _fix_playblast_output_path function --- .../plugins/maya/publish/extract_playblast.py | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index a00d5946c71..16251e74250 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -100,9 +100,7 @@ def process(self, instance): preset.pop("panel", None) path = capture.capture(**preset) - playblast = self._fix_playblast_output_path(path) - self.log.debug("playblast path {}".format(playblast)) collected_files = os.listdir(stagingdir) collections, remainder = clique.assemble(collected_files) @@ -143,42 +141,6 @@ def process(self, instance): } instance.data["representations"].append(representation) - def _fix_playblast_output_path(self, filepath): - """Workaround a bug in maya.cmds.playblast to return correct filepath. - - When the `viewer` argument is set to False and maya.cmds.playblast - does not automatically open the playblasted file the returned - filepath does not have the file's extension added correctly. - - To workaround this we just glob.glob() for any file extensions and - assume the latest modified file is the correct file and return it. - """ - # Catch cancelled playblast - if filepath is None: - self.log.warning("Playblast did not result in output path. " - "Playblast is probably interrupted.") - return None - - # Fix: playblast not returning correct filename (with extension) - # Lets assume the most recently modified file is the correct one. - if not os.path.exists(filepath): - directory = os.path.dirname(filepath) - filename = os.path.basename(filepath) - # check if the filepath is has frame based filename - # example : capture.####.png - parts = filename.split(".") - if len(parts) == 3: - query = os.path.join(directory, "{}.*.{}".format(parts[0], - parts[-1])) - files = glob.glob(query) - else: - files = glob.glob("{}.*".format(filepath)) - - if not files: - raise RuntimeError("Couldn't find playblast from: " - "{0}".format(filepath)) - filepath = max(files, key=os.path.getmtime) - return filepath From c363adf079ef140b8587f769c4661c644175806c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 18 May 2021 21:46:00 +0200 Subject: [PATCH 251/264] bump version to 2.17.4 --- .github_changelog_generator | 2 +- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++------ pype/version.py | 2 +- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index 60b6b8e8116..b690673d6b0 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -7,4 +7,4 @@ enhancement-label=**Enhancements:** release-branch=2.x/develop issues=False exclude-tags-regex=3.\d.\d.* -future-release=2.17.1 \ No newline at end of file +future-release=2.17.4 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a3930d2d8..3bac944671b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,64 @@ # Changelog +## [2.17.4](https://github.com/pypeclub/openpype/tree/2.17.4) (2021-05-18) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.17.4) + +**Enhancements:** + +- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) +- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) + +**Fixed bugs:** + +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) +- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) +- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) + +## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3) + +**Fixed bugs:** + +- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) + +## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) + +**Enhancements:** + +- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) + ## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30) [Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1) **Enhancements:** +- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) +- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) - TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) - PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) -- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) - Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) +- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) - AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) -- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) **Fixed bugs:** +- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) - Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) - AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) -- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) -- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +**Merged pull requests:** + +- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) ## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20) -[Full Changelog](https://github.com/pypeclub/openpype/compare/3.0.0-beta2...2.17.0) +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0) **Enhancements:** @@ -49,10 +84,8 @@ - Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) - Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) - Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) -- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204) - Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) - Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) -- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178) diff --git a/pype/version.py b/pype/version.py index 19768736314..de06464cf19 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.17.1" +__version__ = "2.17.4" From b805fc3f3c06ed596adc87a4c8c14e8e307a90d9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 18 May 2021 22:07:17 +0200 Subject: [PATCH 252/264] bump version --- .github_changelog_generator | 2 +- CHANGELOG.md | 16 +++++++++------- pype/version.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index b690673d6b0..cb934f38902 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -7,4 +7,4 @@ enhancement-label=**Enhancements:** release-branch=2.x/develop issues=False exclude-tags-regex=3.\d.\d.* -future-release=2.17.4 \ No newline at end of file +future-release=2.18.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bac944671b..09882896f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,21 @@ # Changelog -## [2.17.4](https://github.com/pypeclub/openpype/tree/2.17.4) (2021-05-18) +## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18) -[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.17.4) +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0) **Enhancements:** +- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484)) - TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) - Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) **Fixed bugs:** -- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) +- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) +- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) - Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) -- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06) @@ -37,18 +39,15 @@ **Enhancements:** -- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) - Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) - TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) - PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) - Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) - Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) -- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) **Fixed bugs:** - Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) -- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) - AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) **Merged pull requests:** @@ -63,6 +62,7 @@ **Enhancements:** - Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) +- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221) - Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) - TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) - TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) @@ -84,8 +84,10 @@ - Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) - Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) - Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) +- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204) - Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) - Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) +- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178) diff --git a/pype/version.py b/pype/version.py index de06464cf19..6ebac24f894 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.17.4" +__version__ = "2.18.0" From 2476a12c801d51949f67362e25ef85d8096eef37 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 14:16:44 +0200 Subject: [PATCH 253/264] celaction: app not starting --- pype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 6f6ad9386b5..83a5e3a93aa 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -228,7 +228,7 @@ def process(self, session, **kwargs): project_name = session["AVALON_PROJECT"] asset_name = session["AVALON_ASSET"] task_name = session["AVALON_TASK"] - launch_application( + _ = launch_application( project_name, asset_name, task_name, self.name ) From 4bb4113ac120a0f72938b6b13c8ca2a8d36f4c82 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 16:03:37 +0200 Subject: [PATCH 254/264] Celaction: wrong avalon app --- pype/hosts/celaction/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/celaction/cli.py b/pype/hosts/celaction/cli.py index 42f7a1a3856..7813cec3f47 100644 --- a/pype/hosts/celaction/cli.py +++ b/pype/hosts/celaction/cli.py @@ -75,7 +75,7 @@ def _prepare_publish_environments(): env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR") env["AVALON_HIERARCHY"] = hierarchy env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") - env["AVALON_APP"] = f"hosts.{publish_host}" + env["AVALON_APP"] = publish_host env["AVALON_APP_NAME"] = "celaction_local" env["PYBLISH_HOSTS"] = publish_host From b2e23135deb80d534f01992544244e3f919090f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 16:04:00 +0200 Subject: [PATCH 255/264] Celaction: also label with families --- .../celaction/publish/collect_celaction_instances.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py index d3d1d264c06..cb8de136a2a 100644 --- a/pype/plugins/celaction/publish/collect_celaction_instances.py +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -46,13 +46,14 @@ def process(self, context): subset = family + task.capitalize() # Create instance instance = context.create_instance(subset) + families = [family, "ftrack"] # creating instance data instance.data.update({ - "subset": subset, + "subset": "{} {}".format(subset, families), "label": scene_file, "family": family, - "families": [family, "ftrack"], + "families": families, "representations": list() }) @@ -77,12 +78,13 @@ def process(self, context): instance = context.create_instance(name=subset) # getting instance state instance.data["publish"] = True + families = [family] # add assetEntity data into instance instance.data.update({ - "label": "{} - farm".format(subset), + "label": "{} {}".format(subset, families), "family": family, - "families": [family], + "families": families, "subset": subset }) From c404a7871b171df3b7c305ee50eb20f5470e9435 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 May 2021 17:44:25 +0200 Subject: [PATCH 256/264] Delivery in LibraryLoader - added new Loader --- pype/lib/delivery.py | 312 +++++++++++++++++++++++++++ pype/plugins/global/load/delivery.py | 308 ++++++++++++++++++++++++++ 2 files changed, 620 insertions(+) create mode 100644 pype/lib/delivery.py create mode 100644 pype/plugins/global/load/delivery.py diff --git a/pype/lib/delivery.py b/pype/lib/delivery.py new file mode 100644 index 00000000000..a9561db5727 --- /dev/null +++ b/pype/lib/delivery.py @@ -0,0 +1,312 @@ +"""Functions useful for delivery action or loader""" +import os +from avalon import pipeline +from avalon.vendor import filelink +import shutil +import clique +import collections + + +def sizeof_fmt(num, suffix='B'): + """Returns formatted string with size in appropriate unit""" + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + if abs(num) < 1024.0: + return "%3.1f%s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f%s%s" % (num, 'Yi', suffix) + + +def path_from_represenation(representation, anatomy): + try: + template = representation["data"]["template"] + + except KeyError: + return None + + try: + context = representation["context"] + context["root"] = anatomy.roots + path = pipeline.format_template_with_optional_keys( + context, template + ) + + except KeyError: + # Template references unavailable data + return None + + return os.path.normpath(path) + + +def copy_file(src_path, dst_path): + """Hardlink file if possible(to save space), copy if not""" + if os.path.exists(dst_path): + return + try: + filelink.create( + src_path, + dst_path, + filelink.HARDLINK + ) + except OSError: + shutil.copyfile(src_path, dst_path) + + +def get_format_dict(anatomy, location_path): + """Returns replaced root values from user provider value. + + Args: + anatomy (Anatomy) + location_path (str): user provided value + Returns: + (dict): prepared for formatting of a template + """ + format_dict = {} + if location_path: + location_path = location_path.replace("\\", "/") + root_names = anatomy.root_names_from_templates( + anatomy.templates["delivery"] + ) + if root_names is None: + format_dict["root"] = location_path + else: + format_dict["root"] = {} + for name in root_names: + format_dict["root"][name] = location_path + return format_dict + + +def check_destination_path(repre_id, + anatomy, anatomy_data, + datetime_data, template_name): + """ Try to create destination path based on 'template_name'. + + In the case that path cannot be filled, template contains unmatched + keys, provide error message to filter out repre later. + + Args: + anatomy (Anatomy) + anatomy_data (dict): context to fill anatomy + datetime_data (dict): values with actual date + template_name (str): to pick correct delivery template + Returns: + (collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"} + """ + anatomy_data.update(datetime_data) + anatomy_filled = anatomy.format_all(anatomy_data) + dest_path = anatomy_filled["delivery"][template_name] + report_items = collections.defaultdict(list) + sub_msg = None + if not dest_path.solved: + msg = ( + "Missing keys in Representation's context" + " for anatomy template \"{}\"." + ).format(template_name) + + if dest_path.missing_keys: + keys = ", ".join(dest_path.missing_keys) + sub_msg = ( + "Representation: {}
- Missing keys: \"{}\"
" + ).format(repre_id, keys) + + if dest_path.invalid_types: + items = [] + for key, value in dest_path.invalid_types.items(): + items.append("\"{}\" {}".format(key, str(value))) + + keys = ", ".join(items) + sub_msg = ( + "Representation: {}
" + "- Invalid value DataType: \"{}\"
" + ).format(repre_id, keys) + + report_items[msg].append(sub_msg) + + return report_items + + +def process_single_file( + src_path, repre, anatomy, template_name, anatomy_data, format_dict, + report_items, log +): + """Copy single file to calculated path based on template + + Args: + src_path(str): path of source representation file + _repre (dict): full repre, used only in process_sequence, here only + as to share same signature + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (Logger): for log printing + Returns: + (collections.defaultdict , int) + """ + if not os.path.exists(src_path): + msg = "{} doesn't exist for {}".format(src_path, + repre["_id"]) + report_items["Source file was not found"].append(msg) + return report_items, 0 + + anatomy_filled = anatomy.format(anatomy_data) + if format_dict: + template_result = anatomy_filled["delivery"][template_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][template_name] + + # context.representation could be .psd + delivery_path = delivery_path.replace("..", ".") + delivery_folder = os.path.dirname(delivery_path) + if not os.path.exists(delivery_folder): + os.makedirs(delivery_folder) + + log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) + print("Copying single: {} -> {}".format(src_path, delivery_path)) + copy_file(src_path, delivery_path) + + return report_items, 1 + + +def process_sequence( + src_path, repre, anatomy, template_name, anatomy_data, format_dict, + report_items, log +): + """ For Pype2(mainly - works in 3 too) where representation might not + contain files. + + Uses listing physical files (not 'files' on repre as a)might not be + present, b)might not be reliable for representation and copying them. + + TODO Should be refactored when files are sufficient to drive all + representations. + + Args: + src_path(str): path of source representation file + repre (dict): full representation + anatomy (Anatomy) + template_name (string): user selected delivery template name + anatomy_data (dict): data from repre to fill anatomy with + format_dict (dict): root dictionary with names and values + report_items (collections.defaultdict): to return error messages + log (Logger): for log printing + Returns: + (collections.defaultdict , int) + """ + if not os.path.exists(src_path): + msg = "{} doesn't exist for {}".format(src_path, + repre["_id"]) + report_items["Source file was not found"].append(msg) + return report_items, 0 + + dir_path, file_name = os.path.split(str(src_path)) + + context = repre["context"] + ext = context.get("ext", context.get("representation")) + + if not ext: + msg = "Source extension not found, cannot find collection" + report_items[msg].append(src_path) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 + + ext = "." + ext + # context.representation could be .psd + ext = ext.replace("..", ".") + + src_collections, remainder = clique.assemble(os.listdir(dir_path)) + src_collection = None + for col in src_collections: + if col.tail != ext: + continue + + src_collection = col + break + + if src_collection is None: + msg = "Source collection of files was not found" + report_items[msg].append(src_path) + log.warning("{} <{}>".format(msg, src_path)) + return report_items, 0 + + frame_indicator = "@####@" + + anatomy_data["frame"] = frame_indicator + anatomy_filled = anatomy.format(anatomy_data) + + if format_dict: + template_result = anatomy_filled["delivery"][template_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][template_name] + + delivery_folder = os.path.dirname(delivery_path) + dst_head, dst_tail = delivery_path.split(frame_indicator) + dst_padding = src_collection.padding + dst_collection = clique.Collection( + head=dst_head, + tail=dst_tail, + padding=dst_padding + ) + + if not os.path.exists(delivery_folder): + os.makedirs(delivery_folder) + + src_head = src_collection.head + src_tail = src_collection.tail + uploaded = 0 + for index in src_collection.indexes: + src_padding = src_collection.format("{padding}") % index + src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) + src = os.path.normpath( + os.path.join(dir_path, src_file_name) + ) + + dst_padding = dst_collection.format("{padding}") % index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + log.debug("Copying single: {} -> {}".format(src, dst)) + copy_file(src, dst) + uploaded += 1 + + return report_items, uploaded + + +def report(report_items): + """Returns dict with final status of delivery (succes, fail etc.).""" + items = [] + title = "Delivery report" + for msg, _items in report_items.items(): + if not _items: + continue + + if items: + items.append({"type": "label", "value": "---"}) + + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + if not isinstance(_items, (list, tuple)): + _items = [_items] + __items = [] + for item in _items: + __items.append(str(item)) + + items.append({ + "type": "label", + "value": '

{}

'.format("
".join(__items)) + }) + + if not items: + return { + "success": True, + "message": "Delivery Finished" + } + + return { + "items": items, + "title": title, + "success": False, + "message": "Delivery Finished" + } diff --git a/pype/plugins/global/load/delivery.py b/pype/plugins/global/load/delivery.py new file mode 100644 index 00000000000..4a1a0a1b666 --- /dev/null +++ b/pype/plugins/global/load/delivery.py @@ -0,0 +1,308 @@ +import collections +import os +import copy + +from avalon import api, style +from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from avalon.api import AvalonMongoDB +from pype.api import Anatomy, config +from pype import resources + +from pype.lib.delivery import ( + sizeof_fmt, + path_from_represenation, + get_format_dict, + check_destination_path, + process_single_file, + process_sequence, + report +) + + +class Delivery(api.SubsetLoader): + """Export selected versions to folder structure from Template""" + + is_multiple_contexts_compatible = True + sequence_splitter = "__sequence_splitter__" + + representations = ["*"] + families = ["*"] + tool_names = ["library_loader"] + + label = "Deliver Versions" + order = 35 + icon = "upload" + color = "#d8d8d8" + + def message(self, text): + msgBox = QtWidgets.QMessageBox() + msgBox.setText(text) + msgBox.setStyleSheet(style.load_stylesheet()) + msgBox.setWindowFlags( + msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint + ) + msgBox.exec_() + + def load(self, contexts, name=None, namespace=None, options=None): + try: + dialog = DeliveryOptionsDialog(contexts, self.log) + dialog.exec_() + except Exception: + self.log.error("Failed to deliver versions.", exc_info=True) + + +class DeliveryOptionsDialog(QtWidgets.QDialog): + """Dialog to select template where to deliver selected representations.""" + SIZE_W = 950 + SIZE_H = 350 + + def __init__(self, contexts, log=None, parent=None): + super(DeliveryOptionsDialog, self).__init__(parent=parent) + + self.project = contexts[0]["project"]["name"] + self._representations = None + self.log = log + self.currently_uploaded = 0 + + self.dbcon = AvalonMongoDB() + self.dbcon.Session["AVALON_PROJECT"] = self.project + self.dbcon.install() + + self._set_representations(contexts) + + self.setWindowTitle("OpenPype - Deliver versions") + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint | + QtCore.Qt.WindowMinimizeButtonHint + ) + self.setStyleSheet(style.load_stylesheet()) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + + layout = QtWidgets.QVBoxLayout() + + input_layout = QtWidgets.QFormLayout() + input_layout.setContentsMargins(10, 15, 5, 5) + + dropdown = QtWidgets.QComboBox() + self.templates = self._get_templates(self.project) + for name, _ in self.templates.items(): + dropdown.addItem(name) + + template_label = QtWidgets.QLabel() + template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + + root_line_edit = QtWidgets.QLineEdit() + + repre_checkboxes_layout = QtWidgets.QFormLayout() + repre_checkboxes_layout.setContentsMargins(10, 5, 5, 20) + + self._representation_checkboxes = {} + for repre in self._get_representation_names(): + checkbox = QtWidgets.QCheckBox() + checkbox.setChecked(True) + self._representation_checkboxes[repre] = checkbox + + checkbox.stateChanged.connect(self._update_selected_label) + repre_checkboxes_layout.addRow(repre, checkbox) + + selected_label = QtWidgets.QLabel() + + input_layout.addRow("Selected representations", selected_label) + input_layout.addRow("Delivery template", dropdown) + input_layout.addRow("Template value", template_label) + input_layout.addRow("Root", root_line_edit) + input_layout.addRow("Representations", repre_checkboxes_layout) + + btn_delivery = QtWidgets.QPushButton("Deliver") + btn_delivery.setEnabled(bool(dropdown.currentText())) + + progress_bar = QtWidgets.QProgressBar(self) + progress_bar.setMinimum = 0 + progress_bar.setMaximum = 100 + progress_bar.hide() + + text_area = QtWidgets.QTextEdit() + text_area.setReadOnly(True) + text_area.hide() + text_area.setMinimumHeight(100) + + layout.addLayout(input_layout) + layout.addWidget(btn_delivery) + layout.addWidget(progress_bar) + layout.addWidget(text_area) + + self.setLayout(layout) + + self.selected_label = selected_label + self.template_label = template_label + self.dropdown = dropdown + self.root_line_edit = root_line_edit + self.progress_bar = progress_bar + self.text_area = text_area + self.btn_delivery = btn_delivery + + self.files_selected, self.size_selected = \ + self._get_counts(self._get_selected_repres()) + + self._update_selected_label() + self._update_template_value() + + btn_delivery.clicked.connect(self.deliver) + dropdown.currentIndexChanged.connect(self._update_template_value) + + def deliver(self): + """Main method to loop through all selected representations""" + self.progress_bar.show() + self.btn_delivery.setEnabled(False) + # self.resize(self.width(), self.height() + 50) + + report_items = collections.defaultdict(list) + + selected_repres = self._get_selected_repres() + anatomy = Anatomy(self.project) + datetime_data = config.get_datetime_data() + template_name = self.dropdown.currentText() + format_dict = get_format_dict(anatomy, self.root_line_edit.text()) + for repre in self._representations: + if repre["name"] not in selected_repres: + continue + + repre_path = path_from_represenation(repre, anatomy) + + anatomy_data = copy.deepcopy(repre["context"]) + new_report_items = check_destination_path(str(repre["_id"]), + anatomy, + anatomy_data, + datetime_data, + template_name) + + report_items.update(new_report_items) + if new_report_items: + continue + + args = [ + repre_path, + repre, + anatomy, + template_name, + anatomy_data, + format_dict, + report_items, + self.log + ] + + if repre.get("files"): + for repre_file in repre["files"]: + src_path = anatomy.fill_root(repre_file["path"]) + args[0] = src_path + new_report_items, uploaded = process_single_file(*args) + report_items.update(new_report_items) + self._update_progress(uploaded) + else: # fallback for Pype2 and representations without files + frame = repre['context'].get('frame') + if frame: + repre["context"]["frame"] = len(str(frame)) * "#" + + if not frame: + new_report_items, uploaded = process_single_file(*args) + else: + new_report_items, uploaded = process_sequence(*args) + report_items.update(new_report_items) + self._update_progress(uploaded) + + self.text_area.setText(self._format_report(report(report_items), + report_items)) + self.text_area.show() + + self.resize(self.width(), self.height() + 125) + + def _get_representation_names(self): + """Get set of representation names for checkbox filtering.""" + return set([repre["name"] for repre in self._representations]) + + def _get_templates(self, project_name): + """Adds list of delivery templates from Anatomy to dropdown.""" + anatomy = Anatomy(project_name) + + templates = {} + for key, template in ( + anatomy.templates.get("delivery") or {}).items(): + # Use only keys with `{root}` or `{root[*]}` in value + if isinstance(template, str) and "{root" in template: + templates[key] = template + + return templates + + def _set_representations(self, contexts): + version_ids = [context["version"]["_id"] for context in contexts] + + repres = list(self.dbcon.find({ + "type": "representation", + "parent": {"$in": version_ids} + })) + + self._representations = repres + + def _get_counts(self, selected_repres=None): + """Returns tuple of number of selected files and their size.""" + files_selected = 0 + size_selected = 0 + for repre in self._representations: + if repre["name"] in selected_repres: + for repre_file in repre.get("files", []): + + files_selected += 1 + size_selected += repre_file["size"] + + return files_selected, size_selected + + def _prepare_label(self): + """Provides text with no of selected files and their size.""" + label = "{} files, size {}".format(self.files_selected, + sizeof_fmt(self.size_selected)) + return label + + def _get_selected_repres(self): + """Returns list of representation names filtered from checkboxes.""" + selected_repres = [] + for repre_name, chckbox in self._representation_checkboxes.items(): + if chckbox.isChecked(): + selected_repres.append(repre_name) + + return selected_repres + + def _update_selected_label(self): + """Updates label with list of number of selected files.""" + selected_repres = self._get_selected_repres() + self.files_selected, self.size_selected = \ + self._get_counts(selected_repres) + self.selected_label.setText(self._prepare_label()) + + def _update_template_value(self, _index=None): + """Sets template value to label after selection in dropdown.""" + name = self.dropdown.currentText() + template_value = self.templates.get(name) + if template_value: + self.btn_delivery.setEnabled(True) + self.template_label.setText(str(template_value)) + + def _update_progress(self, uploaded): + """Update progress bar after each repre copied.""" + self.currently_uploaded += uploaded + + ratio = self.currently_uploaded / self.files_selected + self.progress_bar.setValue(ratio * self.progress_bar.maximum()) + + def _format_report(self, result, report_items): + """Format final result and error details as html.""" + txt = "

{}

".format(result["message"]) + for header, data in report_items.items(): + txt += "

{}

".format(header) + for item in data: + txt += "{}
".format(item) + + return txt From f236ff955d3e44ecc3a70d81865ac3edaeacf505 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 May 2021 08:26:37 +0100 Subject: [PATCH 257/264] Maya Hardware support - adds Maya Hardware support to the rendering pipeline. Normally render farms might not support this but because we use the Maya Sequence rendering (https://github.com/tokejepsen/MayaSequence) we can use Maya Hardware to render playblasts on the farm. - Ensures the renderer is queried on all render layers. --- pype/hosts/maya/expected_files.py | 30 +++++++++++++++++++ pype/plugins/maya/create/create_render.py | 6 ++-- pype/plugins/maya/publish/collect_render.py | 17 +++++++++++ .../maya/publish/validate_rendersettings.py | 14 +++++++-- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index bb9c0cff0d2..f38facd4e10 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -76,6 +76,7 @@ "arnold": "Arnold", "renderman": "Renderman", "redshift": "Redshift", + "mayahardware2": "Maya Hardware" } # not sure about the renderman image prefix @@ -85,6 +86,7 @@ "arnold": "defaultRenderGlobals.imageFilePrefix", "renderman": "rmanGlobals.imageFileFormat", "redshift": "defaultRenderGlobals.imageFilePrefix", + "mayahardware2": "defaultRenderGlobals.imageFilePrefix" } @@ -136,6 +138,9 @@ def get(self, renderer, layer): if renderer.lower() == "renderman": return self._get_files(ExpectedFilesRenderman( layer, self._render_instance)) + if renderer.lower() == "mayahardware2": + return self._get_files(ExpectedFilesMayaHardware( + layer, self._render_instance)) raise UnsupportedRendererException( "unsupported {}".format(renderer) @@ -917,6 +922,31 @@ def get_aovs(self): return [] +class ExpectedFilesMayaHardware(AExpectedFiles): + """Expected files for Maya Hardware renderer. + + Attributes: + aiDriverExtension (dict): Arnold AOV driver extension mapping. + Is there a better way? + renderer (str): name of renderer. + + """ + + def __init__(self, layer, render_instance): + """Constructor.""" + super(ExpectedFilesMayaHardware, self).__init__(layer, render_instance) + self.renderer = "mayahardware2" + + def get_aovs(self): + """Get all AOVs. + + See Also: + :func:`AExpectedFiles.get_aovs()` + + """ + return [] + + class AOVError(Exception): """Custom exception for determining AOVs.""" diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 755f1cb0b2b..d4ffe33b7b9 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -70,7 +70,8 @@ class CreateRender(plugin.Creator): 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix' } _image_prefixes = { @@ -78,7 +79,8 @@ class CreateRender(plugin.Creator): 'vray': 'maya///', 'arnold': 'maya///_', 'renderman': 'maya///_', - 'redshift': 'maya///_' + 'redshift': 'maya///_', + 'mayahardware2': 'maya///' } deadline_servers = {} diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index d4aebdf8b5a..aec610d63ef 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -46,6 +46,7 @@ from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup +from maya.app.renderSetup.model.collection import RenderSettingsCollection import pyblish.api @@ -115,6 +116,9 @@ def process(self, context): self.maya_layers = maya_render_layers + # Switch to defaultRenderLayer + self._rs.switchToLayerUsingLegacyName("defaultRenderLayer") + for layer in collected_render_layers: try: if layer.startswith("LAYER_"): @@ -172,6 +176,19 @@ def process(self, context): renderer = cmds.getAttr( "defaultRenderGlobals.currentRenderer" ).lower() + + # Find any overrides to the current renderer on the layer. + collections = maya_render_layers[expected_layer_name].getChildren() + for collection in collections: + if isinstance(collection, RenderSettingsCollection): + for override in collection.getChildren(): + attribute_name = override.attributeName() + if attribute_name != "currentRenderer": + continue + + renderer = override.getAttrValue() + continue + # handle various renderman names if renderer.startswith("renderman"): renderer = "renderman" diff --git a/pype/plugins/maya/publish/validate_rendersettings.py b/pype/plugins/maya/publish/validate_rendersettings.py index 297ead5ebb4..9b568073a7a 100644 --- a/pype/plugins/maya/publish/validate_rendersettings.py +++ b/pype/plugins/maya/publish/validate_rendersettings.py @@ -49,7 +49,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayaHardware2': 'defaultRenderGlobals.imageFilePrefix' } ImagePrefixTokens = { @@ -57,7 +58,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): 'arnold': 'maya///_', 'redshift': 'maya///', 'vray': 'maya///', - 'renderman': '_..' + 'renderman': '_..', + 'mayaHardware2': 'maya///' } # WARNING: There is bug? in renderman, translating token @@ -151,6 +153,14 @@ def get_invalid(cls, instance): cls.log.error("Wrong directory prefix [ {} ]".format( dir_prefix)) + elif renderer == "mayaHardware2": + if re.search(cls.R_AOV_TOKEN, prefix): + invalid = True + cls.log.error( + "Do not use AOV token [ {} ] - Maya Hardware does not " + "support AOVs.".format(prefix) + ) + else: multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") if multipart: From cae0d83e6ea11a67ad0b383b5132a72beceb1e9a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 May 2021 08:31:17 +0100 Subject: [PATCH 258/264] More failsafes prevent errored runs. - Ensure `file_path_base` item are in list before removing. - Ensure there is `data` to process. --- pype/plugins/global/load/delete_old_versions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/load/delete_old_versions.py b/pype/plugins/global/load/delete_old_versions.py index b5044b6bac4..5485d7973f2 100644 --- a/pype/plugins/global/load/delete_old_versions.py +++ b/pype/plugins/global/load/delete_old_versions.py @@ -126,7 +126,8 @@ def delete_only_repre_files(self, dir_paths, file_paths, delete=True): os.remove(file_path) self.log.debug("Removed file: {}".format(file_path)) - remainders.remove(file_path_base) + if file_path_base in remainders: + remainders.remove(file_path_base) continue seq_path_base = os.path.split(seq_path)[1] @@ -419,6 +420,9 @@ def load(self, contexts, name=None, namespace=None, options=None): data = self.get_data(context, versions_to_keep) + if not data: + continue + size += self.main(data, remove_publish_folder) print("Progressing {}/{}".format(count + 1, len(contexts))) @@ -446,9 +450,6 @@ class CalculateOldVersions(DeleteOldVersions): def main(self, data, remove_publish_folder): size = 0 - if not data: - return size - if remove_publish_folder: size = self.delete_whole_dir_paths( data["dir_paths"].values(), delete=False From 8e4065548bf7644e1649f0ee85a3028091adc238 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 May 2021 13:59:59 +0100 Subject: [PATCH 259/264] Fix `validate_render_single_camera` --- pype/plugins/maya/publish/validate_render_single_camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/validate_render_single_camera.py b/pype/plugins/maya/publish/validate_render_single_camera.py index 482cf2fb59b..4e2bd7fd87b 100644 --- a/pype/plugins/maya/publish/validate_render_single_camera.py +++ b/pype/plugins/maya/publish/validate_render_single_camera.py @@ -12,7 +12,8 @@ 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix' } From d6cd87e8c19f06757e0cbf92dafe9e408ad496e0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 21 May 2021 18:19:40 +0200 Subject: [PATCH 260/264] Delivery in LibraryLoader - fixed division by 0 for no files --- pype/plugins/global/load/delivery.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/load/delivery.py b/pype/plugins/global/load/delivery.py index 4a1a0a1b666..2b20d0daf8d 100644 --- a/pype/plugins/global/load/delivery.py +++ b/pype/plugins/global/load/delivery.py @@ -253,10 +253,14 @@ def _get_counts(self, selected_repres=None): size_selected = 0 for repre in self._representations: if repre["name"] in selected_repres: - for repre_file in repre.get("files", []): - + files = repre.get("files", []) + if not files: # for repre without files, cannot divide by 0 files_selected += 1 - size_selected += repre_file["size"] + size_selected += 0 + else: + for repre_file in files: + files_selected += 1 + size_selected += repre_file["size"] return files_selected, size_selected From a61a8ef06a0fff71bf2e6404c5dc98abf26dd582 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 24 May 2021 09:08:17 +0100 Subject: [PATCH 261/264] Fix Maya playblast. - Unrecognized `filepath` variable. - Unused `input_path` variable. - Formatting issues. --- pype/plugins/maya/publish/extract_playblast.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 16251e74250..34a9b5897d3 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -101,12 +101,9 @@ def process(self, instance): path = capture.capture(**preset) - collected_files = os.listdir(stagingdir) collections, remainder = clique.assemble(collected_files) - input_path = os.path.join( - stagingdir, collections[0].format('{head}{padding}{tail}')) - + self.log.debug("filename {}".format(filename)) frame_collection = None for collection in collections: @@ -114,8 +111,11 @@ def process(self, instance): self.log.debug("collection head {}".format(filebase)) if filebase in filename: frame_collection = collection - self.log.info("we found collection of interest {}".format(str(frame_collection))) - + self.log.info( + "We found collection of interest {}".format( + str(frame_collection) + ) + ) if "representations" not in instance.data: instance.data["representations"] = [] @@ -141,8 +141,6 @@ def process(self, instance): } instance.data["representations"].append(representation) - return filepath - @contextlib.contextmanager def maintained_time(): From 0113be8ca8c183d2b77cabb0fe70db805cf7dacb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 May 2021 10:30:19 +0200 Subject: [PATCH 262/264] entities are deleted by link length (higher length earlier removement) --- .../ftrack/actions/action_delete_asset.py | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 7d2dac33207..6be83be60b9 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -533,11 +533,13 @@ def launch(self, session, entities, event): ftrack_proc_txt, ", ".join(ftrack_ids_to_delete) )) - ftrack_ents_to_delete = ( + entities_by_link_len = ( self._filter_entities_to_delete(ftrack_ids_to_delete, session) ) - for entity in ftrack_ents_to_delete: - session.delete(entity) + for link_len in sorted(entities_by_link_len.keys(), reverse=True): + for entity in entities_by_link_len[link_len]: + session.delete(entity) + try: session.commit() except Exception: @@ -596,29 +598,10 @@ def _filter_entities_to_delete(self, ftrack_ids_to_delete, session): joined_ids_to_delete ) ).all() - filtered = to_delete_entities[:] - while True: - changed = False - _filtered = filtered[:] - for entity in filtered: - entity_id = entity["id"] - - for _entity in tuple(_filtered): - if entity_id == _entity["id"]: - continue - - for _link in _entity["link"]: - if entity_id == _link["id"] and _entity in _filtered: - _filtered.remove(_entity) - changed = True - break - - filtered = _filtered - - if not changed: - break - - return filtered + entities_by_link_len = collections.defaultdict(list) + for entity in to_delete_entities: + entities_by_link_len[len(entity["link"])].append(entity) + return entities_by_link_len def report_handle(self, report_messages, project_name, event): if not report_messages: From a357e20537f796329f454f19f6a72c677b274cb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 May 2021 10:30:28 +0200 Subject: [PATCH 263/264] faster asset doc query --- pype/modules/ftrack/actions/action_delete_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 6be83be60b9..24ade8e73a7 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -442,7 +442,14 @@ def launch(self, session, entities, event): if len(assets_to_delete) > 0: map_av_ftrack_id = spec_data["without_ftrack_id"] # Prepare data when deleting whole avalon asset - avalon_assets = self.dbcon.find({"type": "asset"}) + avalon_assets = self.dbcon.find( + {"type": "asset"}, + { + "_id": 1, + "data.visualParent": 1, + "data.ftrackId": 1 + } + ) avalon_assets_by_parent = collections.defaultdict(list) for asset in avalon_assets: asset_id = asset["_id"] From 57750495efd87a23d6ceeaf215b8fe20a4c2589d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 25 May 2021 18:17:40 +0200 Subject: [PATCH 264/264] Delivery in LibraryLoader - fixed sequence issue Only single file was being created and overwritten --- pype/lib/delivery.py | 70 ++++++++----------- pype/plugins/global/load/delivery.py | 100 ++++++++++++++------------- 2 files changed, 83 insertions(+), 87 deletions(-) diff --git a/pype/lib/delivery.py b/pype/lib/delivery.py index a9561db5727..d7f32a4a002 100644 --- a/pype/lib/delivery.py +++ b/pype/lib/delivery.py @@ -7,6 +7,36 @@ import collections +def collect_frames(files): + """ + Returns dict of source path and its frame, if from sequence + + Uses clique as most precise solution + + Args: + files(list): list of source paths + Returns: + (dict): {'/asset/subset_v001.0001.png': '0001', ....} + """ + collections, remainder = clique.assemble(files) + + sources_and_frames = {} + if collections: + for collection in collections: + src_head = collection.head + src_tail = collection.tail + + for index in collection.indexes: + src_frame = collection.format("{padding}") % index + src_file_name = "{}{}{}".format(src_head, src_frame, + src_tail) + sources_and_frames[src_file_name] = src_frame + else: + sources_and_frames[remainder.pop()] = None + + return sources_and_frames + + def sizeof_fmt(num, suffix='B'): """Returns formatted string with size in appropriate unit""" for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: @@ -270,43 +300,3 @@ def process_sequence( uploaded += 1 return report_items, uploaded - - -def report(report_items): - """Returns dict with final status of delivery (succes, fail etc.).""" - items = [] - title = "Delivery report" - for msg, _items in report_items.items(): - if not _items: - continue - - if items: - items.append({"type": "label", "value": "---"}) - - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - if not isinstance(_items, (list, tuple)): - _items = [_items] - __items = [] - for item in _items: - __items.append(str(item)) - - items.append({ - "type": "label", - "value": '

{}

'.format("
".join(__items)) - }) - - if not items: - return { - "success": True, - "message": "Delivery Finished" - } - - return { - "items": items, - "title": title, - "success": False, - "message": "Delivery Finished" - } diff --git a/pype/plugins/global/load/delivery.py b/pype/plugins/global/load/delivery.py index 2b20d0daf8d..e474fe69c9c 100644 --- a/pype/plugins/global/load/delivery.py +++ b/pype/plugins/global/load/delivery.py @@ -1,10 +1,11 @@ -import collections -import os +from collections import defaultdict import copy +from Qt import QtWidgets, QtCore, QtGui + from avalon import api, style -from avalon.vendor.Qt import QtWidgets, QtCore, QtGui from avalon.api import AvalonMongoDB + from pype.api import Anatomy, config from pype import resources @@ -15,7 +16,7 @@ check_destination_path, process_single_file, process_sequence, - report + collect_frames ) @@ -53,19 +54,18 @@ def load(self, contexts, name=None, namespace=None, options=None): class DeliveryOptionsDialog(QtWidgets.QDialog): """Dialog to select template where to deliver selected representations.""" - SIZE_W = 950 - SIZE_H = 350 def __init__(self, contexts, log=None, parent=None): super(DeliveryOptionsDialog, self).__init__(parent=parent) - self.project = contexts[0]["project"]["name"] + project = contexts[0]["project"]["name"] + self.anatomy = Anatomy(project) self._representations = None self.log = log self.currently_uploaded = 0 self.dbcon = AvalonMongoDB() - self.dbcon.Session["AVALON_PROJECT"] = self.project + self.dbcon.Session["AVALON_PROJECT"] = project self.dbcon.install() self._set_representations(contexts) @@ -79,15 +79,9 @@ def __init__(self, contexts, log=None, parent=None): QtCore.Qt.WindowMinimizeButtonHint ) self.setStyleSheet(style.load_stylesheet()) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - - layout = QtWidgets.QVBoxLayout() - - input_layout = QtWidgets.QFormLayout() - input_layout.setContentsMargins(10, 15, 5, 5) dropdown = QtWidgets.QComboBox() - self.templates = self._get_templates(self.project) + self.templates = self._get_templates(self.anatomy) for name, _ in self.templates.items(): dropdown.addItem(name) @@ -98,12 +92,12 @@ def __init__(self, contexts, log=None, parent=None): root_line_edit = QtWidgets.QLineEdit() repre_checkboxes_layout = QtWidgets.QFormLayout() - repre_checkboxes_layout.setContentsMargins(10, 5, 5, 20) + repre_checkboxes_layout.setContentsMargins(10, 5, 5, 10) self._representation_checkboxes = {} for repre in self._get_representation_names(): checkbox = QtWidgets.QCheckBox() - checkbox.setChecked(True) + checkbox.setChecked(False) self._representation_checkboxes[repre] = checkbox checkbox.stateChanged.connect(self._update_selected_label) @@ -111,6 +105,10 @@ def __init__(self, contexts, log=None, parent=None): selected_label = QtWidgets.QLabel() + input_widget = QtWidgets.QWidget(self) + input_layout = QtWidgets.QFormLayout(input_widget) + input_layout.setContentsMargins(10, 15, 5, 5) + input_layout.addRow("Selected representations", selected_label) input_layout.addRow("Delivery template", dropdown) input_layout.addRow("Template value", template_label) @@ -123,20 +121,21 @@ def __init__(self, contexts, log=None, parent=None): progress_bar = QtWidgets.QProgressBar(self) progress_bar.setMinimum = 0 progress_bar.setMaximum = 100 - progress_bar.hide() + progress_bar.setVisible(False) text_area = QtWidgets.QTextEdit() text_area.setReadOnly(True) - text_area.hide() + text_area.setVisible(False) text_area.setMinimumHeight(100) - layout.addLayout(input_layout) + layout = QtWidgets.QVBoxLayout(self) + + layout.addWidget(input_widget) + layout.addStretch(1) layout.addWidget(btn_delivery) layout.addWidget(progress_bar) layout.addWidget(text_area) - self.setLayout(layout) - self.selected_label = selected_label self.template_label = template_label self.dropdown = dropdown @@ -156,26 +155,26 @@ def __init__(self, contexts, log=None, parent=None): def deliver(self): """Main method to loop through all selected representations""" - self.progress_bar.show() + self.progress_bar.setVisible(True) self.btn_delivery.setEnabled(False) - # self.resize(self.width(), self.height() + 50) + QtWidgets.QApplication.processEvents() - report_items = collections.defaultdict(list) + report_items = defaultdict(list) selected_repres = self._get_selected_repres() - anatomy = Anatomy(self.project) + datetime_data = config.get_datetime_data() template_name = self.dropdown.currentText() - format_dict = get_format_dict(anatomy, self.root_line_edit.text()) + format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) for repre in self._representations: if repre["name"] not in selected_repres: continue - repre_path = path_from_represenation(repre, anatomy) + repre_path = path_from_represenation(repre, self.anatomy) anatomy_data = copy.deepcopy(repre["context"]) new_report_items = check_destination_path(str(repre["_id"]), - anatomy, + self.anatomy, anatomy_data, datetime_data, template_name) @@ -187,7 +186,7 @@ def deliver(self): args = [ repre_path, repre, - anatomy, + self.anatomy, template_name, anatomy_data, format_dict, @@ -196,9 +195,16 @@ def deliver(self): ] if repre.get("files"): + src_paths = [] for repre_file in repre["files"]: - src_path = anatomy.fill_root(repre_file["path"]) + src_path = self.anatomy.fill_root(repre_file["path"]) + src_paths.append(src_path) + sources_and_frames = collect_frames(src_paths) + + for src_path, frame in sources_and_frames.items(): args[0] = src_path + if frame: + anatomy_data["frame"] = frame new_report_items, uploaded = process_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) @@ -214,26 +220,21 @@ def deliver(self): report_items.update(new_report_items) self._update_progress(uploaded) - self.text_area.setText(self._format_report(report(report_items), - report_items)) - self.text_area.show() - - self.resize(self.width(), self.height() + 125) + self.text_area.setText(self._format_report(report_items)) + self.text_area.setVisible(True) def _get_representation_names(self): """Get set of representation names for checkbox filtering.""" return set([repre["name"] for repre in self._representations]) - def _get_templates(self, project_name): + def _get_templates(self, anatomy): """Adds list of delivery templates from Anatomy to dropdown.""" - anatomy = Anatomy(project_name) - templates = {} - for key, template in ( - anatomy.templates.get("delivery") or {}).items(): - # Use only keys with `{root}` or `{root[*]}` in value - if isinstance(template, str) and "{root" in template: - templates[key] = template + for template_name, value in anatomy.templates["delivery"].items(): + if not isinstance(value, str) or not value.startswith('{root'): + continue + + templates[template_name] = value return templates @@ -292,7 +293,7 @@ def _update_template_value(self, _index=None): template_value = self.templates.get(name) if template_value: self.btn_delivery.setEnabled(True) - self.template_label.setText(str(template_value)) + self.template_label.setText(template_value) def _update_progress(self, uploaded): """Update progress bar after each repre copied.""" @@ -301,9 +302,14 @@ def _update_progress(self, uploaded): ratio = self.currently_uploaded / self.files_selected self.progress_bar.setValue(ratio * self.progress_bar.maximum()) - def _format_report(self, result, report_items): + def _format_report(self, report_items): """Format final result and error details as html.""" - txt = "

{}

".format(result["message"]) + msg = "Delivery finished" + if not report_items: + msg += " successfully" + else: + msg += " with errors" + txt = "

{}

".format(msg) for header, data in report_items.items(): txt += "

{}

".format(header) for item in data: