From 424e206f099e2e85e6faf4e078a8fbe3b9468497 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Feb 2024 16:53:17 +0100 Subject: [PATCH 001/101] Refactor create_bin function to include a set_as_current parameter for setting the resulting bin as the current folder. Add iter_all_media_pool_clips function to recursively iterate all media pool clips in the current project. Introduce RemoveUnusedMedia inventory action plugin to remove unused media pool items and LoadMedia loader plugin for loading media into Resolve. adaptation from https://github.com/BigRoy/OpenPype/commit/37da9c6f785a645d11a6274fa6ef738450ce967c#diff-3ec58869aa5ff3f62216d417256778fd5b44ea0132b953e67d7880f5e7b53df5 --- client/ayon_core/hosts/resolve/api/lib.py | 71 ++-- .../ayon_core/hosts/resolve/api/pipeline.py | 24 ++ .../remove_unused_media_pool_items.py | 31 ++ .../hosts/resolve/plugins/load/load_media.py | 365 ++++++++++++++++++ 4 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py create mode 100644 client/ayon_core/hosts/resolve/plugins/load/load_media.py diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 6e4e17811f..18d6aa31e5 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -145,7 +145,9 @@ def get_new_timeline(timeline_name: str = None): return new_timeline -def create_bin(name: str, root: object = None) -> object: +def create_bin(name: str, + root: object = None, + set_as_current: bool = True) -> object: """ Create media pool's folder. @@ -156,6 +158,8 @@ def create_bin(name: str, root: object = None) -> object: Args: name (str): name of folder / bin, or hierarchycal name "parent/name" root (resolve.Folder)[optional]: root folder / bin object + set_as_current (resolve.Folder)[optional]: Whether to set the + resulting bin as current folder or not. Returns: object: resolve.Folder @@ -168,22 +172,24 @@ def create_bin(name: str, root: object = None) -> object: if "/" in name.replace("\\", "/"): child_bin = None for bname in name.split("/"): - child_bin = create_bin(bname, child_bin or root_bin) + child_bin = create_bin(bname, + root=child_bin or root_bin, + set_as_current=set_as_current) if child_bin: return child_bin else: - created_bin = None + # Find existing folder or create it for subfolder in root_bin.GetSubFolderList(): - if subfolder.GetName() in name: + if subfolder.GetName() == name: created_bin = subfolder - - if not created_bin: - new_folder = media_pool.AddSubFolder(root_bin, name) - media_pool.SetCurrentFolder(new_folder) + break else: + created_bin = media_pool.AddSubFolder(root_bin, name) + + if set_as_current: media_pool.SetCurrentFolder(created_bin) - return media_pool.GetCurrentFolder() + return created_bin def remove_media_pool_item(media_pool_item: object) -> bool: @@ -272,8 +278,7 @@ def create_timeline_item( # get all variables project = get_current_project() media_pool = project.GetMediaPool() - _clip_property = media_pool_item.GetClipProperty - clip_name = _clip_property("File Name") + clip_name = media_pool_item.GetClipProperty("File Name") timeline = timeline or get_current_timeline() # timing variables @@ -298,16 +303,22 @@ def create_timeline_item( if source_end: clip_data["endFrame"] = source_end if timecode_in: + # Note: specifying a recordFrame will fail to place the timeline + # item if there's already an existing clip at that time on the + # active track. clip_data["recordFrame"] = timeline_in # add to timeline - media_pool.AppendToTimeline([clip_data]) + output_timeline_item = media_pool.AppendToTimeline([clip_data])[0] - output_timeline_item = get_timeline_item( - media_pool_item, timeline) + # Adding the item may fail whilst Resolve will still return a + # TimelineItem instance - however all `Get*` calls return None + # Hence, we check whether the result is valid + if output_timeline_item.GetDuration() is None: + output_timeline_item = None assert output_timeline_item, AssertionError(( - "Clip name '{}' was't created on the timeline: '{}' \n\n" + "Clip name '{}' wasn't created on the timeline: '{}' \n\n" "Please check if correct track position is activated, \n" "or if a clip is not already at the timeline in \n" "position: '{}' out: '{}'. \n\n" @@ -330,19 +341,25 @@ def get_timeline_item(media_pool_item: object, Returns: object: resolve.TimelineItem """ - _clip_property = media_pool_item.GetClipProperty - clip_name = _clip_property("File Name") + clip_name = media_pool_item.GetClipProperty("File Name") output_timeline_item = None timeline = timeline or get_current_timeline() with maintain_current_timeline(timeline): # search the timeline for the added clip - for _ti_data in get_current_timeline_items(): - _ti_clip = _ti_data["clip"]["item"] - _ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty - if clip_name in _ti_clip_property("File Name"): - output_timeline_item = _ti_clip + for ti_data in get_current_timeline_items(): + ti_clip_item = ti_data["clip"]["item"] + ti_media_pool_item = ti_clip_item.GetMediaPoolItem() + + # Skip items that do not have a media pool item, like for example + # an "Adjustment Clip" or a "Fusion Composition" from the effects + # toolbox + if not ti_media_pool_item: + continue + + if clip_name in ti_media_pool_item.GetClipProperty("File Name"): + output_timeline_item = ti_clip_item return output_timeline_item @@ -936,3 +953,13 @@ def get_reformated_path(path, padded=False, first=False): else: path = re.sub(num_pattern, "%d", path) return path + + +def iter_all_media_pool_clips(): + """Recursively iterate all media pool clips in current project""" + root = get_current_project().GetMediaPool().GetRootFolder() + queue = [root] + for folder in queue: + for clip in folder.GetClipList(): + yield clip + queue.extend(folder.GetSubFolderList()) diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index 19d33971dc..78ded41ed9 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -2,6 +2,7 @@ Basic avalon integration """ import os +import json import contextlib from collections import OrderedDict @@ -12,6 +13,7 @@ from ayon_core.pipeline import ( schema, register_loader_plugin_path, register_creator_plugin_path, + register_inventory_action_path, AVALON_CONTAINER_ID, ) from ayon_core.host import ( @@ -38,6 +40,7 @@ PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" @@ -65,6 +68,7 @@ class ResolveHost(HostBase, IWorkfileHost, ILoadHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) # register callback for switching publishable pyblish.register_callback("instanceToggled", @@ -145,6 +149,26 @@ def ls(): and the Maya equivalent, which is in `avalon.maya.pipeline` """ + # Media Pool instances from Load Media loader + for clip in lib.iter_all_media_pool_clips(): + data = clip.GetMetadata(lib.pype_tag_name) + if not data: + continue + data = json.loads(data) + + # If not all required data, skip it + required = ['schema', 'id', 'loader', 'representation'] + if not all(key in data for key in required): + continue + + container = {key: data[key] for key in required} + container["objectName"] = clip.GetName() # Get path in folders + container["namespace"] = clip.GetName() + container["name"] = clip.GetUniqueId() + container["_item"] = clip + yield container + + # Timeline instances from Load Clip loader # get all track items from current timeline all_timeline_items = lib.get_current_timeline_items(filter=False) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py new file mode 100644 index 0000000000..6698508d24 --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py @@ -0,0 +1,31 @@ +from ayon_core.pipeline import ( + InventoryAction, +) +from ayon_core.pipeline.load.utils import remove_container + + +class RemoveUnusedMedia(InventoryAction): + + label = "Remove Unused Media" + icon = "trash" + + @staticmethod + def is_compatible(container): + return ( + container.get("loader") == "LoadMedia" + ) + + def process(self, containers): + any_removed = False + for container in containers: + media_pool_item = container["_item"] + usage = int(media_pool_item.GetClipProperty("Usage")) + name = media_pool_item.GetName() + if usage == 0: + print(f"Removing {name}") + remove_container(container) + any_removed = True + else: + print(f"Keeping {name} with usage: {usage}") + + return any_removed diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py new file mode 100644 index 0000000000..2dda2655db --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -0,0 +1,365 @@ +import json +import copy +from collections import defaultdict +from typing import Union + +from openpype.client import ( + get_version_by_id, + get_last_version_by_subset_id, +) +from openpype.pipeline import ( + LoaderPlugin, + get_representation_context, + get_representation_path, + registered_host +) +from openpype.pipeline.context_tools import get_current_project_name +from openpype.hosts.resolve.api import lib +from openpype.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from openpype.lib.transcoding import ( + VIDEO_EXTENSIONS, + IMAGE_EXTENSIONS +) +from openpype.lib import BoolDef + + +def find_clip_usage(media_pool_item, project=None): + """Return all Timeline Items in the project using the Media Pool Item. + + Each entry in the list is a tuple of Timeline and TimelineItem so that + it's easy to know which Timeline the TimelineItem belongs to. + + Arguments: + media_pool_item (MediaPoolItem): The Media Pool Item to search for. + project (Project): The resolve project the media pool item resides in. + + Returns: + List[Tuple[Timeline, TimelineItem]]: A 2-tuple of a timeline with + the timeline item. + + """ + usage = int(media_pool_item.GetClipProperty("Usage")) + if not usage: + return [] + + if project is None: + project = lib.get_current_project() + + matching_items = [] + unique_id = media_pool_item.GetUniqueId() + for timeline_idx in range(project.GetTimelineCount()): + timeline = project.GetTimelineByIndex(timeline_idx+1) + + # Consider audio and video tracks + for track_type in ["video", "audio"]: + for track_idx in range(timeline.GetTrackCount(track_type)): + timeline_items = timeline.GetItemListInTrack(track_type, + track_idx+1) + for timeline_item in timeline_items: + timeline_item_mpi = timeline_item.GetMediaPoolItem() + if not timeline_item_mpi: + continue + + if timeline_item_mpi.GetUniqueId() == unique_id: + matching_items.append((timeline, timeline_item)) + usage -= 1 + if usage <= 0: + # If there should be no usage left after this found + # entry we return early + return matching_items + + return matching_items + + +class LoadMedia(LoaderPlugin): + """Load a subset as media pool item.""" + + families = ["render2d", "source", "plate", "render", "review"] + + representations = ["*"] + extensions = set( + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + ) + + label = "Load media" + order = -20 + icon = "code-fork" + color = "orange" + + options = [ + BoolDef( + "load_to_timeline", + label="Load to timeline", + default=True, + tooltip="Whether on load to automatically add it to the current " + "timeline" + ), + BoolDef( + "load_once", + label="Re-use existing", + default=True, + tooltip="When enabled - if this particular version is already" + "loaded it will not be loaded again but will be re-used." + ) + ] + + # for loader multiselection + timeline = None + + # presets + clip_color_last = "Olive" + clip_color = "Orange" + + bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" + + def load(self, context, name, namespace, options): + + # For loading multiselection, we store timeline before first load + # because the current timeline can change with the imported media. + if self.timeline is None: + self.timeline = lib.get_current_timeline() + + representation = context["representation"] + + project = lib.get_current_project() + media_pool = project.GetMediaPool() + + # Allow to use an existing media pool item and re-use it + item = None + if options.get("load_once", True): + host = registered_host() + repre_id = str(context["representation"]["_id"]) + for container in host.ls(): + if container["representation"] != repre_id: + continue + + if container["loader"] != self.__class__.__name__: + continue + + print(f"Re-using existing container: {container}") + item = container["_item"] + + if item is None: + # Create or set the bin folder, we add it in there + # If bin path is not set we just add into the current active bin + if self.bin_path: + bin_path = self.bin_path.format(**context) + folder = lib.create_bin( + name=bin_path, + root=media_pool.GetRootFolder(), + set_as_current=False + ) + media_pool.SetCurrentFolder(folder) + + # Import media + path = self._get_filepath(representation) + items = media_pool.ImportMedia([path]) + + assert len(items) == 1, "Must import only one media item" + item = items[0] + + self._set_metadata(media_pool_item=item, + context=context) + + data = self._get_container_data(representation) + + # Add containerise data only needed on first load + data.update({ + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "loader": str(self.__class__.__name__), + }) + + item.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + # Always update clip color - even if re-using existing clip + color = self.get_item_color(representation) + item.SetClipColor(color) + + if options.get("load_to_timeline", True): + timeline = options.get("timeline", self.timeline) + if timeline: + # Add media to active timeline + lib.create_timeline_item( + media_pool_item=item, + timeline=timeline + ) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + # Update MediaPoolItem filepath and metadata + item = container["_item"] + + # Get the existing metadata before we update because the + # metadata gets removed + data = json.loads(item.GetMetadata(lib.pype_tag_name)) + + # Update path + path = self._get_filepath(representation) + item.ReplaceClip(path) + + # Update the metadata + update_data = self._get_container_data(representation) + data.update(update_data) + item.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + context = get_representation_context(representation) + self._set_metadata(media_pool_item=item, context=context) + + # Update the clip color + color = self.get_item_color(representation) + item.SetClipColor(color) + + def remove(self, container): + # Remove MediaPoolItem entry + project = lib.get_current_project() + media_pool = project.GetMediaPool() + item = container["_item"] + + # Delete any usages of the media pool item so there's no trail + # left in existing timelines. Currently only the media pool item + # gets removed which fits the Resolve workflow but is confusing + # artists + usage = find_clip_usage(media_pool_item=item, project=project) + if usage: + # Group all timeline items per timeline, so we can delete the clips + # in the timeline at once. The Resolve objects are not hashable, so + # we need to store them in the dict by id + usage_by_timeline = defaultdict(list) + timeline_by_id = {} + for timeline, timeline_item in usage: + timeline_id = timeline.GetUniqueId() + timeline_by_id[timeline_id] = timeline + usage_by_timeline[timeline.GetUniqueId()].append(timeline_item) + + for timeline_id, timeline_items in usage_by_timeline.items(): + timeline = timeline_by_id[timeline_id] + timeline.DeleteClips(timeline_items) + + # Delete the media pool item + media_pool.DeleteClips([item]) + + def _get_container_data(self, representation): + """Return metadata related to the representation and version.""" + + # load clip to timeline and get main variables + project_name = get_current_project_name() + version = get_version_by_id(project_name, representation["parent"]) + version_data = version.get("data", {}) + version_name = version.get("name", None) + colorspace = version_data.get("colorspace", None) + + # add additional metadata from the version to imprint Avalon knob + add_keys = [ + "frameStart", "frameEnd", "source", "author", + "fps", "handleStart", "handleEnd" + ] + data = { + key: version_data.get(key, str(None)) for key in add_keys + } + + # add variables related to version context + data.update({ + "representation": str(representation["_id"]), + "version": version_name, + "colorspace": colorspace, + }) + + return data + + @classmethod + def get_item_color(cls, representation) -> str: + """Return item color name. + + Coloring depends on whether representation is the latest version. + """ + # Compare version with last version + project_name = get_current_project_name() + version = get_version_by_id( + project_name, + representation["parent"], + fields=["name", "parent"] + ) + last_version = get_last_version_by_subset_id( + project_name, + version["parent"], + fields=["name"] + ) or {} + + # set clip colour + if version.get("name") == last_version.get("name"): + return cls.clip_color_last + else: + return cls.clip_color + + def _set_metadata(self, media_pool_item, context: dict): + """Set Media Pool Item Clip Properties""" + + # Set the timecode for the loaded clip if Resolve doesn't parse it + # correctly from the input. An image sequence will have timecode + # parsed from its frame range, we will want to preserve that. + # TODO: Setting the Start TC breaks existing clips on update + # See: https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197327 + # Once that's resolved we should enable this + # start_tc = media_pool_item.GetClipProperty("Start TC") + # if start_tc == "00:00:00:00": + # from openpype.pipeline.editorial import frames_to_timecode + # # Assume no timecode was detected from the source media + # + # fps = float(media_pool_item.GetClipProperty("FPS")) + # handle_start = context["version"]["data"].get("handleStart", 0) + # frame_start = context["version"]["data"].get("frameStart", 0) + # frame_start_handle = frame_start - handle_start + # timecode = frames_to_timecode(frame_start_handle, fps) + # + # if timecode != start_tc: + # media_pool_item.SetClipProperty("Start TC", timecode) + + # Set more clip metadata based on the loaded clip's context + metadata = { + "Clip Name": "{asset[name]} {subset[name]} " + "v{version[name]:03d} ({representation[name]})", + "Shot": "{asset[name]}", + "Take": "{subset[name]} v{version[name]:03d}", + "Comments": "{version[data][comment]}" + } + for clip_property, value in metadata.items(): + media_pool_item.SetClipProperty(clip_property, + value.format_map(context)) + + def _get_filepath(self, representation: dict) -> Union[str, dict]: + + is_sequence = bool(representation["context"].get("frame")) + if not is_sequence: + return get_representation_path(representation) + + context = get_representation_context(representation) + version = context["version"] + + # Get the start and end frame of the image sequence, incl. handles + frame_start = version["data"].get("frameStart", 0) + frame_end = version["data"].get("frameEnd", 0) + handle_start = version["data"].get("handleStart", 0) + handle_end = version["data"].get("handleEnd", 0) + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end + padding = len(representation["context"].get("frame")) + + # We format the frame number to the required token. To do so + # we in-place change the representation context data to format the path + # with that replaced data + representation = copy.deepcopy(representation) + representation["context"]["frame"] = f"%0{padding}d" + path = get_representation_path(representation) + + # See Resolve API, to import for example clip "file_[001-100].dpx": + # ImportMedia([{"FilePath":"file_%03d.dpx", + # "StartIndex":1, + # "EndIndex":100}]) + return { + "FilePath": path, + "StartIndex": frame_start_handle, + "EndIndex": frame_end_handle + } From 5ee9fca4026e176f80f9aa18394f9d120daf5346 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Feb 2024 17:05:48 +0100 Subject: [PATCH 002/101] Update client and pipeline imports, refactor transcoding library references, and adjust track index handling in find_clip_usage function. --- .../hosts/resolve/plugins/load/load_media.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 2dda2655db..60fbea483e 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -3,24 +3,24 @@ import copy from collections import defaultdict from typing import Union -from openpype.client import ( +from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) -from openpype.pipeline import ( +from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, get_representation_path, registered_host ) -from openpype.pipeline.context_tools import get_current_project_name -from openpype.hosts.resolve.api import lib -from openpype.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID -from openpype.lib.transcoding import ( +from ayon_core.pipeline.context_tools import get_current_project_name +from ayon_core.hosts.resolve.api import lib +from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) -from openpype.lib import BoolDef +from ayon_core.lib import BoolDef def find_clip_usage(media_pool_item, project=None): @@ -48,13 +48,13 @@ def find_clip_usage(media_pool_item, project=None): matching_items = [] unique_id = media_pool_item.GetUniqueId() for timeline_idx in range(project.GetTimelineCount()): - timeline = project.GetTimelineByIndex(timeline_idx+1) + timeline = project.GetTimelineByIndex(timeline_idx + 1) # Consider audio and video tracks for track_type in ["video", "audio"]: for track_idx in range(timeline.GetTrackCount(track_type)): timeline_items = timeline.GetItemListInTrack(track_type, - track_idx+1) + track_idx + 1) for timeline_item in timeline_items: timeline_item_mpi = timeline_item.GetMediaPoolItem() if not timeline_item_mpi: From 7d391231ea3df68a58f0484dc2efc5246e03e1cf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Mar 2024 17:28:41 +0100 Subject: [PATCH 003/101] Update metadata handling in LoadMedia class Added a list of metadata items for clip properties. Refactored setting clip metadata based on context. --- .../hosts/resolve/plugins/load/load_media.py | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 60fbea483e..dd71e429f1 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -112,6 +112,29 @@ class LoadMedia(LoaderPlugin): bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" + metadata = [ + { + "name": "Comments", + "value": "{version[data][comment]}" + }, + { + "name": "Shot", + "value": "{asset[name]}" + }, + { + "name": "Take", + "value": "{subset[name]} v{version[name]:03d}" + }, + { + "name": "Clip Name", + "value": ( + "{asset[name]} {subset[name]} " + "v{version[name]:03d} ({representation[name]})" + ) + } + ] + + def load(self, context, name, namespace, options): # For loading multiselection, we store timeline before first load @@ -297,35 +320,10 @@ class LoadMedia(LoaderPlugin): def _set_metadata(self, media_pool_item, context: dict): """Set Media Pool Item Clip Properties""" - # Set the timecode for the loaded clip if Resolve doesn't parse it - # correctly from the input. An image sequence will have timecode - # parsed from its frame range, we will want to preserve that. - # TODO: Setting the Start TC breaks existing clips on update - # See: https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197327 - # Once that's resolved we should enable this - # start_tc = media_pool_item.GetClipProperty("Start TC") - # if start_tc == "00:00:00:00": - # from openpype.pipeline.editorial import frames_to_timecode - # # Assume no timecode was detected from the source media - # - # fps = float(media_pool_item.GetClipProperty("FPS")) - # handle_start = context["version"]["data"].get("handleStart", 0) - # frame_start = context["version"]["data"].get("frameStart", 0) - # frame_start_handle = frame_start - handle_start - # timecode = frames_to_timecode(frame_start_handle, fps) - # - # if timecode != start_tc: - # media_pool_item.SetClipProperty("Start TC", timecode) - # Set more clip metadata based on the loaded clip's context - metadata = { - "Clip Name": "{asset[name]} {subset[name]} " - "v{version[name]:03d} ({representation[name]})", - "Shot": "{asset[name]}", - "Take": "{subset[name]} v{version[name]:03d}", - "Comments": "{version[data][comment]}" - } - for clip_property, value in metadata.items(): + for meta_item in self.metadata: + clip_property = meta_item["name"] + value = meta_item["value"] media_pool_item.SetClipProperty(clip_property, value.format_map(context)) From 1f9b9c03623d264ace3c17d434fc34e9c9a3e4d8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Mar 2024 22:45:37 +0100 Subject: [PATCH 004/101] Update Resolve settings and LoadMedia plugin configurations: - Refactored clip color variables - Updated media pool bin path template - Modified metadata mapping structure for LoadMedia plugin --- .../hosts/resolve/plugins/load/load_media.py | 43 +++----- server_addon/resolve/server/settings.py | 100 +++++++++++++++++- server_addon/resolve/server/version.py | 2 +- 3 files changed, 109 insertions(+), 36 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index dd71e429f1..e6270ee2ab 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -7,6 +7,7 @@ from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) +from ayon_core.lib import StringTemplate from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, @@ -108,32 +109,12 @@ class LoadMedia(LoaderPlugin): # presets clip_color_last = "Olive" - clip_color = "Orange" + clip_color_old = "Orange" - bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" - - metadata = [ - { - "name": "Comments", - "value": "{version[data][comment]}" - }, - { - "name": "Shot", - "value": "{asset[name]}" - }, - { - "name": "Take", - "value": "{subset[name]} v{version[name]:03d}" - }, - { - "name": "Clip Name", - "value": ( - "{asset[name]} {subset[name]} " - "v{version[name]:03d} ({representation[name]})" - ) - } - ] + media_pool_bin_path = ( + "Loader/{representation[context][hierarchy]}/{asset[name]}") + metadata = [] def load(self, context, name, namespace, options): @@ -165,10 +146,11 @@ class LoadMedia(LoaderPlugin): if item is None: # Create or set the bin folder, we add it in there # If bin path is not set we just add into the current active bin - if self.bin_path: - bin_path = self.bin_path.format(**context) + if self.media_pool_bin_path: + media_pool_bin_path = StringTemplate( + self.media_pool_bin_path).format_strict(context) folder = lib.create_bin( - name=bin_path, + name=media_pool_bin_path, root=media_pool.GetRootFolder(), set_as_current=False ) @@ -315,7 +297,7 @@ class LoadMedia(LoaderPlugin): if version.get("name") == last_version.get("name"): return cls.clip_color_last else: - return cls.clip_color + return cls.clip_color_old def _set_metadata(self, media_pool_item, context: dict): """Set Media Pool Item Clip Properties""" @@ -324,8 +306,9 @@ class LoadMedia(LoaderPlugin): for meta_item in self.metadata: clip_property = meta_item["name"] value = meta_item["value"] - media_pool_item.SetClipProperty(clip_property, - value.format_map(context)) + value_formatted = StringTemplate(value).format_strict(context) + media_pool_item.SetClipProperty( + clip_property, value_formatted) def _get_filepath(self, representation: dict) -> Union[str, dict]: diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index dcdb2f1b27..d7c098d023 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -1,4 +1,9 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField +from pydantic import validator +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField, + ensure_unique_names, +) from .imageio import ResolveImageIOModel @@ -56,7 +61,7 @@ class CreateShotClipModels(BaseSettingsModel): workfileFrameStart: int = SettingsField( 1001, - title="Workfiles Start Frame", + title="Workfile Start Frame", section="Shot Attributes" ) handleStart: int = SettingsField( @@ -69,13 +74,64 @@ class CreateShotClipModels(BaseSettingsModel): ) -class CreatorPuginsModel(BaseSettingsModel): +class CreatorPluginsModel(BaseSettingsModel): CreateShotClip: CreateShotClipModels = SettingsField( default_factory=CreateShotClipModels, title="Create Shot Clip" ) +class MetadataMappingModel(BaseSettingsModel): + """Metadata mapping + + Representation document context data are used for formatting of + anatomy tokens. Following are supported: + - version + - task + - asset + + """ + name: str = SettingsField( + "", + title="Metadata property name" + ) + value: str = SettingsField( + "", + title="Metadata value template" + ) + + +class LoadMediaModel(BaseSettingsModel): + clip_color_last: str = SettingsField( + "Olive", + title="Clip color for last version" + ) + clip_color_old: str = SettingsField( + "Orange", + title="Clip color for old version" + ) + media_pool_bin_path: str = SettingsField( + "Loader/{representation[context][hierarchy]}/{asset[name]}", + title="Media Pool bin path template" + ) + metadata: list[MetadataMappingModel] = SettingsField( + default_factory=list, + title="Metadata mapping" + ) + + @validator("metadata") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class LoaderPluginsModel(BaseSettingsModel): + LoadMedia: LoadMediaModel = SettingsField( + default_factory=LoadMediaModel, + title="Load Media" + ) + + class ResolveSettings(BaseSettingsModel): launch_openpype_menu_on_start: bool = SettingsField( False, title="Launch OpenPype menu on start of Resolve" @@ -84,10 +140,14 @@ class ResolveSettings(BaseSettingsModel): default_factory=ResolveImageIOModel, title="Color Management (ImageIO)" ) - create: CreatorPuginsModel = SettingsField( - default_factory=CreatorPuginsModel, + create: CreatorPluginsModel = SettingsField( + default_factory=CreatorPluginsModel, title="Creator plugins", ) + load: LoaderPluginsModel = SettingsField( + default_factory=LoaderPluginsModel, + title="Loader plugins", + ) DEFAULT_VALUES = { @@ -109,5 +169,35 @@ DEFAULT_VALUES = { "handleStart": 10, "handleEnd": 10 } + }, + "load": { + "LoadMedia": { + "clip_color_last": "Olive", + "clip_color_old": "Orange", + "media_pool_bin_path": ( + "Loader/{representation[context][hierarchy]}/{asset[name]}" + ), + "metadata": [ + { + "name": "Comments", + "value": "{version[data][comment]}" + }, + { + "name": "Shot", + "value": "{asset[name]}" + }, + { + "name": "Take", + "value": "{subset[name]} v{version[name]:0>3}" + }, + { + "name": "Clip Name", + "value": ( + "{asset[name]} {subset[name]} " + "v{version[name]:0>3} ({representation[name]})" + ) + } + ] + } } } diff --git a/server_addon/resolve/server/version.py b/server_addon/resolve/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/resolve/server/version.py +++ b/server_addon/resolve/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From b59c646469090bd3f10c95477aa8baa3023209c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 12 Mar 2024 11:05:54 +0100 Subject: [PATCH 005/101] Reversing changes those are addressed in https://github.com/ynput/ayon-core/pull/92 --- client/ayon_core/hosts/resolve/api/lib.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 18d6aa31e5..deeb8d4d11 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -341,25 +341,19 @@ def get_timeline_item(media_pool_item: object, Returns: object: resolve.TimelineItem """ - clip_name = media_pool_item.GetClipProperty("File Name") + _clip_property = media_pool_item.GetClipProperty + clip_name = _clip_property("File Name") output_timeline_item = None timeline = timeline or get_current_timeline() with maintain_current_timeline(timeline): # search the timeline for the added clip - for ti_data in get_current_timeline_items(): - ti_clip_item = ti_data["clip"]["item"] - ti_media_pool_item = ti_clip_item.GetMediaPoolItem() - - # Skip items that do not have a media pool item, like for example - # an "Adjustment Clip" or a "Fusion Composition" from the effects - # toolbox - if not ti_media_pool_item: - continue - - if clip_name in ti_media_pool_item.GetClipProperty("File Name"): - output_timeline_item = ti_clip_item + for _ti_data in get_current_timeline_items(): + _ti_clip = _ti_data["clip"]["item"] + _ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty + if clip_name in _ti_clip_property("File Name"): + output_timeline_item = _ti_clip return output_timeline_item From 084472647d61ee08941c65fcebdda563a7afecfc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 16:35:44 +0100 Subject: [PATCH 006/101] Fixing reported bug on updating mediapoolitem --- client/ayon_core/hosts/resolve/plugins/load/load_media.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index e6270ee2ab..7f56eafe53 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -202,8 +202,12 @@ class LoadMedia(LoaderPlugin): data = json.loads(item.GetMetadata(lib.pype_tag_name)) # Update path - path = self._get_filepath(representation) - item.ReplaceClip(path) + path = get_representation_path(representation) + success = item.ReplaceClip(path) + if not success: + raise RuntimeError( + f"Failed to replace media pool item clip to filepath: {path}" + ) # Update the metadata update_data = self._get_container_data(representation) From 15b23b13313faf8080ed9c23c3365b4f5a7026f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 16:37:16 +0100 Subject: [PATCH 007/101] Adapting Colorbleeds colorspace distribution https://github.com/BigRoy/OpenPype/commit/e22d5dbb9fe5b9006a6b81bb156e875c47ab3638 https://github.com/BigRoy/OpenPype/commit/a403669e8389681f4041438ecf443365c0f972b9 --- .../hosts/resolve/plugins/load/load_media.py | 131 +++++++++++++++++- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 7f56eafe53..ccbf76b3d0 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -1,13 +1,15 @@ import json import copy +import contextlib from collections import defaultdict -from typing import Union +from typing import Union, Optional from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) from ayon_core.lib import StringTemplate +from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, @@ -24,6 +26,57 @@ from ayon_core.lib.transcoding import ( from ayon_core.lib import BoolDef +@contextlib.contextmanager +def project_color_science_mode(project=None, mode="davinciYRGBColorManagedv2"): + """Set project color science mode during context. + + This is especially useful as context for setting the colorspace for media + pool items, because when Resolve is not set to `davinciYRGBColorManagedv2` + it fails to set its "Input Color Space" clip property even though it is + accessible and settable via the Resolve User Interface. + + Args + project (Project): The active Resolve Project. + mode (Optional[str]): The color science mode to apply during the + context. Defaults to 'davinciYRGBColorManagedv2' + + See Also: + https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197441 + """ + + if project is None: + project = lib.get_current_project() + + original_mode = project.GetSetting("colorScienceMode") + if original_mode != mode: + project.SetSetting("colorScienceMode", mode) + try: + yield + finally: + if project.GetSetting("colorScienceMode") != original_mode: + project.SetSetting("colorScienceMode", original_mode) + + +def set_colorspace(media_pool_item, + colorspace, + mode="davinciYRGBColorManagedv2"): + """Set MediaPoolItem colorspace. + This implements a workaround that you cannot set the input colorspace + unless the Resolve project's color science mode is set to + `davinciYRGBColorManagedv2`. + Args: + media_pool_item (MediaPoolItem): The media pool item. + colorspace (str): The colorspace to apply. + mode (Optional[str]): The Resolve project color science mode to be in + while setting the colorspace. + Defaults to 'davinciYRGBColorManagedv2' + Returns: + bool: Whether applying the colorspace succeeded. + """ + with project_color_science_mode(mode=mode): + return media_pool_item.SetClipProperty("Input Color Space", colorspace) + + def find_clip_usage(media_pool_item, project=None): """Return all Timeline Items in the project using the Media Pool Item. @@ -116,6 +169,13 @@ class LoadMedia(LoaderPlugin): metadata = [] + # cached on apply settings + _host_imageio_settings = None + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls._host_imageio_settings = project_settings["resolve"]["imageio"] + def load(self, context, name, namespace, options): # For loading multiselection, we store timeline before first load @@ -163,8 +223,8 @@ class LoadMedia(LoaderPlugin): assert len(items) == 1, "Must import only one media item" item = items[0] - self._set_metadata(media_pool_item=item, - context=context) + self._set_metadata(item, context) + self._set_colorspace_from_representation(item, representation) data = self._get_container_data(representation) @@ -201,6 +261,10 @@ class LoadMedia(LoaderPlugin): # metadata gets removed data = json.loads(item.GetMetadata(lib.pype_tag_name)) + # Get metadata to preserve after the clip replacement + # TODO: Maybe preserve more, like LUT, Alpha Mode, Input Sizing Preset + colorspace_before = item.GetClipProperty("Input Color Space") + # Update path path = get_representation_path(representation) success = item.ReplaceClip(path) @@ -215,7 +279,20 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) context = get_representation_context(representation) - self._set_metadata(media_pool_item=item, context=context) + self._set_metadata(item, context) + self._set_colorspace_from_representation(item, representation) + + # If no specific colorspace is set then we want to preserve the + # colorspace a user might have set before the clip replacement + if ( + item.GetClipProperty("Input Color Space") == "Project" + and colorspace_before != "Project" + ): + result = set_colorspace(item, colorspace_before) + if not result: + self.log.warning( + f"Failed to re-apply colorspace: {colorspace_before}." + ) # Update the clip color color = self.get_item_color(representation) @@ -348,3 +425,49 @@ class LoadMedia(LoaderPlugin): "StartIndex": frame_start_handle, "EndIndex": frame_end_handle } + + def _get_colorspace(self, representation: dict) -> Optional[str]: + """Return Resolve native colorspace from OCIO colorspace data. + + Returns: + Optional[str]: The Resolve native colorspace name, if any mapped. + """ + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return + + ocio_colorspace = data["colorspace"] + if not ocio_colorspace: + return + + resolve_colorspace = get_remapped_colorspace_to_native( + ocio_colorspace_name=ocio_colorspace, + host_name="resolve", + imageio_host_settings=self._host_imageio_settings + ) + if resolve_colorspace: + return resolve_colorspace + else: + self.log.warning( + f"No mapping from OCIO colorspace '{ocio_colorspace}' " + "found to a Resolve colorspace. " + "Ignoring colorspace." + ) + + def _set_colorspace_from_representation( + self, media_pool_item, representation: dict): + """Set the colorspace for the media pool item. + + Args: + media_pool_item (MediaPoolItem): The media pool item. + representation (dict): The representation data. + """ + # Set the Resolve Input Color Space for the media. + colorspace = self._get_colorspace(representation) + if colorspace: + result = set_colorspace(media_pool_item, colorspace) + if not result: + self.log.warning( + f"Failed to apply colorspace: {colorspace}." + ) From dac1c2ba4c134ad697580ab5d5f9315d0e92c896 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:21:54 +0100 Subject: [PATCH 008/101] Fix default setting --- server_addon/resolve/server/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index d7c098d023..9a3f78c0ed 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -111,7 +111,7 @@ class LoadMediaModel(BaseSettingsModel): title="Clip color for old version" ) media_pool_bin_path: str = SettingsField( - "Loader/{representation[context][hierarchy]}/{asset[name]}", + "Loader/{folder[path]}", title="Media Pool bin path template" ) metadata: list[MetadataMappingModel] = SettingsField( @@ -180,21 +180,21 @@ DEFAULT_VALUES = { "metadata": [ { "name": "Comments", - "value": "{version[data][comment]}" + "value": "{version[attrib][comment]}" }, { "name": "Shot", - "value": "{asset[name]}" + "value": "{folder[path]}" }, { "name": "Take", - "value": "{subset[name]} v{version[name]:0>3}" + "value": "{product[name]} {version[name]}" }, { "name": "Clip Name", "value": ( - "{asset[name]} {subset[name]} " - "v{version[name]:0>3} ({representation[name]})" + "{folder[path]} {product[name]} " + "{version[name]} ({representation[name]})" ) } ] From 78104de8488e75ef0bba5a37db993d59ab1a266a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:22:02 +0100 Subject: [PATCH 009/101] Add description --- server_addon/resolve/server/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 9a3f78c0ed..7fd790837d 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -116,7 +116,11 @@ class LoadMediaModel(BaseSettingsModel): ) metadata: list[MetadataMappingModel] = SettingsField( default_factory=list, - title="Metadata mapping" + title="Metadata mapping", + description=( + "Set these media pool item metadata values on load and update. The" + " keys must match the exact Resolve metadata names like" + " 'Clip Name' or 'Shot'" ) @validator("metadata") From e610b7412dfd3566427c886cc03c86606acebef8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:22:18 +0100 Subject: [PATCH 010/101] Fix loader refactor to AYON --- .../hosts/resolve/plugins/load/load_media.py | 109 ++++++++---------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index ccbf76b3d0..c1016ce053 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -2,21 +2,16 @@ import json import copy import contextlib from collections import defaultdict -from typing import Union, Optional +from typing import Union, List, Optional, TypedDict -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) +from ayon_api import version_is_latest from ayon_core.lib import StringTemplate from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( LoaderPlugin, - get_representation_context, get_representation_path, registered_host ) -from ayon_core.pipeline.context_tools import get_current_project_name from ayon_core.hosts.resolve.api import lib from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( @@ -26,6 +21,12 @@ from ayon_core.lib.transcoding import ( from ayon_core.lib import BoolDef +class MetadataEntry(TypedDict): + """Metadata entry is dict with {"name": "key", "value: "value"}""" + name: str + value: str + + @contextlib.contextmanager def project_color_science_mode(project=None, mode="davinciYRGBColorManagedv2"): """Set project color science mode during context. @@ -126,9 +127,9 @@ def find_clip_usage(media_pool_item, project=None): class LoadMedia(LoaderPlugin): - """Load a subset as media pool item.""" + """Load product as media pool item.""" - families = ["render2d", "source", "plate", "render", "review"] + product_types = {"render2d", "source", "plate", "render", "review"} representations = ["*"] extensions = set( @@ -164,16 +165,16 @@ class LoadMedia(LoaderPlugin): clip_color_last = "Olive" clip_color_old = "Orange" - media_pool_bin_path = ( - "Loader/{representation[context][hierarchy]}/{asset[name]}") + media_pool_bin_path = "Loader/{folder[path]}" - metadata = [] + metadata: List[MetadataEntry] = [] # cached on apply settings _host_imageio_settings = None @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): + super(LoadMedia, cls).apply_settings(project_settings) cls._host_imageio_settings = project_settings["resolve"]["imageio"] def load(self, context, name, namespace, options): @@ -192,7 +193,7 @@ class LoadMedia(LoaderPlugin): item = None if options.get("load_once", True): host = registered_host() - repre_id = str(context["representation"]["_id"]) + repre_id = context["representation"]["id"] for container in host.ls(): if container["representation"] != repre_id: continue @@ -217,7 +218,7 @@ class LoadMedia(LoaderPlugin): media_pool.SetCurrentFolder(folder) # Import media - path = self._get_filepath(representation) + path = self._get_filepath(context) items = media_pool.ImportMedia([path]) assert len(items) == 1, "Must import only one media item" @@ -226,7 +227,7 @@ class LoadMedia(LoaderPlugin): self._set_metadata(item, context) self._set_colorspace_from_representation(item, representation) - data = self._get_container_data(representation) + data = self._get_container_data(context) # Add containerise data only needed on first load data.update({ @@ -238,7 +239,7 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) # Always update clip color - even if re-using existing clip - color = self.get_item_color(representation) + color = self.get_item_color(context) item.SetClipColor(color) if options.get("load_to_timeline", True): @@ -250,10 +251,10 @@ class LoadMedia(LoaderPlugin): timeline=timeline ) - def switch(self, container, representation): - self.update(container, representation) + def switch(self, container, context): + self.update(container, context) - def update(self, container, representation): + def update(self, container, context): # Update MediaPoolItem filepath and metadata item = container["_item"] @@ -266,7 +267,7 @@ class LoadMedia(LoaderPlugin): colorspace_before = item.GetClipProperty("Input Color Space") # Update path - path = get_representation_path(representation) + path = get_representation_path(context["representation"]) success = item.ReplaceClip(path) if not success: raise RuntimeError( @@ -274,12 +275,11 @@ class LoadMedia(LoaderPlugin): ) # Update the metadata - update_data = self._get_container_data(representation) + update_data = self._get_container_data(context) data.update(update_data) item.SetMetadata(lib.pype_tag_name, json.dumps(data)) - context = get_representation_context(representation) - self._set_metadata(item, context) + self._set_metadata(media_pool_item=item, context=context) self._set_colorspace_from_representation(item, representation) # If no specific colorspace is set then we want to preserve the @@ -295,7 +295,7 @@ class LoadMedia(LoaderPlugin): ) # Update the clip color - color = self.get_item_color(representation) + color = self.get_item_color(context) item.SetClipColor(color) def remove(self, container): @@ -327,55 +327,43 @@ class LoadMedia(LoaderPlugin): # Delete the media pool item media_pool.DeleteClips([item]) - def _get_container_data(self, representation): + def _get_container_data(self, context: dict) -> dict: """Return metadata related to the representation and version.""" - # load clip to timeline and get main variables - project_name = get_current_project_name() - version = get_version_by_id(project_name, representation["parent"]) - version_data = version.get("data", {}) - version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) + # add additional metadata from the version to imprint AYON knob + version = context["version"] + data = {} - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "source", "author", - "fps", "handleStart", "handleEnd" - ] - data = { - key: version_data.get(key, str(None)) for key in add_keys - } + # version.attrib + for key in [ + "frameStart", "frameEnd", + "handleStart", "handleEnd", + "source", "fps", "colorSpace" + ]: + data[key] = version["attrib"][key] + + # version.data + for key in ["author"]: + data[key] = version["data"][key] # add variables related to version context data.update({ - "representation": str(representation["_id"]), - "version": version_name, - "colorspace": colorspace, + "representation": context["representation"]["id"], + "version": version["name"], }) return data @classmethod - def get_item_color(cls, representation) -> str: + def get_item_color(cls, context: dict) -> str: """Return item color name. Coloring depends on whether representation is the latest version. """ # Compare version with last version - project_name = get_current_project_name() - version = get_version_by_id( - project_name, - representation["parent"], - fields=["name", "parent"] - ) - last_version = get_last_version_by_subset_id( - project_name, - version["parent"], - fields=["name"] - ) or {} - # set clip colour - if version.get("name") == last_version.get("name"): + if version_is_latest(project_name=context["project"]["name"], + version_id=context["version"]["id"]): return cls.clip_color_last else: return cls.clip_color_old @@ -388,16 +376,15 @@ class LoadMedia(LoaderPlugin): clip_property = meta_item["name"] value = meta_item["value"] value_formatted = StringTemplate(value).format_strict(context) - media_pool_item.SetClipProperty( - clip_property, value_formatted) + media_pool_item.SetClipProperty(clip_property, value_formatted) - def _get_filepath(self, representation: dict) -> Union[str, dict]: + def _get_filepath(self, context: dict) -> Union[str, dict]: + representation = context["representation"] is_sequence = bool(representation["context"].get("frame")) if not is_sequence: return get_representation_path(representation) - context = get_representation_context(representation) version = context["version"] # Get the start and end frame of the image sequence, incl. handles From 9b3e4008ed8ef2956c08534aa8b762d2cd984e74 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:25:54 +0100 Subject: [PATCH 011/101] Fix missing bracket --- server_addon/resolve/server/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 7fd790837d..8fd996d8bd 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -121,6 +121,7 @@ class LoadMediaModel(BaseSettingsModel): "Set these media pool item metadata values on load and update. The" " keys must match the exact Resolve metadata names like" " 'Clip Name' or 'Shot'" + ) ) @validator("metadata") From dd399586fc7314b3f488bad4420dbec0232be6bb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:27:08 +0100 Subject: [PATCH 012/101] Fix default setting --- server_addon/resolve/server/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 8fd996d8bd..4d363b1a8f 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -180,7 +180,7 @@ DEFAULT_VALUES = { "clip_color_last": "Olive", "clip_color_old": "Orange", "media_pool_bin_path": ( - "Loader/{representation[context][hierarchy]}/{asset[name]}" + "Loader/{folder[path]}" ), "metadata": [ { From 2811eaddb8696ab3816370f9f78699d8dfa9795c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Apr 2024 22:00:21 +0100 Subject: [PATCH 013/101] Working version --- client/ayon_core/hosts/nuke/api/pipeline.py | 6 + .../hosts/nuke/api/push_to_project.py | 116 ++++++++++++++++++ .../tools/push_to_project/control.py | 5 +- .../ayon_core/tools/push_to_project/main.py | 16 ++- .../tools/push_to_project/ui/window.py | 43 +++++-- 5 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 client/ayon_core/hosts/nuke/api/push_to_project.py diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index 0d44aba2f9..23d06c4609 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -68,6 +68,7 @@ from .workio import ( current_file ) from .constants import ASSIST +from . import push_to_project log = Logger.get_logger(__name__) @@ -339,6 +340,11 @@ def _install_menu(): lambda: update_placeholder() ) + menu.addCommand( + "Push to Project", + lambda: push_to_project.main() + ) + menu.addSeparator() menu.addCommand( "Experimental tools...", diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py new file mode 100644 index 0000000000..a67a032179 --- /dev/null +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -0,0 +1,116 @@ +from collections import defaultdict +import shutil +import os + +from ayon_api import get_project, get_folder_by_id, get_task_by_id +from ayon_core.settings import get_ayon_settings, get_project_settings +from ayon_core.pipeline import Anatomy, registered_host +from ayon_core.pipeline.template_data import get_template_data +from ayon_core.pipeline.workfile import get_workdir_with_workdir_data +from ayon_core.tools.push_to_project.main import main_show + +from .utils import bake_gizmos_recursively + +import nuke + + +def bake_container(container): + """Bake containers to read nodes.""" + + node = container["node"] + + # Fetch knobs to remove in order. + knobs_to_remove = [] + remove = False + for count in range(0, node.numKnobs()): + knob = node.knob(count) + + # All knobs from "OpenPype" tab knob onwards. + if knob.name() == "AYON": + remove = True + + if remove: + knobs_to_remove.append(knob) + + # Dont remove knobs from "containerId" onwards. + if knob.name() == "containerId": + remove = False + + # Knobs needs to be remove in reverse order, because child knobs needs to + # be remove first. + for knob in reversed(knobs_to_remove): + node.removeKnob(knob) + + node["tile_color"].setValue(0) + + +def main(): + context = main_show("", "", False, True) + + if context is None: + return + + # Get workfile path to save to. + project_name = context["project_name"] + project = get_project(project_name) + folder = get_folder_by_id(project_name, context["folder_id"]) + task = get_task_by_id(project_name, context["task_id"]) + host = registered_host() + ayon_settings = get_ayon_settings() + project_settings = get_project_settings(project_name) + anatomy = Anatomy(project_name) + + workdir_data = get_template_data( + project, folder, task, host.name, ayon_settings + ) + + workdir = get_workdir_with_workdir_data( + workdir_data, + project_name, + anatomy, + project_settings=project_settings + ) + + # Save current workfile. + current_file = host.current_file() + host.save_file(current_file) + + for container in host.ls(): + bake_container(container) + + # Bake gizmos. + bake_gizmos_recursively() + + # Copy all read node files to "resources" folder next to workfile and + # change file path. + first_frame = int(nuke.root()["first_frame"].value()) + last_frame = int(nuke.root()["last_frame"].value()) + files_by_node_name = defaultdict(set) + nodes_by_name = {} + for count in range(first_frame, last_frame + 1): + nuke.frame(count) + for node in nuke.allNodes(filter="Read"): + files_by_node_name[node.name()].add( + nuke.filename(node, nuke.REPLACE) + ) + nodes_by_name[node.name()] = node + + resources_dir = os.path.join(workdir, "resources") + for name, files in files_by_node_name.items(): + dir = os.path.join(resources_dir, name) + if not os.path.exists(dir): + os.makedirs(dir) + + for f in files: + shutil.copy(f, os.path.join(dir, os.path.basename(f))) + + node = nodes_by_name[name] + path = node["file"].value().replace(os.path.dirname(f), dir) + node["file"].setValue(path.replace("\\", "/")) + + # Save current workfile to new context. + basename = os.path.basename(current_file) + host.save_file(os.path.join(workdir, basename)) + + # Open current contex workfile. + host.open_file(current_file) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58447a8389..96792a2e9b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -169,13 +169,16 @@ class PushToContextController: return self._integrate_model.get_item_status(item_id) # Processing methods - def submit(self, wait=True): + def submit(self, wait=True, context_only=False): if not self._submission_enabled: return if self._process_thread is not None: return + if context_only: + return + item_id = self._integrate_model.create_process_item( self._src_project_name, self._src_version_id, diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index a6ff38c16f..d230f6a660 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,14 +4,20 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def main_show(project_name, version_id): - app = get_ayon_qt_app() - - window = PushToContextSelectWindow() +def main_show(project_name, version_id, library_filter, context_only): + window = PushToContextSelectWindow( + library_filter=library_filter, context_only=context_only + ) window.show() window.set_source(project_name, version_id) - app.exec_() + if __name__ == "__main__": + app = get_ayon_qt_app() + app.exec_() + else: + window.exec_() + + return window.context @click.command() diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 4d64509afd..2a26388221 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -14,12 +14,16 @@ from ayon_core.tools.push_to_project.control import ( ) -class PushToContextSelectWindow(QtWidgets.QWidget): - def __init__(self, controller=None): +class PushToContextSelectWindow(QtWidgets.QDialog): + def __init__( + self, controller=None, library_filter=True, context_only=False + ): super(PushToContextSelectWindow, self).__init__() if controller is None: controller = PushToContextController() self._controller = controller + self.context_only = context_only + self.context = None self.setWindowTitle("Push to project (select context)") self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) @@ -45,7 +49,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(True) + projects_combobox.set_standard_filter_enabled(False) + if library_filter: + projects_combobox.set_standard_filter_enabled(True) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -89,13 +95,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # --- Buttons widget --- btns_widget = QtWidgets.QWidget(self) cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - publish_btn = QtWidgets.QPushButton("Publish", btns_widget) + push_btn = QtWidgets.QPushButton("Push", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(publish_btn, 0) + btns_layout.addWidget(push_btn, 0) sep_1 = SeparatorWidget(parent=main_context_widget) sep_2 = SeparatorWidget(parent=main_context_widget) @@ -160,7 +166,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) - publish_btn.clicked.connect(self._on_select_click) + push_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -206,7 +212,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_name_input = folder_name_input self._comment_input = comment_input - self._publish_btn = publish_btn + self._push_btn = push_btn self._overlay_widget = overlay_widget self._overlay_close_btn = overlay_close_btn @@ -234,7 +240,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._variant_is_valid = None self._folder_is_valid = None - publish_btn.setEnabled(False) + push_btn.setEnabled(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -372,13 +378,30 @@ class PushToContextSelectWindow(QtWidgets.QWidget): set_style_property(self._variant_input, "state", state) def _on_submission_change(self, event): - self._publish_btn.setEnabled(event["enabled"]) + self._push_btn.setEnabled(event["enabled"]) def _on_close_click(self): self.close() def _on_select_click(self): - self._process_item_id = self._controller.submit(wait=False) + result = self._controller.submit( + wait=True, context_only=self.context_only + ) + + if self.context_only: + user_values = self._controller.get_user_values() + selection_model = self._controller._selection_model + self.context = { + "project_name": selection_model._project_name, + "folder_id": selection_model._folder_id, + "task_id": selection_model._task_id, + "variant": user_values["variant"], + "comment": user_values["comment"], + "folder_name": user_values["new_folder_name"] + } + self.close() + + self._process_item = result def _on_try_again_click(self): self._process_item_id = None From e0ad6af4fb841a5ca923e0fa59295ce4880f48aa Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:28 +0100 Subject: [PATCH 014/101] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index a67a032179..b58dd99734 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -56,7 +56,6 @@ def main(): folder = get_folder_by_id(project_name, context["folder_id"]) task = get_task_by_id(project_name, context["task_id"]) host = registered_host() - ayon_settings = get_ayon_settings() project_settings = get_project_settings(project_name) anatomy = Anatomy(project_name) From 81a75f7788f3debd781a851a48c23ce539ba1f36 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:42 +0100 Subject: [PATCH 015/101] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index b58dd99734..cd7e522d06 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -60,7 +60,7 @@ def main(): anatomy = Anatomy(project_name) workdir_data = get_template_data( - project, folder, task, host.name, ayon_settings + project, folder, task, host.name, project_settings ) workdir = get_workdir_with_workdir_data( From 5f858139a7ffb57f65a93d904fc7c71cfb408094 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:51 +0100 Subject: [PATCH 016/101] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index cd7e522d06..75ce08fb71 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -25,7 +25,7 @@ def bake_container(container): for count in range(0, node.numKnobs()): knob = node.knob(count) - # All knobs from "OpenPype" tab knob onwards. + # All knobs from "AYON" tab knob onwards. if knob.name() == "AYON": remove = True From 7411f2181c8ae4ec6e31639eb588e693b3f9554a Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:35:53 +0100 Subject: [PATCH 017/101] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 75ce08fb71..2f14f4e5f4 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -26,7 +26,7 @@ def bake_container(container): knob = node.knob(count) # All knobs from "AYON" tab knob onwards. - if knob.name() == "AYON": + if knob.name() == MENU_LABEL: remove = True if remove: From 80f340cb6ce2cd3ab1bd51238660c36d6f32db31 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 30 Apr 2024 16:49:13 +0100 Subject: [PATCH 018/101] Separate show method --- .../hosts/nuke/api/push_to_project.py | 7 ++++--- .../ayon_core/tools/push_to_project/main.py | 20 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 2f14f4e5f4..b26e7f9aff 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -3,13 +3,14 @@ import shutil import os from ayon_api import get_project, get_folder_by_id, get_task_by_id -from ayon_core.settings import get_ayon_settings, get_project_settings +from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.push_to_project.main import main_show +from ayon_core.tools.push_to_project.main import show from .utils import bake_gizmos_recursively +from .lib import MENU_LABEL import nuke @@ -45,7 +46,7 @@ def bake_container(container): def main(): - context = main_show("", "", False, True) + context = show("", "", False, True) if context is None: return diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index d230f6a660..bfb921a2b7 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,22 +4,26 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def main_show(project_name, version_id, library_filter, context_only): +def show(project_name, version_id, library_filter, context_only): window = PushToContextSelectWindow( library_filter=library_filter, context_only=context_only ) window.show() window.set_source(project_name, version_id) - - if __name__ == "__main__": - app = get_ayon_qt_app() - app.exec_() - else: - window.exec_() - + window.exec_() return window.context +def main_show(project_name, version_id): + app = get_ayon_qt_app() + + window = PushToContextSelectWindow() + window.show() + window.set_source(project_name, version_id) + + app.exec_() + + @click.command() @click.option("--project", help="Source project name") @click.option("--version", help="Source version id") From 5564f07a37aba1688b2e7e5b7988ceacfc4314c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 14:09:39 +0100 Subject: [PATCH 019/101] Use context_dialog --- client/ayon_core/hosts/nuke/api/push_to_project.py | 4 ++-- client/ayon_core/tools/context_dialog/__init__.py | 3 ++- client/ayon_core/tools/context_dialog/window.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index b26e7f9aff..7d6bfa0f32 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -7,7 +7,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.push_to_project.main import show +from ayon_core.tools.context_dialog import show from .utils import bake_gizmos_recursively from .lib import MENU_LABEL @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = show("", "", False, True) + context = show() if context is None: return diff --git a/client/ayon_core/tools/context_dialog/__init__.py b/client/ayon_core/tools/context_dialog/__init__.py index 4fb912fb62..66920c583c 100644 --- a/client/ayon_core/tools/context_dialog/__init__.py +++ b/client/ayon_core/tools/context_dialog/__init__.py @@ -1,7 +1,8 @@ -from .window import ContextDialog, main +from .window import ContextDialog, main, show __all__ = ( "ContextDialog", "main", + "show" ) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index 828d771142..f235aa85d8 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -343,7 +343,7 @@ class ContextDialogController: def store_output(self): if not self._output_path: - return + return self.get_selected_context() dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) @@ -791,3 +791,15 @@ def main( window.show() app.exec_() controller.store_output() + + +def show( + strict=True +): + controller = ContextDialogController() + controller.set_strict(strict) + window = ContextDialog(controller=controller) + window.show() + window.exec_() + + return controller.store_output() From 7dd6f28e8dda2e41b4443577e1456aa415cf160a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 14:12:39 +0100 Subject: [PATCH 020/101] Revert changes to push_to_project --- .../tools/push_to_project/control.py | 5 +-- .../ayon_core/tools/push_to_project/main.py | 10 ----- .../tools/push_to_project/ui/window.py | 43 +++++-------------- 3 files changed, 11 insertions(+), 47 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 96792a2e9b..58447a8389 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -169,16 +169,13 @@ class PushToContextController: return self._integrate_model.get_item_status(item_id) # Processing methods - def submit(self, wait=True, context_only=False): + def submit(self, wait=True): if not self._submission_enabled: return if self._process_thread is not None: return - if context_only: - return - item_id = self._integrate_model.create_process_item( self._src_project_name, self._src_version_id, diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index bfb921a2b7..a6ff38c16f 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,16 +4,6 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def show(project_name, version_id, library_filter, context_only): - window = PushToContextSelectWindow( - library_filter=library_filter, context_only=context_only - ) - window.show() - window.set_source(project_name, version_id) - window.exec_() - return window.context - - def main_show(project_name, version_id): app = get_ayon_qt_app() diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 2a26388221..4d64509afd 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -14,16 +14,12 @@ from ayon_core.tools.push_to_project.control import ( ) -class PushToContextSelectWindow(QtWidgets.QDialog): - def __init__( - self, controller=None, library_filter=True, context_only=False - ): +class PushToContextSelectWindow(QtWidgets.QWidget): + def __init__(self, controller=None): super(PushToContextSelectWindow, self).__init__() if controller is None: controller = PushToContextController() self._controller = controller - self.context_only = context_only - self.context = None self.setWindowTitle("Push to project (select context)") self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) @@ -49,9 +45,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(False) - if library_filter: - projects_combobox.set_standard_filter_enabled(True) + projects_combobox.set_standard_filter_enabled(True) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -95,13 +89,13 @@ class PushToContextSelectWindow(QtWidgets.QDialog): # --- Buttons widget --- btns_widget = QtWidgets.QWidget(self) cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - push_btn = QtWidgets.QPushButton("Push", btns_widget) + publish_btn = QtWidgets.QPushButton("Publish", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(push_btn, 0) + btns_layout.addWidget(publish_btn, 0) sep_1 = SeparatorWidget(parent=main_context_widget) sep_2 = SeparatorWidget(parent=main_context_widget) @@ -166,7 +160,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) - push_btn.clicked.connect(self._on_select_click) + publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -212,7 +206,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): self._folder_name_input = folder_name_input self._comment_input = comment_input - self._push_btn = push_btn + self._publish_btn = publish_btn self._overlay_widget = overlay_widget self._overlay_close_btn = overlay_close_btn @@ -240,7 +234,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): self._variant_is_valid = None self._folder_is_valid = None - push_btn.setEnabled(False) + publish_btn.setEnabled(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -378,30 +372,13 @@ class PushToContextSelectWindow(QtWidgets.QDialog): set_style_property(self._variant_input, "state", state) def _on_submission_change(self, event): - self._push_btn.setEnabled(event["enabled"]) + self._publish_btn.setEnabled(event["enabled"]) def _on_close_click(self): self.close() def _on_select_click(self): - result = self._controller.submit( - wait=True, context_only=self.context_only - ) - - if self.context_only: - user_values = self._controller.get_user_values() - selection_model = self._controller._selection_model - self.context = { - "project_name": selection_model._project_name, - "folder_id": selection_model._folder_id, - "task_id": selection_model._task_id, - "variant": user_values["variant"], - "comment": user_values["comment"], - "folder_name": user_values["new_folder_name"] - } - self.close() - - self._process_item = result + self._process_item_id = self._controller.submit(wait=False) def _on_try_again_click(self): self._process_item_id = None From 2082214b356671d9a28854ad7ee2a32e2509d1db Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 21:11:29 +0100 Subject: [PATCH 021/101] BigRoy feedback --- client/ayon_core/hosts/nuke/api/push_to_project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 7d6bfa0f32..fde26bfe81 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -7,7 +7,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.context_dialog import show +from ayon_core.tools import context_dialog from .utils import bake_gizmos_recursively from .lib import MENU_LABEL @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = show() + context = context_dialog.show() if context is None: return From 92f9fda546c09f097479bc3fc9db1fd642ced0e5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 May 2024 23:02:28 +0100 Subject: [PATCH 022/101] Update client/ayon_core/tools/context_dialog/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/context_dialog/window.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index f235aa85d8..532bfe878e 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -793,13 +793,10 @@ def main( controller.store_output() -def show( - strict=True -): +def ask_for_context(strict=True): controller = ContextDialogController() controller.set_strict(strict) window = ContextDialog(controller=controller) - window.show() window.exec_() - return controller.store_output() + return controller.get_selected_context() From f73d0a544d746ad1dd8f252c11720404912eb271 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 May 2024 23:02:34 +0100 Subject: [PATCH 023/101] Update client/ayon_core/tools/context_dialog/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/context_dialog/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index 532bfe878e..ea5fdfbaec 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -343,7 +343,7 @@ class ContextDialogController: def store_output(self): if not self._output_path: - return self.get_selected_context() + return dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) From 71471cf72592d8a155a2384ff07a84c8a182df54 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 May 2024 23:07:02 +0100 Subject: [PATCH 024/101] Fixes for ask_for_context --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- client/ayon_core/tools/context_dialog/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index fde26bfe81..f145ed652b 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = context_dialog.show() + context = context_dialog.ask_for_context() if context is None: return diff --git a/client/ayon_core/tools/context_dialog/__init__.py b/client/ayon_core/tools/context_dialog/__init__.py index 66920c583c..8a77a46109 100644 --- a/client/ayon_core/tools/context_dialog/__init__.py +++ b/client/ayon_core/tools/context_dialog/__init__.py @@ -1,8 +1,8 @@ -from .window import ContextDialog, main, show +from .window import ContextDialog, main, ask_for_context __all__ = ( "ContextDialog", "main", - "show" + "ask_for_context" ) From 9e181340c4f5ab2e1843787353daa48b581b4b0a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 7 Jun 2024 18:09:57 +0200 Subject: [PATCH 025/101] Remove deprecated ExtractReviewDataMov settings and related code. Refactor ExtractReviewIntermediates settings. --- .../publish/extract_review_intermediates.py | 23 ------ .../nuke/server/settings/publish_plugins.py | 72 ------------------- 2 files changed, 95 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py index b7bb911347..48c9988c5b 100644 --- a/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py @@ -28,29 +28,6 @@ class ExtractReviewIntermediates(publish.Extractor): viewer_lut_raw = None outputs = {} - @classmethod - def apply_settings(cls, project_settings): - """Apply the settings from the deprecated - ExtractReviewDataMov plugin for backwards compatibility - """ - nuke_publish = project_settings["nuke"]["publish"] - deprecated_setting = nuke_publish["ExtractReviewDataMov"] - current_setting = nuke_publish.get("ExtractReviewIntermediates") - if not deprecated_setting["enabled"] and ( - not current_setting["enabled"] - ): - cls.enabled = False - - if deprecated_setting["enabled"]: - # Use deprecated settings if they are still enabled - cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] - cls.outputs = deprecated_setting["outputs"] - elif current_setting is None: - pass - elif current_setting["enabled"]: - cls.viewer_lut_raw = current_setting["viewer_lut_raw"] - cls.outputs = current_setting["outputs"] - def process(self, instance): # TODO 'families' should not be included for filtering of outputs families = set(instance.data["families"]) diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 6c37ecd37a..c06d60abc8 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -155,18 +155,6 @@ class IntermediateOutputModel(BaseSettingsModel): title="Custom tags", default_factory=list) -class ExtractReviewDataMovModel(BaseSettingsModel): - """[deprecated] use Extract Review Data Baking - Streams instead. - """ - enabled: bool = SettingsField(title="Enabled") - viewer_lut_raw: bool = SettingsField(title="Viewer lut raw") - outputs: list[IntermediateOutputModel] = SettingsField( - default_factory=list, - title="Baking streams" - ) - - class ExtractReviewIntermediatesModel(BaseSettingsModel): enabled: bool = SettingsField(title="Enabled") viewer_lut_raw: bool = SettingsField(title="Viewer lut raw") @@ -259,10 +247,6 @@ class PublishPluginsModel(BaseSettingsModel): title="Extract Review Data Lut", default_factory=ExtractReviewDataLutModel ) - ExtractReviewDataMov: ExtractReviewDataMovModel = SettingsField( - title="Extract Review Data Mov", - default_factory=ExtractReviewDataMovModel - ) ExtractReviewIntermediates: ExtractReviewIntermediatesModel = ( SettingsField( title="Extract Review Intermediates", @@ -332,62 +316,6 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "ExtractReviewDataLut": { "enabled": False }, - "ExtractReviewDataMov": { - "enabled": False, - "viewer_lut_raw": False, - "outputs": [ - { - "name": "baking", - "publish": False, - "filter": { - "task_types": [], - "product_types": [], - "product_names": [] - }, - "read_raw": False, - "viewer_process_override": "", - "bake_viewer_process": True, - "bake_viewer_input_process": True, - "reformat_nodes_config": { - "enabled": False, - "reposition_nodes": [ - { - "node_class": "Reformat", - "knobs": [ - { - "type": "text", - "name": "type", - "text": "to format" - }, - { - "type": "text", - "name": "format", - "text": "HD_1080" - }, - { - "type": "text", - "name": "filter", - "text": "Lanczos6" - }, - { - "type": "boolean", - "name": "black_outside", - "boolean": True - }, - { - "type": "boolean", - "name": "pbb", - "boolean": False - } - ] - } - ] - }, - "extension": "mov", - "add_custom_tags": [] - } - ] - }, "ExtractReviewIntermediates": { "enabled": True, "viewer_lut_raw": False, From 6ecff1924e1f4cc5c69f4727cf92ee7838dbd3be Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 7 Jun 2024 18:10:13 +0200 Subject: [PATCH 026/101] Update version numbers to 0.2.3 in package and addon files. --- server_addon/nuke/client/ayon_nuke/version.py | 2 +- server_addon/nuke/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/version.py b/server_addon/nuke/client/ayon_nuke/version.py index 1130392592..2262afb410 100644 --- a/server_addon/nuke/client/ayon_nuke/version.py +++ b/server_addon/nuke/client/ayon_nuke/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'nuke' version.""" -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/server_addon/nuke/package.py b/server_addon/nuke/package.py index 9081205c44..7347d21b35 100644 --- a/server_addon/nuke/package.py +++ b/server_addon/nuke/package.py @@ -1,6 +1,6 @@ name = "nuke" title = "Nuke" -version = "0.2.2" +version = "0.2.3" client_dir = "ayon_nuke" From 67b59f9af48ab927c0e4cc1cfe7903cefcdf98d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 7 Jun 2024 18:10:38 +0200 Subject: [PATCH 027/101] introducing new baking targeting Added ColorspaceConfigurationModel for baking target selection and override in various settings to enhance flexibility and customization. --- server_addon/nuke/server/settings/common.py | 50 +++++++++++++++++++ server_addon/nuke/server/settings/imageio.py | 21 +++++--- .../nuke/server/settings/publish_plugins.py | 40 ++++++++++++--- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py index e0ee2b7b3d..50313fa5ef 100644 --- a/server_addon/nuke/server/settings/common.py +++ b/server_addon/nuke/server/settings/common.py @@ -133,3 +133,53 @@ class KnobModel(BaseSettingsModel): "", title="Expression" ) + + +colorspace_types_enum = [ + {"value": "colorspace", "label": "Use Colorspace"}, + {"value": "display_view", "label": "Use Display & View"}, +] + + +class DisplayAndViewProfileModel(BaseSettingsModel): + _layout = "expanded" + + display: str = SettingsField( + "", + title="Display", + description="What display to use", + ) + + view: str = SettingsField( + "", + title="View", + description="What view to use", + ) + + +class ColorspaceConfigurationModel(BaseSettingsModel): + enabled: bool = SettingsField( + False, + title="Enabled", + description="Enable baking target (colorspace or display/view)", + ) + + type: str = SettingsField( + "colorspace", + title="Target baking type", + description="Switch between different knob types", + enum_resolver=lambda: colorspace_types_enum, + conditionalEnum=True, + ) + + colorspace: str = SettingsField( + "", + title="Colorspace", + description="What colorspace name to use", + ) + + display_view: DisplayAndViewProfileModel = SettingsField( + title="Display & View", + description="What display & view to use", + default_factory=DisplayAndViewProfileModel, + ) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 9cdb0bf1d7..93433f3f54 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -6,8 +6,10 @@ from ayon_server.settings import ( ensure_unique_names, ) -from .common import KnobModel - +from .common import ( + KnobModel, + ColorspaceConfigurationModel, +) class NodesModel(BaseSettingsModel): _layout = "expanded" @@ -198,12 +200,10 @@ class ImageIOSettings(BaseSettingsModel): Creation of new viewer node at knob viewerProcess""" ) - """# TODO: enhance settings with host api: - to restructure settings for simplification. - - now: nuke/imageio/baking/viewerProcess - future: nuke/imageio/baking - """ + baking_target: ColorspaceConfigurationModel = SettingsField( + default_factory=ColorspaceConfigurationModel, + title="Baking Target Colorspace" + ) baking: ViewProcessModel = SettingsField( default_factory=ViewProcessModel, title="Baking", @@ -235,6 +235,11 @@ DEFAULT_IMAGEIO_SETTINGS = { "viewerProcess": "ACES/sRGB", "output_transform": "ACES/sRGB" }, + "baking_target": { + "enabled": False, + "type": "colorspace", + "colorspace": "" + }, "baking": { "viewerProcess": "ACES/Rec.709", "output_transform": "ACES/Rec.709" diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index c06d60abc8..1373b3b13d 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -5,7 +5,11 @@ from ayon_server.settings import ( ensure_unique_names, task_types_enum ) -from .common import KnobModel, validate_json_dict +from .common import ( + KnobModel, + ColorspaceConfigurationModel, + validate_json_dict, +) def nuke_render_publish_types_enum(): @@ -130,19 +134,30 @@ class IntermediateOutputModel(BaseSettingsModel): title="Filter", default_factory=BakingStreamFilterModel) read_raw: bool = SettingsField( False, - title="Read raw switch" - ) - viewer_process_override: str = SettingsField( - "", - title="Viewer process override" + title="Input read node RAW switch" ) bake_viewer_process: bool = SettingsField( True, - title="Bake viewer process" + title="Bake viewer process", + section="Baking target", + ) + colorspace_override: ColorspaceConfigurationModel = SettingsField( + title="Target baking colorspace override", + description="Override Baking target with colorspace or display/view", + default_factory=ColorspaceConfigurationModel + ) + viewer_process_override: str = SettingsField( + "", + title="Viewer process override", + description=( + "[DEPRECATED - use 'Target baking colorspace override'] " + "Override viewer process node (LUT)" + ), ) bake_viewer_input_process: bool = SettingsField( True, - title="Bake viewer input process node (LUT)" + title="Bake viewer input process node (LUT)", + section="Baking additional", ) reformat_nodes_config: ReformatNodesConfigModel = SettingsField( default_factory=ReformatNodesConfigModel, @@ -330,6 +345,15 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { }, "read_raw": False, "viewer_process_override": "", + "colorspace_override": { + "enabled": False, + "type": "colorspace", + "colorspace": "", + "display_view": { + "display": "", + "view": "" + } + }, "bake_viewer_process": True, "bake_viewer_input_process": True, "reformat_nodes_config": { From 35e180149b4e6a2a8bcc1aff61cd95b253cddfb8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 7 Jun 2024 18:19:18 +0200 Subject: [PATCH 028/101] Update descriptions for 'View' and 'Colorspace' settings. Add deprecation notice for 'Baking' setting in ImageIOSettings. - Update descriptions for 'View' and 'Colorspace' settings to include information about using Anatomy context tokens. - Add a deprecation notice for the 'Baking' setting in ImageIOSettings, recommending the use of 'Baking Target Colorspace'. --- server_addon/nuke/server/settings/common.py | 10 ++++++++-- server_addon/nuke/server/settings/imageio.py | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py index 50313fa5ef..54884ac828 100644 --- a/server_addon/nuke/server/settings/common.py +++ b/server_addon/nuke/server/settings/common.py @@ -153,7 +153,10 @@ class DisplayAndViewProfileModel(BaseSettingsModel): view: str = SettingsField( "", title="View", - description="What view to use", + description=( + "What view to use. Anatomy context tokens can " + "be used to dynamically set the value." + ), ) @@ -175,7 +178,10 @@ class ColorspaceConfigurationModel(BaseSettingsModel): colorspace: str = SettingsField( "", title="Colorspace", - description="What colorspace name to use", + description=( + "What colorspace name to use. Anatomy context tokens can " + "be used to dynamically set the value." + ), ) display_view: DisplayAndViewProfileModel = SettingsField( diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 93433f3f54..08686a08e8 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -207,8 +207,9 @@ class ImageIOSettings(BaseSettingsModel): baking: ViewProcessModel = SettingsField( default_factory=ViewProcessModel, title="Baking", - description="""Baking profile is used during - publishing baked colorspace data at knob viewerProcess""" + description="""[DEPRECATED - use 'Baking Target Colorspace' instead] + Baking profile is used during + publishing baked colorspace data at knob viewerProcess""", ) workfile: WorkfileColorspaceSettings = SettingsField( From d1214478781d689d5057f4d9e5a1e254989eaff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 7 Jun 2024 21:06:21 +0200 Subject: [PATCH 029/101] Pass status and author when integrating version --- client/ayon_core/plugins/publish/integrate.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 865b566e6e..89164255dc 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -380,29 +380,29 @@ class IntegrateAsset(pyblish.api.InstancePlugin): data = { "families": get_instance_families(instance) } - attribibutes = {} + attributes = {} product_group = instance.data.get("productGroup") if product_group: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group elif existing_product_entity: # Preserve previous product group if new version does not set it product_group = existing_product_entity.get("attrib", {}).get( "productGroup" ) if product_group is not None: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - + product_entity = new_product_entity( product_name, product_type, folder_entity["id"], data=data, - attribs=attribibutes, + attribs=attributes, entity_id=product_id ) @@ -464,6 +464,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): version_number, product_entity["id"], task_id=task_id, + author=instance.context.data.get("user"), + status=instance.data.get("status"), data=version_data, attribs=version_attributes, entity_id=version_id, From 43787fd9c39f16bc8bf75d401b038a5a2fb56988 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 8 Jun 2024 10:52:37 +0200 Subject: [PATCH 030/101] use AYON_USERNAME during publishing --- client/ayon_core/cli_commands.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 0fb18be687..c56ed16f1c 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -64,9 +64,10 @@ class Commands: get_global_context, ) - # Register target and host + import ayon_api import pyblish.util + # Register target and host if not isinstance(path, str): raise RuntimeError("Path to JSON must be a string.") @@ -86,6 +87,18 @@ class Commands: log = Logger.get_logger("CLI-publish") + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + try: + ayon_api.set_default_service_username(username) + except ValueError: + pass + install_ayon_plugins() manager = AddonsManager() From 3b73a8881dbd7fe66e35a7cd389635c60b1053fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jun 2024 10:48:29 +0200 Subject: [PATCH 031/101] working on settings patching --- server_addon/nuke/server/__init__.py | 18 ++++++++-- server_addon/nuke/server/settings/__init__.py | 3 ++ .../nuke/server/settings/conversion.py | 34 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 server_addon/nuke/server/settings/conversion.py diff --git a/server_addon/nuke/server/__init__.py b/server_addon/nuke/server/__init__.py index aeb5e36675..0806ea8e87 100644 --- a/server_addon/nuke/server/__init__.py +++ b/server_addon/nuke/server/__init__.py @@ -1,8 +1,12 @@ -from typing import Type +from typing import Type, Any from ayon_server.addons import BaseServerAddon -from .settings import NukeSettings, DEFAULT_VALUES +from .settings import ( + NukeSettings, + DEFAULT_VALUES, + convert_settings_overrides +) class NukeAddon(BaseServerAddon): @@ -11,3 +15,13 @@ class NukeAddon(BaseServerAddon): async def get_default_settings(self): settings_model_cls = self.get_settings_model() return settings_model_cls(**DEFAULT_VALUES) + + async def convert_settings_overrides( + self, + source_version: str, + overrides: dict[str, Any], + ) -> dict[str, Any]: + convert_settings_overrides(source_version, overrides) + # Use super conversion + return await super().convert_settings_overrides( + source_version, overrides) diff --git a/server_addon/nuke/server/settings/__init__.py b/server_addon/nuke/server/settings/__init__.py index 1e58865395..da79b947f7 100644 --- a/server_addon/nuke/server/settings/__init__.py +++ b/server_addon/nuke/server/settings/__init__.py @@ -2,9 +2,12 @@ from .main import ( NukeSettings, DEFAULT_VALUES, ) +from .conversion import convert_settings_overrides __all__ = ( "NukeSettings", "DEFAULT_VALUES", + + "convert_settings_overrides", ) diff --git a/server_addon/nuke/server/settings/conversion.py b/server_addon/nuke/server/settings/conversion.py new file mode 100644 index 0000000000..e88cee884d --- /dev/null +++ b/server_addon/nuke/server/settings/conversion.py @@ -0,0 +1,34 @@ +from typing import Any + +from .publish_plugins import DEFAULT_PUBLISH_VALUES + + +def _convert_imageio_configs_0_2_2(overrides): + """Image IO settings had changed. + + 0.2.2. is the latest version using the old way. + """ + pass + + +def _convert_extract_intermediate_files_0_2_2(publish_overrides): + """Extract intermediate files settings had changed. + + 0.2.2. is the latest version using the old way. + """ + pass + + +def _convert_publish_plugins(overrides): + if "publish" not in overrides: + return + _convert_extract_intermediate_files_0_2_2(overrides["publish"]) + + +def convert_settings_overrides( + source_version: str, + overrides: dict[str, Any], +) -> dict[str, Any]: + _convert_imageio_configs_0_2_2(overrides) + _convert_publish_plugins(overrides) + return overrides From b413a04cab65132aefe20cd9b56bf111265e497f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:27:25 +0200 Subject: [PATCH 032/101] skip import from hosts folder --- client/ayon_core/addon/base.py | 106 +++++++++++------------------ client/ayon_core/hosts/__init__.py | 0 2 files changed, 40 insertions(+), 66 deletions(-) delete mode 100644 client/ayon_core/hosts/__init__.py diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index b10629ede8..6d0dd94df0 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -411,82 +411,56 @@ def _load_addons_in_core( ): # Add current directory at first place # - has small differences in import logic - hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts") modules_dir = os.path.join(AYON_CORE_ROOT, "modules") + if not os.path.exists(modules_dir): + log.warning(( + "Could not find path when loading AYON addons \"{}\"" + ).format(modules_dir)) + return - for dirpath in {hosts_dir, modules_dir}: - if not os.path.exists(dirpath): - log.warning(( - "Could not find path when loading AYON addons \"{}\"" - ).format(dirpath)) + ignored_filenames = set(IGNORED_DEFAULT_FILENAMES) + + for filename in os.listdir(modules_dir): + # Ignore filenames + if filename in IGNORED_FILENAMES or filename in ignored_filenames: continue - is_in_modules_dir = dirpath == modules_dir - ignored_filenames = set() - if is_in_modules_dir: - ignored_filenames = set(IGNORED_DEFAULT_FILENAMES) + fullpath = os.path.join(modules_dir, filename) + basename, ext = os.path.splitext(filename) - for filename in os.listdir(dirpath): - # Ignore filenames - if filename in IGNORED_FILENAMES or filename in ignored_filenames: + if basename in ignore_addon_names: + continue + + # Validations + if os.path.isdir(fullpath): + # Check existence of init file + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + log.debug(( + "Addon directory does not contain __init__.py" + " file {}" + ).format(fullpath)) continue - fullpath = os.path.join(dirpath, filename) - basename, ext = os.path.splitext(filename) + elif ext not in (".py", ): + continue - if basename in ignore_addon_names: - continue + # TODO add more logic how to define if folder is addon or not + # - check manifest and content of manifest + try: + # Don't import dynamically current directory modules + new_import_str = "{}.{}".format(modules_key, basename) - # Validations - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Addon directory does not contain __init__.py" - " file {}" - ).format(fullpath)) - continue + import_str = "ayon_core.modules.{}".format(basename) + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) - elif ext not in (".py", ): - continue - - # TODO add more logic how to define if folder is addon or not - # - check manifest and content of manifest - try: - # Don't import dynamically current directory modules - new_import_str = "{}.{}".format(modules_key, basename) - if is_in_modules_dir: - import_str = "ayon_core.modules.{}".format(basename) - default_module = __import__(import_str, fromlist=("", )) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - else: - import_str = "ayon_core.hosts.{}".format(basename) - # Until all hosts are converted to be able use them as - # modules is this error check needed - try: - default_module = __import__( - import_str, fromlist=("", ) - ) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - except Exception: - log.warning( - "Failed to import host folder {}".format(basename), - exc_info=True - ) - - except Exception: - if is_in_modules_dir: - msg = "Failed to import in-core addon '{}'.".format( - basename - ) - else: - msg = "Failed to import addon '{}'.".format(fullpath) - log.error(msg, exc_info=True) + except Exception: + log.error( + f"Failed to import in-core addon '{basename}'.", + exc_info=True + ) def _load_addons(): diff --git a/client/ayon_core/hosts/__init__.py b/client/ayon_core/hosts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From df3670f30fc595fd14f5aab73ff8f17ec54ec4a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:27:32 +0200 Subject: [PATCH 033/101] use sets --- client/ayon_core/addon/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 6d0dd94df0..1f1e749865 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -28,16 +28,16 @@ from .interfaces import ( ) # Files that will be always ignored on addons import -IGNORED_FILENAMES = ( +IGNORED_FILENAMES = { "__pycache__", -) +} # Files ignored on addons import from "./ayon_core/modules" -IGNORED_DEFAULT_FILENAMES = ( +IGNORED_DEFAULT_FILENAMES = { "__init__.py", "base.py", "interfaces.py", "click_wrap.py", -) +} # When addon was moved from ayon-core codebase # - this is used to log the missing addon @@ -418,11 +418,11 @@ def _load_addons_in_core( ).format(modules_dir)) return - ignored_filenames = set(IGNORED_DEFAULT_FILENAMES) + ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES for filename in os.listdir(modules_dir): # Ignore filenames - if filename in IGNORED_FILENAMES or filename in ignored_filenames: + if filename in ignored_filenames: continue fullpath = os.path.join(modules_dir, filename) From cbac60598985697079f78f9b7edbc5b0141f4f23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:28:24 +0200 Subject: [PATCH 034/101] use f-strings --- client/ayon_core/addon/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 1f1e749865..878c50e039 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -438,8 +438,8 @@ def _load_addons_in_core( if not os.path.exists(init_path): log.debug(( "Addon directory does not contain __init__.py" - " file {}" - ).format(fullpath)) + f" file {fullpath}" + )) continue elif ext not in (".py", ): @@ -449,9 +449,9 @@ def _load_addons_in_core( # - check manifest and content of manifest try: # Don't import dynamically current directory modules - new_import_str = "{}.{}".format(modules_key, basename) + new_import_str = f"{modules_key}.{basename}" - import_str = "ayon_core.modules.{}".format(basename) + import_str = f"ayon_core.modules.{basename}" default_module = __import__(import_str, fromlist=("", )) sys.modules[new_import_str] = default_module setattr(openpype_modules, basename, default_module) From ef3068a4c205767e47398a225497dae13bc08a09 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:28:32 +0200 Subject: [PATCH 035/101] use direct comparison --- client/ayon_core/addon/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 878c50e039..8c5158a712 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -442,7 +442,7 @@ def _load_addons_in_core( )) continue - elif ext not in (".py", ): + elif ext != ".py": continue # TODO add more logic how to define if folder is addon or not From b14c041cf86b5bded6d4bbf26becbfe0456be166 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:37:52 +0200 Subject: [PATCH 036/101] more f-string --- client/ayon_core/addon/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 8c5158a712..b9ecff4233 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -413,9 +413,9 @@ def _load_addons_in_core( # - has small differences in import logic modules_dir = os.path.join(AYON_CORE_ROOT, "modules") if not os.path.exists(modules_dir): - log.warning(( - "Could not find path when loading AYON addons \"{}\"" - ).format(modules_dir)) + log.warning( + f"Could not find path when loading AYON addons \"{modules_dir}\"" + ) return ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES From c7a2cb95a1c37f16a0c262a479edc7c5a9636310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 10 Jun 2024 15:43:40 +0200 Subject: [PATCH 037/101] Update client/ayon_core/plugins/publish/integrate.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/integrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 89164255dc..229dea7ecd 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -396,7 +396,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - product_entity = new_product_entity( product_name, product_type, From f6e3dcb4742e083664744de016476d8c134f5ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 10 Jun 2024 17:34:41 +0200 Subject: [PATCH 038/101] Remove `author` kwarg from new_version_entity as it's being resolved at https://github.com/ynput/ayon-core/commit/43787fd9c39f16bc8bf75d401b038a5a2fb56988 now --- client/ayon_core/plugins/publish/integrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 229dea7ecd..1a4cda4dbb 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -463,7 +463,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): version_number, product_entity["id"], task_id=task_id, - author=instance.context.data.get("user"), status=instance.data.get("status"), data=version_data, attribs=version_attributes, From 5bffaa730efce006283b66462d52a543713fcce0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 17:27:25 +0800 Subject: [PATCH 039/101] fix the incorrect fps value when loading point cache --- server_addon/max/client/ayon_max/api/lib.py | 17 ++++++++++++----- .../ayon_max/plugins/load/load_pointcache.py | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index eb22dbafd2..8fce21311a 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -272,10 +272,8 @@ def reset_frame_range(fps: bool = True): scene frame rate in frames-per-second. """ if fps: - task_entity = get_current_task_entity() - task_attributes = task_entity["attrib"] - fps_number = float(task_attributes["fps"]) - rt.frameRate = fps_number + set_fps() + frame_range = get_frame_range() set_timeline( @@ -284,6 +282,15 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) +def set_fps(): + """Set fps to current folder + """ + task_entity = get_current_task_entity() + task_attributes = task_entity["attrib"] + fps_number = float(task_attributes["fps"]) + rt.frameRate = fps_number + + def reset_unit_scale(): """Apply the unit scale setting to 3dsMax """ @@ -358,7 +365,7 @@ def is_headless(): def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ - rt.animationRange = rt.interval(frameStart, frameEnd) + rt.animationRange = rt.interval(int(frameStart), int(frameEnd)) return rt.animationRange diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index d7def3d0ba..7f2d4c05f8 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, set_fps from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,6 +31,8 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) + set_fps() + abc_before = { c for c in rt.rootNode.Children From 7cb23851e1e986a9c0731305ba155c6d52638343 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:56:56 +0200 Subject: [PATCH 040/101] cleanup PYTHONPATH from existing addons on update --- client/ayon_core/tools/tray/tray.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/tray.py index eca87eb11d..c0b90dd764 100644 --- a/client/ayon_core/tools/tray/tray.py +++ b/client/ayon_core/tools/tray/tray.py @@ -182,7 +182,27 @@ class TrayManager: }: envs.pop(key, None) + # Remove any existing addon path from 'PYTHONPATH' + addons_dir = os.environ.get("AYON_ADDONS_DIR", "") + if addons_dir: + addons_dir = os.path.normpath(addons_dir) + addons_dir = addons_dir.lower() + + pythonpath = envs.get("PYTHONPATH") or "" + new_python_paths = [] + for path in pythonpath.split(os.pathsep): + if not path: + continue + path = os.path.normpath(path) + if path.lower().startswith(addons_dir): + continue + new_python_paths.append(path) + + envs["PYTHONPATH"] = os.pathsep.join(new_python_paths) + + # Start new process run_detached_process(args, env=envs) + # Exit current tray process self.exit() def exit(self): From f861dfa73d8a17c33e82c263f01341e410401b44 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:47:49 +0200 Subject: [PATCH 041/101] Fix refactor of `remove_unused_media_pool_items` location --- .../plugins/inventory/remove_unused_media_pool_items.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {client/ayon_core/hosts/resolve => server_addon/resolve/client/ayon_resolve}/plugins/inventory/remove_unused_media_pool_items.py (100%) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py similarity index 100% rename from client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py rename to server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py From d39acab7158387fcd42499c921ab92fd6fff9221 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:51:39 +0200 Subject: [PATCH 042/101] Fix usage of undefined variable --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c1016ce053..d5dbfa8934 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -280,7 +280,10 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) self._set_metadata(media_pool_item=item, context=context) - self._set_colorspace_from_representation(item, representation) + self._set_colorspace_from_representation( + item, + representation=context["representation"] + ) # If no specific colorspace is set then we want to preserve the # colorspace a user might have set before the clip replacement From 14781e795c317f534b29c384663a446a854c8b10 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:51:53 +0200 Subject: [PATCH 043/101] Cosmetics --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index d5dbfa8934..114e8dca79 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -62,15 +62,18 @@ def set_colorspace(media_pool_item, colorspace, mode="davinciYRGBColorManagedv2"): """Set MediaPoolItem colorspace. + This implements a workaround that you cannot set the input colorspace unless the Resolve project's color science mode is set to `davinciYRGBColorManagedv2`. + Args: media_pool_item (MediaPoolItem): The media pool item. colorspace (str): The colorspace to apply. mode (Optional[str]): The Resolve project color science mode to be in while setting the colorspace. Defaults to 'davinciYRGBColorManagedv2' + Returns: bool: Whether applying the colorspace succeeded. """ From 7a72ecb7fbbe024dca908bb1cfec7284964133ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:53:57 +0200 Subject: [PATCH 044/101] Fix refactor of imports --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index 114e8dca79..6d42061eb0 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -12,13 +12,13 @@ from ayon_core.pipeline import ( get_representation_path, registered_host ) -from ayon_core.hosts.resolve.api import lib -from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) from ayon_core.lib import BoolDef +from ayon_resolve.api import lib +from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID class MetadataEntry(TypedDict): From a51b2b4e3e17bc80c4f66e8177f24b158841a140 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:56:13 +0200 Subject: [PATCH 045/101] Add selected to label --- .../plugins/inventory/remove_unused_media_pool_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py index 6698508d24..7ea55dc1ff 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py @@ -6,7 +6,7 @@ from ayon_core.pipeline.load.utils import remove_container class RemoveUnusedMedia(InventoryAction): - label = "Remove Unused Media" + label = "Remove Unused Selected Media" icon = "trash" @staticmethod From e3f8ff9de35ad5ec37b6981c2f7fd3710acd6356 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 19:58:08 +0800 Subject: [PATCH 046/101] set scene fps during loading point cache context --- server_addon/max/client/ayon_max/api/lib.py | 55 ++++++++++-- .../ayon_max/plugins/load/load_pointcache.py | 85 ++++++++++--------- 2 files changed, 92 insertions(+), 48 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 8fce21311a..eb29af5ec5 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -7,8 +7,11 @@ from typing import Any, Dict, Union import six +import ayon_api + from ayon_core.pipeline import ( get_current_project_name, + get_current_folder_path, colorspace ) from ayon_core.settings import get_project_settings @@ -272,7 +275,7 @@ def reset_frame_range(fps: bool = True): scene frame rate in frames-per-second. """ if fps: - set_fps() + rt.frameRate = float(get_fps_for_current_context()) frame_range = get_frame_range() @@ -282,13 +285,37 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) -def set_fps(): - """Set fps to current folder +def get_fps_for_current_context(): + """Get fps that should be set for current context. + + Todos: + - Skip project value. + - Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> + all the values in the functions can be collected at one place as + they have same requirements. + + Returns: + Union[int, float]: FPS value. """ - task_entity = get_current_task_entity() - task_attributes = task_entity["attrib"] - fps_number = float(task_attributes["fps"]) - rt.frameRate = fps_number + task_entity = get_current_task_entity(fields={"attrib"}) + fps = task_entity.get("attrib", {}).get("fps") + if not fps: + project_name = get_current_project_name() + folder_path = get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"attrib.fps"} + ) or {} + + fps = folder_entity.get("attrib", {}).get("fps") + if not fps: + project_entity = ayon_api.get_project( + project_name, fields=["attrib.fps"] + ) or {} + fps = project_entity.get("attrib", {}).get("fps") + + if not fps: + fps = 25 + return fps def reset_unit_scale(): @@ -580,3 +607,17 @@ def suspended_refresh(): finally: rt.enableSceneRedraw() rt.resumeEditing() + + +@contextlib.contextmanager +def scene_fps(fps): + """Set scene fps during context + + Args: + fps (float): fps value + """ + try: + rt.frameRate = float(get_fps_for_current_context()) + yield + finally: + rt.frameRate = fps diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 7f2d4c05f8..f862f2ed67 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, set_fps +from ayon_max.api.lib import unique_namespace, scene_fps from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,47 +31,50 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - set_fps() - - abc_before = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } - - rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) - - abc_after = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } - - # This should yield new AlembicContainer node - abc_containers = abc_after.difference(abc_before) - - if len(abc_containers) != 1: - self.log.error("Something failed when loading.") - - abc_container = abc_containers.pop() - selections = rt.GetCurrentSelection() - for abc in selections: - for cam_shape in abc.Children: - cam_shape.playbackType = 0 - - namespace = unique_namespace( - name + "_", - suffix="_", - ) abc_objects = [] - for abc_object in abc_container.Children: - abc_object.name = f"{namespace}:{abc_object.name}" - abc_objects.append(abc_object) - # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" - abc_container.name = abc_container_name - abc_objects.append(abc_container) + product_fps = float(context["version"]["attrib"].get("fps")) + if product_fps is None: + # Just stick to current scene FPS + product_fps = float(rt.frameRate) + with scene_fps(product_fps): + abc_before = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + rt.AlembicImport.ImportToRoot = False + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) + + abc_after = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) + + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") + + abc_container = abc_containers.pop() + selections = rt.GetCurrentSelection() + for abc in selections: + for cam_shape in abc.Children: + cam_shape.playbackType = 0 + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + abc_objects.append(abc_object) + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + abc_objects.append(abc_container) return containerise( name, abc_objects, context, From a0ba192306429ce64921869c1161ce8593be536c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:05:07 +0200 Subject: [PATCH 047/101] Rename resolve paths and update imports for media loading - Renamed resolve paths for media pool items - Updated imports for media loading functionality --- .../plugins/inventory/remove_unused_media_pool_items.py | 0 .../resolve/client/ayon_resolve/plugins/load/load_media.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename {client/ayon_core/hosts/resolve => server_addon/resolve/client/ayon_resolve}/plugins/inventory/remove_unused_media_pool_items.py (100%) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py similarity index 100% rename from client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py rename to server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c1016ce053..dea0431747 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -12,8 +12,8 @@ from ayon_core.pipeline import ( get_representation_path, registered_host ) -from ayon_core.hosts.resolve.api import lib -from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from ayon_resolve.api import lib +from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS From d7aa02c3e681474d16a1f245a0116c7a83706042 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:11:06 +0800 Subject: [PATCH 048/101] code tweaks - big roy's comment --- .../ayon_max/plugins/load/load_pointcache.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index f862f2ed67..498c5c6ce7 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,50 +31,52 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - abc_objects = [] + abc_before = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + rt.AlembicImport.ImportToRoot = False product_fps = float(context["version"]["attrib"].get("fps")) if product_fps is None: # Just stick to current scene FPS product_fps = float(rt.frameRate) with scene_fps(product_fps): - abc_before = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } + rt.importFile(file_path, + rt.name("noPrompt"), + using=rt.AlembicImport) - rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) + abc_after = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } - abc_after = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) - # This should yield new AlembicContainer node - abc_containers = abc_after.difference(abc_before) + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") - if len(abc_containers) != 1: - self.log.error("Something failed when loading.") + abc_container = abc_containers.pop() + selections = rt.GetCurrentSelection() + for abc in selections: + for cam_shape in abc.Children: + cam_shape.playbackType = 0 - abc_container = abc_containers.pop() - selections = rt.GetCurrentSelection() - for abc in selections: - for cam_shape in abc.Children: - cam_shape.playbackType = 0 - - namespace = unique_namespace( - name + "_", - suffix="_", - ) - for abc_object in abc_container.Children: - abc_object.name = f"{namespace}:{abc_object.name}" - abc_objects.append(abc_object) - # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" - abc_container.name = abc_container_name - abc_objects.append(abc_container) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + abc_objects = [] + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + abc_objects.append(abc_object) + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + abc_objects.append(abc_container) return containerise( name, abc_objects, context, From ee5d82994db8eca9c67a9b2923ebb28bf41831b8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:11:28 +0200 Subject: [PATCH 049/101] Remove subproject integration for Unreal Engine. Deleted unused subproject integration for Unreal Engine. --- client/ayon_core/hosts/unreal/integration | 1 - 1 file changed, 1 deletion(-) delete mode 160000 client/ayon_core/hosts/unreal/integration diff --git a/client/ayon_core/hosts/unreal/integration b/client/ayon_core/hosts/unreal/integration deleted file mode 160000 index 04b35dbf5f..0000000000 --- a/client/ayon_core/hosts/unreal/integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04b35dbf5fc42d905281fc30d3a22b139c1855e5 From 253ae11736fe75d5ae4ccb01a86605a35db2cea7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:31:44 +0800 Subject: [PATCH 050/101] code tweaks - big roy's comment --- server_addon/max/client/ayon_max/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index eb29af5ec5..03310ff16f 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -616,8 +616,9 @@ def scene_fps(fps): Args: fps (float): fps value """ + fps_before = rt.frameRate try: - rt.frameRate = float(get_fps_for_current_context()) + rt.frameRate = float(fps) yield finally: - rt.frameRate = fps + rt.frameRate = fps_before From 9e4d1f19ebebcdc35f7e7ae1c1100c281e2042c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:32:25 +0200 Subject: [PATCH 051/101] Update version to 0.2.2 in package.py Bumped up the version number from 0.2.1 to 0.2.2 in the package.py file for DaVinci Resolve addon. --- server_addon/resolve/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/package.py b/server_addon/resolve/package.py index 47b6c9a8b6..643e497253 100644 --- a/server_addon/resolve/package.py +++ b/server_addon/resolve/package.py @@ -1,6 +1,6 @@ name = "resolve" title = "DaVinci Resolve" -version = "0.2.1" +version = "0.2.2" client_dir = "ayon_resolve" From 92d49139a5c565ffa1f17314ff13ab0e5fbf7405 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:51:13 +0800 Subject: [PATCH 052/101] make sure the timeline and custom frame range matched to the export --- server_addon/max/client/ayon_max/api/lib.py | 15 --------------- .../ayon_max/plugins/load/load_pointcache.py | 9 ++++----- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 03310ff16f..7c140be72e 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -607,18 +607,3 @@ def suspended_refresh(): finally: rt.enableSceneRedraw() rt.resumeEditing() - - -@contextlib.contextmanager -def scene_fps(fps): - """Set scene fps during context - - Args: - fps (float): fps value - """ - fps_before = rt.frameRate - try: - rt.frameRate = float(fps) - yield - finally: - rt.frameRate = fps_before diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 498c5c6ce7..3eb4ba3323 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, scene_fps +from ayon_max.api.lib import unique_namespace from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -42,10 +42,9 @@ class AbcLoader(load.LoaderPlugin): if product_fps is None: # Just stick to current scene FPS product_fps = float(rt.frameRate) - with scene_fps(product_fps): - rt.importFile(file_path, - rt.name("noPrompt"), - using=rt.AlembicImport) + # TODO: remove after the post-system fps setup + rt.frameRange = product_fps + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c From 21c86742d4de1abd928d373f3634b58cdf1fae98 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:55:00 +0200 Subject: [PATCH 053/101] Bump addon version to 0.2.2 Update the addon version from 0.2.1 to 0.2.2 in the codebase. --- server_addon/resolve/client/ayon_resolve/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/version.py b/server_addon/resolve/client/ayon_resolve/version.py index 53e8882ed7..585f44b5a5 100644 --- a/server_addon/resolve/client/ayon_resolve/version.py +++ b/server_addon/resolve/client/ayon_resolve/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'resolve' version.""" -__version__ = "0.2.1" +__version__ = "0.2.2" From ba90adbfba75c407b39fbf7b2ca6da47c56d2fee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 15:16:33 +0200 Subject: [PATCH 054/101] Only report errors for not found addons if no valid farm addon found --- .../plugins/publish/collect_farm_target.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_target.py b/client/ayon_core/plugins/publish/collect_farm_target.py index 3bf89450ec..bd7dfdcd46 100644 --- a/client/ayon_core/plugins/publish/collect_farm_target.py +++ b/client/ayon_core/plugins/publish/collect_farm_target.py @@ -14,22 +14,20 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): if not instance.data.get("farm"): return - context = instance.context + addons_manager = instance.context.data.get("ayonAddonsManager") - farm_name = "" - addons_manager = context.data.get("ayonAddonsManager") - - for farm_renderer in ["deadline", "royalrender"]: + farm_renderer_addons = ["deadline", "royalrender"] + for farm_renderer in farm_renderer_addons: addon = addons_manager.get(farm_renderer, False) - - if not addon: - self.log.error("Cannot find AYON addon '{0}'.".format( - farm_renderer)) - elif addon.enabled: + if addon.enabled: farm_name = farm_renderer - - if farm_name: - self.log.debug("Collected render target: {0}".format(farm_name)) - instance.data["toBeRenderedOn"] = farm_name + break else: + # No enabled farm render addon found, then report all farm + # addons that were searched for yet not found + for farm_renderer in farm_renderer_addons: + self.log.error(f"Cannot find AYON addon '{farm_renderer}'.") AssertionError("No AYON renderer addon found") + + self.log.debug("Collected render target: {0}".format(farm_name)) + instance.data["toBeRenderedOn"] = farm_name From a0ce95f935a06cafa97cbdbd2d619e023a4e697c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 15:21:20 +0200 Subject: [PATCH 055/101] Actually raise an error instead of just creating an `AssertionError` instance and doing nothing with it --- client/ayon_core/plugins/publish/collect_farm_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_target.py b/client/ayon_core/plugins/publish/collect_farm_target.py index bd7dfdcd46..708f1b45a4 100644 --- a/client/ayon_core/plugins/publish/collect_farm_target.py +++ b/client/ayon_core/plugins/publish/collect_farm_target.py @@ -27,7 +27,7 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): # addons that were searched for yet not found for farm_renderer in farm_renderer_addons: self.log.error(f"Cannot find AYON addon '{farm_renderer}'.") - AssertionError("No AYON renderer addon found") + raise RuntimeError("No AYON renderer addon found.") self.log.debug("Collected render target: {0}".format(farm_name)) instance.data["toBeRenderedOn"] = farm_name From 3b5791a8e08c8b9831b2ad95a2511006fcf3478c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 15:27:28 +0200 Subject: [PATCH 056/101] Fix logic --- client/ayon_core/plugins/publish/collect_farm_target.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_target.py b/client/ayon_core/plugins/publish/collect_farm_target.py index 708f1b45a4..e0edd795d8 100644 --- a/client/ayon_core/plugins/publish/collect_farm_target.py +++ b/client/ayon_core/plugins/publish/collect_farm_target.py @@ -18,8 +18,8 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): farm_renderer_addons = ["deadline", "royalrender"] for farm_renderer in farm_renderer_addons: - addon = addons_manager.get(farm_renderer, False) - if addon.enabled: + addon = addons_manager.get(farm_renderer) + if addon and addon.enabled: farm_name = farm_renderer break else: From 8a5a078c29eaefbd08358d7f7b3bea41cb0cb0e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:50:37 +0200 Subject: [PATCH 057/101] use 'get_server_api_connection' to get connection --- client/ayon_core/cli_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index c56ed16f1c..ca0693db2f 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -94,8 +94,9 @@ class Commands: # NOTE: ayon-python-api does not have public api function to find # out if is used service user. So we need to have try > except # block. + con = ayon_api.get_server_api_connection() try: - ayon_api.set_default_service_username(username) + con.set_default_service_username(username) except ValueError: pass From 3cb6b869e3483d44f30f0ae407c621d62c1092ae Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 23:07:36 +0800 Subject: [PATCH 058/101] fix scene inventory not shown up --- .../max/client/ayon_max/api/pipeline.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/pipeline.py b/server_addon/max/client/ayon_max/api/pipeline.py index 5f5e896e86..324de9fba1 100644 --- a/server_addon/max/client/ayon_max/api/pipeline.py +++ b/server_addon/max/client/ayon_max/api/pipeline.py @@ -145,7 +145,27 @@ attributes "OpenPypeContext" rt.saveMaxFile(dst_path) -def ls() -> list: +def parse_container(container): + """Return the container node's full container data. + + Args: + container (str): A container node name. + + Returns: + dict: The container schema data for this container node. + + """ + data = lib.read(container) + + # Backwards compatibility pre-schemas for containers + data["schema"] = data.get("schema", "openpype:container-1.0") + + # Append transient data + data["objectName"] = container.Name + return data + + +def ls(): """Get all AYON containers.""" objs = rt.objects containers = [ @@ -156,7 +176,7 @@ def ls() -> list: ] for container in sorted(containers, key=attrgetter("name")): - yield lib.read(container) + yield parse_container(container) def on_new(): From 6d42703cf4d47d43c32027c2c4d5ca88b35e4f98 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Tue, 11 Jun 2024 23:11:03 +0800 Subject: [PATCH 059/101] Update server_addon/max/client/ayon_max/api/pipeline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/max/client/ayon_max/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/api/pipeline.py b/server_addon/max/client/ayon_max/api/pipeline.py index 324de9fba1..a87cd657ce 100644 --- a/server_addon/max/client/ayon_max/api/pipeline.py +++ b/server_addon/max/client/ayon_max/api/pipeline.py @@ -158,7 +158,7 @@ def parse_container(container): data = lib.read(container) # Backwards compatibility pre-schemas for containers - data["schema"] = data.get("schema", "openpype:container-1.0") + data["schema"] = data.get("schema", "openpype:container-3.0") # Append transient data data["objectName"] = container.Name From 7b1c4a8a080114e299dfe26ff52ccf63776edfcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 11:17:29 +0200 Subject: [PATCH 060/101] Refactor Resolve media loading logic for single files and sequences. - Refactored file path handling for importing media. - Updated method to determine if the file is a sequence. - Improved handling of frame numbers in file paths. --- .../ayon_resolve/plugins/load/load_media.py | 103 +++++++++++++----- 1 file changed, 75 insertions(+), 28 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index 6d42061eb0..c6f8b97ada 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -1,17 +1,19 @@ import json -import copy import contextlib +from pathlib import Path from collections import defaultdict -from typing import Union, List, Optional, TypedDict +from typing import Union, List, Optional, TypedDict, Tuple from ayon_api import version_is_latest from ayon_core.lib import StringTemplate from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( + Anatomy, LoaderPlugin, get_representation_path, registered_host ) +from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -21,6 +23,9 @@ from ayon_resolve.api import lib from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID +FRAME_SPLITTER = "__frame_splitter__" + + class MetadataEntry(TypedDict): """Metadata entry is dict with {"name": "key", "value: "value"}""" name: str @@ -188,6 +193,7 @@ class LoadMedia(LoaderPlugin): self.timeline = lib.get_current_timeline() representation = context["representation"] + self._project_name = context["project"]["name"] project = lib.get_current_project() media_pool = project.GetMediaPool() @@ -221,10 +227,17 @@ class LoadMedia(LoaderPlugin): media_pool.SetCurrentFolder(folder) # Import media - path = self._get_filepath(context) - items = media_pool.ImportMedia([path]) + # Resolve API: ImportMedia function requires a list of dictionaries + # with keys "FilePath", "StartIndex" and "EndIndex" for sequences + # but only string with absolute path for single files. + is_sequence, file_info = self._get_file_info(context) + if is_sequence: + items = media_pool.ImportMedia([file_info]) + else: + items = media_pool.ImportMedia([file_info["FilePath"]]) assert len(items) == 1, "Must import only one media item" + item = items[0] self._set_metadata(item, context) @@ -384,40 +397,74 @@ class LoadMedia(LoaderPlugin): value_formatted = StringTemplate(value).format_strict(context) media_pool_item.SetClipProperty(clip_property, value_formatted) - def _get_filepath(self, context: dict) -> Union[str, dict]: + def _get_file_info(self, context: dict) -> Tuple[bool, Union[str, dict]]: + """Return file info for Resolve ImportMedia. + + Args: + context (dict): The context dictionary. + + Returns: + Tuple[bool, Union[str, dict]]: A tuple of whether the file is a + sequence and the file info dictionary. + """ representation = context["representation"] - is_sequence = bool(representation["context"].get("frame")) - if not is_sequence: - return get_representation_path(representation) + anatomy = Anatomy(self._project_name) - version = context["version"] + # Get path to representation with correct frame number + repre_path = get_representation_path_with_anatomy( + representation, anatomy) - # Get the start and end frame of the image sequence, incl. handles - frame_start = version["data"].get("frameStart", 0) - frame_end = version["data"].get("frameEnd", 0) - handle_start = version["data"].get("handleStart", 0) - handle_end = version["data"].get("handleEnd", 0) - frame_start_handle = frame_start - handle_start - frame_end_handle = frame_end + handle_end - padding = len(representation["context"].get("frame")) + first_frame = representation["context"].get("frame") - # We format the frame number to the required token. To do so - # we in-place change the representation context data to format the path - # with that replaced data - representation = copy.deepcopy(representation) - representation["context"]["frame"] = f"%0{padding}d" - path = get_representation_path(representation) + is_sequence = False + # is not sequence + if first_frame is None: + return ( + is_sequence, {"FilePath": repre_path} + ) + + # This is sequence + is_sequence = True + repre_files = [ + file["path"].format(root=anatomy.roots) + for file in representation["files"] + ] + + # Change frame in representation context to get path with frame + # splitter. + representation["context"]["frame"] = FRAME_SPLITTER + frame_repre_path = get_representation_path_with_anatomy( + representation, anatomy + ) + frame_repre_path = Path(frame_repre_path) + repre_dir, repre_filename = ( + frame_repre_path.parent, frame_repre_path.name) + # Get sequence prefix and suffix + file_prefix, file_suffix = repre_filename.split(FRAME_SPLITTER) + # Get frame number from path as string to get frame padding + frame_str = str(repre_path)[len(file_prefix):][:len(file_suffix)] + frame_padding = len(frame_str) + + file_name = f"{file_prefix}%0{frame_padding}d{file_suffix}" + + abs_filepath = Path(repre_dir, file_name) + + start_index = int(first_frame) + end_index = int(int(first_frame) + len(repre_files) - 1) # See Resolve API, to import for example clip "file_[001-100].dpx": # ImportMedia([{"FilePath":"file_%03d.dpx", # "StartIndex":1, # "EndIndex":100}]) - return { - "FilePath": path, - "StartIndex": frame_start_handle, - "EndIndex": frame_end_handle - } + return ( + is_sequence, + { + "FilePath": abs_filepath.as_posix(), + "StartIndex": start_index, + "EndIndex": end_index, + } + ) def _get_colorspace(self, representation: dict) -> Optional[str]: """Return Resolve native colorspace from OCIO colorspace data. From 31b614d71d7533f5dacbc7b3001cc01474ee17bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:20:36 +0200 Subject: [PATCH 061/101] fixed implementation of users model --- client/ayon_core/tools/common_models/users.py | 108 +++++++++++++++--- client/ayon_core/tools/workfiles/control.py | 3 +- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index f8beb31aa1..babb690f87 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -1,6 +1,80 @@ -import ayon_api +import json +import collections -from ayon_core.lib import CacheItem +import ayon_api +from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict + +from ayon_core.lib import NestedCacheItem + + +# --- Implementation that should be in ayon-python-api --- +# The implementation is not available in all versions of ayon-python-api. +def users_graphql_query(fields): + query = GraphQlQuery("Users") + names_var = query.add_variable("userNames", "[String!]") + project_name_var = query.add_variable("projectName", "String!") + + users_field = query.add_field_with_edges("users") + users_field.set_filter("names", names_var) + users_field.set_filter("projectName", project_name_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, users_field)) + + while query_queue: + item = query_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + query_queue.append((k, v, field)) + return query + + +def get_users(project_name=None, usernames=None, fields=None): + """Get Users. + + Only administrators and managers can fetch all users. For other users + it is required to pass in 'project_name' filter. + + Args: + project_name (Optional[str]): Project name. + usernames (Optional[Iterable[str]]): Filter by usernames. + fields (Optional[Iterable[str]]): Fields to be queried + for users. + + Returns: + Generator[dict[str, Any]]: Queried users. + + """ + filters = {} + if usernames is not None: + usernames = set(usernames) + if not usernames: + return + filters["userNames"] = list(usernames) + + if project_name is not None: + filters["projectName"] = project_name + + con = ayon_api.get_server_api_connection() + if not fields: + fields = con.get_default_fields_for_type("user") + + query = users_graphql_query(set(fields)) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(con): + for user in parsed_data["users"]: + user["accessGroups"] = json.loads(user["accessGroups"]) + yield user +# --- END of ayon-python-api implementation --- class UserItem: @@ -32,19 +106,19 @@ class UserItem: class UsersModel: def __init__(self, controller): self._controller = controller - self._users_cache = CacheItem(default_factory=list) + self._users_cache = NestedCacheItem(default_factory=list) - def get_user_items(self): + def get_user_items(self, project_name): """Get user items. Returns: List[UserItem]: List of user items. """ - self._invalidate_cache() - return self._users_cache.get_data() + self._invalidate_cache(project_name) + return self._users_cache[project_name].get_data() - def get_user_items_by_name(self): + def get_user_items_by_name(self, project_name): """Get user items by name. Implemented as most of cases using this model will need to find @@ -56,10 +130,10 @@ class UsersModel: """ return { user_item.username: user_item - for user_item in self.get_user_items() + for user_item in self.get_user_items(project_name) } - def get_user_item_by_username(self, username): + def get_user_item_by_username(self, project_name, username): """Get user item by username. Args: @@ -70,15 +144,21 @@ class UsersModel: """ self._invalidate_cache() - for user_item in self.get_user_items(): + for user_item in self.get_user_items(project_name): if user_item.username == username: return user_item return None - def _invalidate_cache(self): - if self._users_cache.is_valid: + def _invalidate_cache(self, project_name): + cache = self._users_cache[project_name] + if cache.is_valid: return - self._users_cache.update_data([ + + if project_name is None: + cache.update_data([]) + return + + self._users_cache[project_name].update_data([ UserItem.from_entity_data(user) - for user in ayon_api.get_users() + for user in get_users(project_name) ]) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 8fa9135bc0..bf48894654 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -278,7 +278,8 @@ class BaseWorkfileController( ) def get_user_items_by_name(self): - return self._users_model.get_user_items_by_name() + project_name = self.get_current_project_name() + return self._users_model.get_user_items_by_name(project_name) # Host information def get_workfile_extensions(self): From c241546842d5c9278fa7cf5f6ac71805311d41b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 11:31:54 +0200 Subject: [PATCH 062/101] Refactor media import process in LoadMedia plugin Extracted media import logic to a separate method for clarity. The new method handles importing media to Resolve Media Pool and creating bins if necessary. --- .../ayon_resolve/plugins/load/load_media.py | 101 +++++++++++------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c6f8b97ada..d7d4f33266 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -20,6 +20,7 @@ from ayon_core.lib.transcoding import ( ) from ayon_core.lib import BoolDef from ayon_resolve.api import lib +from ayon_resolve.api import bmdvr from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID @@ -214,46 +215,7 @@ class LoadMedia(LoaderPlugin): item = container["_item"] if item is None: - # Create or set the bin folder, we add it in there - # If bin path is not set we just add into the current active bin - if self.media_pool_bin_path: - media_pool_bin_path = StringTemplate( - self.media_pool_bin_path).format_strict(context) - folder = lib.create_bin( - name=media_pool_bin_path, - root=media_pool.GetRootFolder(), - set_as_current=False - ) - media_pool.SetCurrentFolder(folder) - - # Import media - # Resolve API: ImportMedia function requires a list of dictionaries - # with keys "FilePath", "StartIndex" and "EndIndex" for sequences - # but only string with absolute path for single files. - is_sequence, file_info = self._get_file_info(context) - if is_sequence: - items = media_pool.ImportMedia([file_info]) - else: - items = media_pool.ImportMedia([file_info["FilePath"]]) - - assert len(items) == 1, "Must import only one media item" - - item = items[0] - - self._set_metadata(item, context) - self._set_colorspace_from_representation(item, representation) - - data = self._get_container_data(context) - - # Add containerise data only needed on first load - data.update({ - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "loader": str(self.__class__.__name__), - }) - - item.SetMetadata(lib.pype_tag_name, json.dumps(data)) - + item = self._import_media_to_bin(context, media_pool, representation) # Always update clip color - even if re-using existing clip color = self.get_item_color(context) item.SetClipColor(color) @@ -267,6 +229,65 @@ class LoadMedia(LoaderPlugin): timeline=timeline ) + def _import_media_to_bin( + self, context, media_pool, representation + ): + """Import media to Resolve Media Pool. + + Also create a bin if `media_pool_bin_path` is set. + + Args: + context (dict): The context dictionary. + media_pool (resolve.MediaPool): The Resolve Media Pool. + representation (dict): The representation data. + + Returns: + resolve.MediaPoolItem: The imported media pool item. + """ + # Create or set the bin folder, we add it in there + # If bin path is not set we just add into the current active bin + if self.media_pool_bin_path: + media_pool_bin_path = StringTemplate( + self.media_pool_bin_path).format_strict(context) + + folder = lib.create_bin( + # double slashes will create unconnected folders + name=media_pool_bin_path.replace("//", "/"), + root=media_pool.GetRootFolder(), + set_as_current=False + ) + media_pool.SetCurrentFolder(folder) + + # Import media + # Resolve API: ImportMedia function requires a list of dictionaries + # with keys "FilePath", "StartIndex" and "EndIndex" for sequences + # but only string with absolute path for single files. + is_sequence, file_info = self._get_file_info(context) + items = ( + media_pool.ImportMedia([file_info]) + if is_sequence + else media_pool.ImportMedia([file_info["FilePath"]]) + ) + assert len(items) == 1, "Must import only one media item" + + result = items[0] + + self._set_metadata(result, context) + self._set_colorspace_from_representation(result, representation) + + data = self._get_container_data(context) + + # Add containerise data only needed on first load + data.update({ + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "loader": str(self.__class__.__name__), + }) + + result.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + return result + def switch(self, container, context): self.update(container, context) From 728e3d6a7301b046a9e105b18f3f9496c46dad92 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jun 2024 18:50:20 +0800 Subject: [PATCH 063/101] update get_fps_for_current_context() function --- server_addon/max/client/ayon_max/api/lib.py | 19 +------------------ .../ayon_max/plugins/load/load_pointcache.py | 4 +++- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 7c140be72e..4b5b07880b 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -298,24 +298,7 @@ def get_fps_for_current_context(): Union[int, float]: FPS value. """ task_entity = get_current_task_entity(fields={"attrib"}) - fps = task_entity.get("attrib", {}).get("fps") - if not fps: - project_name = get_current_project_name() - folder_path = get_current_folder_path() - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path, fields={"attrib.fps"} - ) or {} - - fps = folder_entity.get("attrib", {}).get("fps") - if not fps: - project_entity = ayon_api.get_project( - project_name, fields=["attrib.fps"] - ) or {} - fps = project_entity.get("attrib", {}).get("fps") - - if not fps: - fps = 25 - return fps + return task_entity["attrib"]["fps"] def reset_unit_scale(): diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 3eb4ba3323..8ab25f2eeb 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, get_fps_for_current_context from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,6 +31,8 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) + rt.frameRate = get_fps_for_current_context() + abc_before = { c for c in rt.rootNode.Children From 80293ae26442a84692ed6f48b62f530dbbc7cc43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jun 2024 18:55:08 +0800 Subject: [PATCH 064/101] ruff cosmetic fix --- server_addon/max/client/ayon_max/api/lib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 4b5b07880b..7acc18196f 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -7,11 +7,8 @@ from typing import Any, Dict, Union import six -import ayon_api - from ayon_core.pipeline import ( get_current_project_name, - get_current_folder_path, colorspace ) from ayon_core.settings import get_project_settings From 8bff418a0937979c20442cb57ee5fa3f82a2c822 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:21:39 +0800 Subject: [PATCH 065/101] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- server_addon/max/client/ayon_max/plugins/load/load_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 8ab25f2eeb..cb5aaca2fd 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,7 +31,6 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - rt.frameRate = get_fps_for_current_context() abc_before = { c From 6e6d3c02f603331560586430b4b180e3bb46b8f3 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:22:54 +0800 Subject: [PATCH 066/101] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- server_addon/max/client/ayon_max/plugins/load/load_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index cb5aaca2fd..cb1ffaa430 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,7 +31,6 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - abc_before = { c for c in rt.rootNode.Children From e8107e2b971142626ebaf3ddc047a7f67db0094a Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:23:01 +0800 Subject: [PATCH 067/101] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- .../max/client/ayon_max/plugins/load/load_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index cb1ffaa430..3eb4ba3323 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, get_fps_for_current_context +from ayon_max.api.lib import unique_namespace from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, From da1bb3fad53f3dbe020e1294a6f115618ea1eb2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:28:20 +0200 Subject: [PATCH 068/101] pointcache does not append attr definitions to base list but to new list --- .../plugins/create/create_animation_pointcache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index ea4cdb57fe..521dc1a681 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -99,8 +99,8 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - defs = super(CreateAnimation, self).get_instance_attr_defs() - defs += _get_animation_attr_defs(self) + defs = list(super().get_instance_attr_defs()) + defs.extend(_get_animation_attr_defs(self)) return defs @@ -123,8 +123,8 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - defs = super(CreatePointCache, self).get_instance_attr_defs() - defs += _get_animation_attr_defs(self) + defs = list(super().get_instance_attr_defs()) + defs.extend(_get_animation_attr_defs(self)) return defs def create(self, product_name, instance_data, pre_create_data): From dc5c55bb1abaace10704b31f4d85a6fef970a30b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:31:46 +0200 Subject: [PATCH 069/101] hero version is smaller than standard version --- client/ayon_core/tools/loader/abstract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index a1c1e6a062..ba1dcb73b6 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -177,7 +177,7 @@ class VersionItem: other_version = abs(other.version) # Hero version is greater than non-hero if version == other_version: - return self.is_hero + return not self.is_hero return version > other_version def __lt__(self, other): @@ -188,7 +188,7 @@ class VersionItem: other_version = abs(other.version) # Non-hero version is lesser than hero if version == other_version: - return not self.is_hero + return self.is_hero return version < other_version def __ge__(self, other): From 820bb7318e51248237b0352e63c79e869faabf89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:48:35 +0200 Subject: [PATCH 070/101] don't call 'super' in 'get_instance_attr_defs' --- .../plugins/create/create_animation_pointcache.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index 521dc1a681..aa9b61b46e 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -99,9 +99,7 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - defs = list(super().get_instance_attr_defs()) - defs.extend(_get_animation_attr_defs(self)) - return defs + return _get_animation_attr_defs(self) class CreatePointCache(plugin.MayaCreator): @@ -123,9 +121,7 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - defs = list(super().get_instance_attr_defs()) - defs.extend(_get_animation_attr_defs(self)) - return defs + return _get_animation_attr_defs(self) def create(self, product_name, instance_data, pre_create_data): instance = super(CreatePointCache, self).create( From 5a74d5c986084ddd7537648b9e6784295ed901d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:48:52 +0200 Subject: [PATCH 071/101] do not pass self to the function --- .../ayon_maya/plugins/create/create_animation_pointcache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index aa9b61b46e..d98b0dd5fa 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -8,7 +8,7 @@ from ayon_core.lib import ( ) -def _get_animation_attr_defs(cls): +def _get_animation_attr_defs(): """Get Animation generic definitions.""" defs = lib.collect_animation_defs() defs.extend( @@ -99,7 +99,7 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - return _get_animation_attr_defs(self) + return _get_animation_attr_defs() class CreatePointCache(plugin.MayaCreator): @@ -121,7 +121,7 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - return _get_animation_attr_defs(self) + return _get_animation_attr_defs() def create(self, product_name, instance_data, pre_create_data): instance = super(CreatePointCache, self).create( From fca9eaa4238cc440009193796acff83c1c5c705f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:49:02 +0200 Subject: [PATCH 072/101] fix pre create attributes in look --- .../maya/client/ayon_maya/plugins/create/create_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_look.py b/server_addon/maya/client/ayon_maya/plugins/create/create_look.py index 1f90d18607..3e1ec103ba 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_look.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_look.py @@ -42,6 +42,6 @@ class CreateLook(plugin.MayaCreator): def get_pre_create_attr_defs(self): # Show same attributes on create but include use selection - defs = super(CreateLook, self).get_pre_create_attr_defs() + defs = list(super().get_pre_create_attr_defs()) defs.extend(self.get_instance_attr_defs()) return defs From 70b5ac77984fd34dc3eafd797b821c2b60667d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:49:23 +0200 Subject: [PATCH 073/101] bump maya version to '0.2.3' --- server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index 1655067287..fcad19941f 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 627e824413..e0fc2ee5cf 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.2" +version = "0.2.3" client_dir = "ayon_maya" ayon_required_addons = { From f17aacd83e7b4de7c87f0e3baa31bdd8a1b5530c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 15:51:56 +0200 Subject: [PATCH 074/101] :recycle: remove unused container name name isn't used anywhere but it is hard required, causing crashes in hosts that are not having it. Also adding broad try/except to catch any invalid containers so they won't crash the whole list --- .../tools/sceneinventory/models/containers.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 5230827ef6..f0248494e9 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -90,7 +90,6 @@ class ContainerItem: representation_id, loader_name, namespace, - name, object_name, item_id ): @@ -98,7 +97,6 @@ class ContainerItem: self.loader_name = loader_name self.object_name = object_name self.namespace = namespace - self.name = name self.item_id = item_id @classmethod @@ -107,7 +105,6 @@ class ContainerItem: representation_id=container["representation"], loader_name=container["loader"], namespace=container["namespace"], - name=container["name"], object_name=container["objectName"], item_id=uuid.uuid4().hex, ) @@ -204,7 +201,7 @@ class ContainersModel: def get_container_items(self): self._update_cache() return list(self._items_cache) - + def get_container_items_by_id(self, item_ids): return { item_id: self._container_items_by_id.get(item_id) @@ -329,14 +326,23 @@ class ContainersModel: containers = list(host.ls()) else: containers = [] + container_items = [] containers_by_id = {} container_items_by_id = {} for container in containers: - item = ContainerItem.from_container_data(container) - containers_by_id[item.item_id] = container - container_items_by_id[item.item_id] = item - container_items.append(item) + try: + item = ContainerItem.from_container_data(container) + containers_by_id[item.item_id] = container + container_items_by_id[item.item_id] = item + container_items.append(item) + except Exception as e: + # skip item if required data are missing + self._controller.log_error( + f"Failed to create item: {e}" + ) + continue + self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id From 66994e81ca16db06911cb9ea8fcaf2e2896e7821 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 16:53:43 +0300 Subject: [PATCH 075/101] Get the type name from the HDA definition --- .../client/ayon_houdini/plugins/load/load_hda.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index b04e211aa4..233ec91ced 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -28,14 +28,17 @@ class HdaLoader(plugin.HoudiniLoader): # Get the root node obj = hou.node("/obj") - # Create a unique name - counter = 1 namespace = namespace or context["folder"]["name"] - formatted = "{}_{}".format(namespace, name) if namespace else name - node_name = "{0}_{1:03d}".format(formatted, counter) + node_name = "{}_{}".format(namespace, name) if namespace else name hou.hda.installFile(file_path) - hda_node = obj.createNode(name, node_name) + + # Get the type name from the HDA definition. + hda_defs = hou.hda.definitionsInFile(file_path) + for hda_def in hda_defs: + type_name = hda_def.nodeTypeName() + + hda_node = obj.createNode(type_name, node_name) self[:] = [hda_node] From 92c000aabf9051bceb3764027c3afe299fb528dc Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 16:54:31 +0300 Subject: [PATCH 076/101] create HDA with unique type name --- .../ayon_houdini/plugins/create/create_hda.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index 6a1adce8cc..af7862b3ff 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -2,7 +2,11 @@ """Creator plugin for creating publishable Houdini Digital Assets.""" import ayon_api -from ayon_core.pipeline import CreatorError +from ayon_core.pipeline import ( + CreatorError, + get_current_project_name, + get_current_folder_path, +) from ayon_houdini.api import plugin import hou @@ -56,8 +60,18 @@ class CreateHDA(plugin.HoudiniCreator): raise CreatorError( "cannot create hda from node {}".format(to_hda)) + # Pick a unique type name for HDA product per folder path per project. + type_name = ( + "{project_name}{folder_path}_{node_name}".format( + project_name=get_current_project_name(), + folder_path=get_current_folder_path().replace("/","_"), + node_name=node_name + ) + ) + hda_node = to_hda.createDigitalAsset( - name=node_name, + name=type_name, + description=node_name, hda_file_name="$HIP/{}.hda".format(node_name) ) hda_node.layoutChildren() From a8bace3780377b06dbf4a69e41a20e8ad6964536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 15:56:04 +0200 Subject: [PATCH 077/101] :arrow_up: bump version --- client/ayon_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e4297e2000..efcd78cd11 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.3-dev.1" +__version__ = "0.3.4-dev.1" From b2be138552c4758e75d599c3164234255560704f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 16:01:37 +0200 Subject: [PATCH 078/101] :arrow_down: un-bump the version --- client/ayon_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index efcd78cd11..e4297e2000 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.4-dev.1" +__version__ = "0.3.3-dev.1" From 7d838eb216b171bc8f8d90f2618efa0d3cabfbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:04:21 +0200 Subject: [PATCH 079/101] Update client/ayon_core/tools/sceneinventory/models/containers.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/sceneinventory/models/containers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index f0248494e9..95c5322343 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -333,9 +333,6 @@ class ContainersModel: for container in containers: try: item = ContainerItem.from_container_data(container) - containers_by_id[item.item_id] = container - container_items_by_id[item.item_id] = item - container_items.append(item) except Exception as e: # skip item if required data are missing self._controller.log_error( @@ -343,6 +340,10 @@ class ContainersModel: ) continue + containers_by_id[item.item_id] = container + container_items_by_id[item.item_id] = item + container_items.append(item) + self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id From a5ba2ce5c8c74083c5db6bd98d62fbb2c8277b49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:47:29 +0200 Subject: [PATCH 080/101] fix cache invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- client/ayon_core/tools/common_models/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index babb690f87..f7939e5cd3 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -143,7 +143,7 @@ class UsersModel: Union[UserItem, None]: User item or None if not found. """ - self._invalidate_cache() + self._invalidate_cache(project_name) for user_item in self.get_user_items(project_name): if user_item.username == username: return user_item From 513826d6081ec8598e0c49495418b63e2b2eabf9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:52:44 +0200 Subject: [PATCH 081/101] remove unused import --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index d7d4f33266..c1aaeca6bd 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -20,7 +20,6 @@ from ayon_core.lib.transcoding import ( ) from ayon_core.lib import BoolDef from ayon_resolve.api import lib -from ayon_resolve.api import bmdvr from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID From f464d190321d4c2289eb92c98c95eeb44eb0de53 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 18:15:04 +0300 Subject: [PATCH 082/101] use the folder_path argument instead of get_current_folder_path() --- .../houdini/client/ayon_houdini/plugins/create/create_hda.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index af7862b3ff..694bc4f3c3 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -4,8 +4,7 @@ import ayon_api from ayon_core.pipeline import ( CreatorError, - get_current_project_name, - get_current_folder_path, + get_current_project_name ) from ayon_houdini.api import plugin import hou @@ -64,7 +63,7 @@ class CreateHDA(plugin.HoudiniCreator): type_name = ( "{project_name}{folder_path}_{node_name}".format( project_name=get_current_project_name(), - folder_path=get_current_folder_path().replace("/","_"), + folder_path=folder_path.replace("/","_"), node_name=node_name ) ) From cc6c24c9829558679f41cb41427d5a938bccfa47 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 18:29:38 +0300 Subject: [PATCH 083/101] use the first hda_def instead of using a for loop --- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 233ec91ced..fad9281c08 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -35,9 +35,10 @@ class HdaLoader(plugin.HoudiniLoader): # Get the type name from the HDA definition. hda_defs = hou.hda.definitionsInFile(file_path) - for hda_def in hda_defs: - type_name = hda_def.nodeTypeName() + if not hda_defs: + raise RuntimeError(f"No HDA definitions found in file: {file_path}") + type_name = hda_defs[0].nodeTypeName() hda_node = obj.createNode(type_name, node_name) self[:] = [hda_node] From 0f350d3704f2aa830186445bcff5531d471d7aab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:01:23 +0200 Subject: [PATCH 084/101] try to auto-fix shell true --- client/ayon_core/scripts/otio_burnin.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/scripts/otio_burnin.py b/client/ayon_core/scripts/otio_burnin.py index f12d298ac6..6b132b9a6a 100644 --- a/client/ayon_core/scripts/otio_burnin.py +++ b/client/ayon_core/scripts/otio_burnin.py @@ -14,9 +14,10 @@ from ayon_core.lib import ( convert_ffprobe_fps_value, ) +FFMPEG_EXE_COMMAND = subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) FFMPEG = ( '{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' -).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))) +).format(FFMPEG_EXE_COMMAND) DRAWTEXT = ( "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" @@ -482,10 +483,19 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): ) print("Launching command: {}".format(command)) + use_shell = True + try: + test_proc = subprocess.Popen( + f"{FFMPEG_EXE_COMMAND} --help", shell=True + ) + test_proc.wait() + except BaseException: + use_shell = False + kwargs = { "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, - "shell": True, + "shell": use_shell, } proc = subprocess.Popen(command, **kwargs) From edd19c313585b48b975435b67e54e6d939359b62 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Jun 2024 14:25:57 +0800 Subject: [PATCH 085/101] reset frame range & frame rate for the correct frame range & fps value --- .../client/ayon_max/plugins/load/load_pointcache.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 3eb4ba3323..87ea5c75bc 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, reset_frame_range from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -38,12 +38,9 @@ class AbcLoader(load.LoaderPlugin): } rt.AlembicImport.ImportToRoot = False - product_fps = float(context["version"]["attrib"].get("fps")) - if product_fps is None: - # Just stick to current scene FPS - product_fps = float(rt.frameRate) - # TODO: remove after the post-system fps setup - rt.frameRange = product_fps + # TODO: it will be removed after the improvement + # on the post-system setup + reset_frame_range() rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { From 84616064dcaf4b5ed5301e0083a8e06c4ebea833 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 15:30:20 +0200 Subject: [PATCH 086/101] settings conversions Refactor settings conversions for better viewer configuration handling. - Added `_isGroup` attribute to models - Implemented functions to extract display and viewer from strings - Updated conversion functions for imageio settings --- server_addon/nuke/server/settings/common.py | 6 +- .../nuke/server/settings/conversion.py | 117 +++++++- server_addon/nuke/server/settings/imageio.py | 250 ++++++------------ .../nuke/server/settings/publish_plugins.py | 10 - 4 files changed, 203 insertions(+), 180 deletions(-) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py index 54884ac828..2ddbc3ca26 100644 --- a/server_addon/nuke/server/settings/common.py +++ b/server_addon/nuke/server/settings/common.py @@ -161,10 +161,14 @@ class DisplayAndViewProfileModel(BaseSettingsModel): class ColorspaceConfigurationModel(BaseSettingsModel): + _isGroup: bool = True + enabled: bool = SettingsField( False, title="Enabled", - description="Enable baking target (colorspace or display/view)", + description=( + "Enable baking target (colorspace or display/view)." + ), ) type: str = SettingsField( diff --git a/server_addon/nuke/server/settings/conversion.py b/server_addon/nuke/server/settings/conversion.py index e88cee884d..3e1ca3a0a9 100644 --- a/server_addon/nuke/server/settings/conversion.py +++ b/server_addon/nuke/server/settings/conversion.py @@ -1,6 +1,94 @@ +import re from typing import Any -from .publish_plugins import DEFAULT_PUBLISH_VALUES +from .publish_plugins import DEFAULT_PUBLISH_PLUGIN_SETTINGS + + +def _get_viewer_config_from_string(input_string): + """Convert string to display and viewer string + + Args: + input_string (str): string with viewer + + Raises: + IndexError: if more then one slash in input string + IndexError: if missing closing bracket + + Returns: + tuple[str]: display, viewer + """ + display = None + viewer = input_string + # check if () or / or \ in name + if "/" in viewer: + split = viewer.split("/") + + # rise if more then one column + if len(split) > 2: + raise IndexError( + "Viewer Input string is not correct. " + f"More then two `/` slashes! {input_string}" + ) + + viewer = split[1] + display = split[0] + elif "(" in viewer: + pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]" + result_ = re.findall(pattern, viewer) + try: + result_ = result_.pop() + display = str(result_[1]).rstrip() + viewer = str(result_[0]).rstrip() + except IndexError as e: + raise IndexError( + "Viewer Input string is not correct. " + f"Missing bracket! {input_string}" + ) from e + + return (display, viewer) + + +def _convert_imageio_baking_0_2_2(overrides): + if "baking" not in overrides: + return + + baking_view_process = overrides["baking"].get("viewerProcess") + + if baking_view_process is None: + return + + display, view = _get_viewer_config_from_string(baking_view_process) + + overrides["baking_target"] = { + "enabled": True, + "type": "display_view", + "display_view": { + "display": display, + "view": view, + }, + } + + +def _convert_viewers_0_2_2(overrides): + if "viewer" not in overrides: + return + + viewer = overrides["viewer"] + + if "viewerProcess" in viewer: + viewer_process = viewer["viewerProcess"] + display, view = _get_viewer_config_from_string(viewer_process) + viewer.update({ + "display": display, + "view": view, + }) + if "output_transform" in viewer: + output_transform = viewer["output_transform"] + display, view = _get_viewer_config_from_string(output_transform) + overrides["monitor"] = { + "display": display, + "view": view, + } def _convert_imageio_configs_0_2_2(overrides): @@ -8,7 +96,13 @@ def _convert_imageio_configs_0_2_2(overrides): 0.2.2. is the latest version using the old way. """ - pass + if "imageio" not in overrides: + return + + imageio_overrides = overrides["imageio"] + + _convert_imageio_baking_0_2_2(imageio_overrides) + _convert_viewers_0_2_2(imageio_overrides) def _convert_extract_intermediate_files_0_2_2(publish_overrides): @@ -16,7 +110,24 @@ def _convert_extract_intermediate_files_0_2_2(publish_overrides): 0.2.2. is the latest version using the old way. """ - pass + # override can be either `display/view` or `view (display)` + if "ExtractReviewIntermediates" in publish_overrides: + extract_review_intermediates = publish_overrides[ + "ExtractReviewIntermediates"] + + for output in extract_review_intermediates.get("outputs", []): + if viewer_process_override := output.get("viewer_process_override"): + display, view = _get_viewer_config_from_string( + viewer_process_override) + + output["colorspace_override"] = { + "enabled": True, + "type": "display_view", + "display_view": { + "display": display, + "view": view, + }, + } def _convert_publish_plugins(overrides): diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 08686a08e8..34deb351ed 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -11,6 +11,7 @@ from .common import ( ColorspaceConfigurationModel, ) + class NodesModel(BaseSettingsModel): _layout = "expanded" plugins: list[str] = SettingsField( @@ -54,6 +55,8 @@ class OverrideNodesModel(NodesModel): class NodesSetting(BaseSettingsModel): + _isGroup: bool = True + required_nodes: list[RequiredNodesModel] = SettingsField( title="Plugin required", default_factory=list @@ -85,6 +88,8 @@ def ocio_configs_switcher_enum(): class WorkfileColorspaceSettings(BaseSettingsModel): """Nuke workfile colorspace preset. """ + _isGroup: bool = True + color_management: Literal["Nuke", "OCIO"] = SettingsField( title="Color Management Workflow" ) @@ -127,6 +132,8 @@ class ReadColorspaceRulesItems(BaseSettingsModel): class RegexInputsModel(BaseSettingsModel): + _isGroup: bool = True + inputs: list[ReadColorspaceRulesItems] = SettingsField( default_factory=list, title="Inputs" @@ -134,15 +141,44 @@ class RegexInputsModel(BaseSettingsModel): class ViewProcessModel(BaseSettingsModel): - viewerProcess: str = SettingsField( - title="Viewer Process Name" + _isGroup: bool = True + + display: str = SettingsField( + "", + title="Display", + description="What display to use", ) - output_transform: str = SettingsField( - title="Output Transform" + view: str = SettingsField( + "", + title="View", + description=( + "What view to use. Anatomy context tokens can " + "be used to dynamically set the value." + ), + ) + + +class MonitorProcessModel(BaseSettingsModel): + _isGroup: bool = True + + display: str = SettingsField( + "", + title="Display", + description="What display to use", + ) + view: str = SettingsField( + "", + title="View", + description=( + "What view to use. Anatomy context tokens can " + "be used to dynamically set the value." + ), ) class ImageIOConfigModel(BaseSettingsModel): + _isGroup: bool = True + override_global_config: bool = SettingsField( False, title="Override global OCIO config" @@ -161,6 +197,8 @@ class ImageIOFileRuleModel(BaseSettingsModel): class ImageIOFileRulesModel(BaseSettingsModel): + _isGroup: bool = True + activate_host_rules: bool = SettingsField(False) rules: list[ImageIOFileRuleModel] = SettingsField( default_factory=list, @@ -175,14 +213,7 @@ class ImageIOFileRulesModel(BaseSettingsModel): class ImageIOSettings(BaseSettingsModel): """Nuke color management project settings. """ - _isGroup: bool = True - """# TODO: enhance settings with host api: - to restructure settings for simplification. - - now: nuke/imageio/viewer/viewerProcess - future: nuke/imageio/viewer - """ activate_host_color_management: bool = SettingsField( True, title="Enable Color Management") ocio_config: ImageIOConfigModel = SettingsField( @@ -199,18 +230,14 @@ class ImageIOSettings(BaseSettingsModel): description="""Viewer profile is used during Creation of new viewer node at knob viewerProcess""" ) - + monitor: MonitorProcessModel = SettingsField( + default_factory=MonitorProcessModel, + title="Monitor OUT" + ) baking_target: ColorspaceConfigurationModel = SettingsField( default_factory=ColorspaceConfigurationModel, title="Baking Target Colorspace" ) - baking: ViewProcessModel = SettingsField( - default_factory=ViewProcessModel, - title="Baking", - description="""[DEPRECATED - use 'Baking Target Colorspace' instead] - Baking profile is used during - publishing baked colorspace data at knob viewerProcess""", - ) workfile: WorkfileColorspaceSettings = SettingsField( default_factory=WorkfileColorspaceSettings, @@ -232,18 +259,12 @@ class ImageIOSettings(BaseSettingsModel): DEFAULT_IMAGEIO_SETTINGS = { - "viewer": { - "viewerProcess": "ACES/sRGB", - "output_transform": "ACES/sRGB" - }, + "viewer": {"display": "ACES", "view": "sRGB"}, + "monitor": {"display": "ACES", "view": "Rec.709"}, "baking_target": { - "enabled": False, + "enabled": True, "type": "colorspace", - "colorspace": "" - }, - "baking": { - "viewerProcess": "ACES/Rec.709", - "output_transform": "ACES/Rec.709" + "colorspace": "Output - Rec.709", }, "workfile": { "color_management": "OCIO", @@ -254,170 +275,67 @@ DEFAULT_IMAGEIO_SETTINGS = { "int_8_lut": "role_matte_paint", "int_16_lut": "role_texture_paint", "log_lut": "role_compositing_log", - "float_lut": "role_scene_linear" + "float_lut": "role_scene_linear", }, "nodes": { "required_nodes": [ { - "plugins": [ - "CreateWriteRender" - ], + "plugins": ["CreateWriteRender"], "nuke_node_class": "Write", "knobs": [ - { - "type": "text", - "name": "file_type", - "text": "exr" - }, - { - "type": "text", - "name": "datatype", - "text": "16 bit half" - }, - { - "type": "text", - "name": "compression", - "text": "Zip (1 scanline)" - }, - { - "type": "boolean", - "name": "autocrop", - "boolean": True - }, + {"type": "text", "name": "file_type", "text": "exr"}, + {"type": "text", "name": "datatype", "text": "16 bit half"}, + {"type": "text", "name": "compression", "text": "Zip (1 scanline)"}, + {"type": "boolean", "name": "autocrop", "boolean": True}, { "type": "color_gui", "name": "tile_color", - "color_gui": [ - 186, - 35, - 35 - ] + "color_gui": [186, 35, 35], }, - { - "type": "text", - "name": "channels", - "text": "rgb" - }, - { - "type": "text", - "name": "colorspace", - "text": "scene_linear" - }, - { - "type": "boolean", - "name": "create_directories", - "boolean": True - } - ] + {"type": "text", "name": "channels", "text": "rgb"}, + {"type": "text", "name": "colorspace", "text": "scene_linear"}, + {"type": "boolean", "name": "create_directories", "boolean": True}, + ], }, { - "plugins": [ - "CreateWritePrerender" - ], + "plugins": ["CreateWritePrerender"], "nuke_node_class": "Write", "knobs": [ - { - "type": "text", - "name": "file_type", - "text": "exr" - }, - { - "type": "text", - "name": "datatype", - "text": "16 bit half" - }, - { - "type": "text", - "name": "compression", - "text": "Zip (1 scanline)" - }, - { - "type": "boolean", - "name": "autocrop", - "boolean": True - }, + {"type": "text", "name": "file_type", "text": "exr"}, + {"type": "text", "name": "datatype", "text": "16 bit half"}, + {"type": "text", "name": "compression", "text": "Zip (1 scanline)"}, + {"type": "boolean", "name": "autocrop", "boolean": True}, { "type": "color_gui", "name": "tile_color", - "color_gui": [ - 171, - 171, - 10 - ] + "color_gui": [171, 171, 10], }, - { - "type": "text", - "name": "channels", - "text": "rgb" - }, - { - "type": "text", - "name": "colorspace", - "text": "scene_linear" - }, - { - "type": "boolean", - "name": "create_directories", - "boolean": True - } - ] + {"type": "text", "name": "channels", "text": "rgb"}, + {"type": "text", "name": "colorspace", "text": "scene_linear"}, + {"type": "boolean", "name": "create_directories", "boolean": True}, + ], }, { - "plugins": [ - "CreateWriteImage" - ], + "plugins": ["CreateWriteImage"], "nuke_node_class": "Write", "knobs": [ - { - "type": "text", - "name": "file_type", - "text": "tiff" - }, - { - "type": "text", - "name": "datatype", - "text": "16 bit" - }, - { - "type": "text", - "name": "compression", - "text": "Deflate" - }, + {"type": "text", "name": "file_type", "text": "tiff"}, + {"type": "text", "name": "datatype", "text": "16 bit"}, + {"type": "text", "name": "compression", "text": "Deflate"}, { "type": "color_gui", "name": "tile_color", - "color_gui": [ - 56, - 162, - 7 - ] + "color_gui": [56, 162, 7], }, - { - "type": "text", - "name": "channels", - "text": "rgb" - }, - { - "type": "text", - "name": "colorspace", - "text": "texture_paint" - }, - { - "type": "boolean", - "name": "create_directories", - "boolean": True - } - ] - } + {"type": "text", "name": "channels", "text": "rgb"}, + {"type": "text", "name": "colorspace", "text": "texture_paint"}, + {"type": "boolean", "name": "create_directories", "boolean": True}, + ], + }, ], - "override_nodes": [] + "override_nodes": [], }, "regex_inputs": { - "inputs": [ - { - "regex": "(beauty).*(?=.exr)", - "colorspace": "linear" - } - ] - } + "inputs": [{"regex": "(beauty).*(?=.exr)", "colorspace": "linear"}] + }, } diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 1373b3b13d..2c92d6f02a 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -146,14 +146,6 @@ class IntermediateOutputModel(BaseSettingsModel): description="Override Baking target with colorspace or display/view", default_factory=ColorspaceConfigurationModel ) - viewer_process_override: str = SettingsField( - "", - title="Viewer process override", - description=( - "[DEPRECATED - use 'Target baking colorspace override'] " - "Override viewer process node (LUT)" - ), - ) bake_viewer_input_process: bool = SettingsField( True, title="Bake viewer input process node (LUT)", @@ -278,7 +270,6 @@ class PublishPluginsModel(BaseSettingsModel): section="Integrators" ) - DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "CollectInstanceData": { "sync_workfile_version_on_product_types": [ @@ -344,7 +335,6 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "product_names": [] }, "read_raw": False, - "viewer_process_override": "", "colorspace_override": { "enabled": False, "type": "colorspace", From eb124b54ad3781b79f507ef270fc86f2737d9128 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:01:46 +0200 Subject: [PATCH 087/101] use task name as source of truth from ui --- client/ayon_core/tools/workfiles/abstract.py | 13 ++++++----- client/ayon_core/tools/workfiles/control.py | 23 +++++++++++++++---- .../tools/workfiles/models/workfiles.py | 12 ++++++---- .../widgets/files_widget_workarea.py | 10 ++++---- .../tools/workfiles/widgets/side_panel.py | 16 ++++++------- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index f345e20dca..330b413300 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -834,12 +834,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): """Get workarea file items. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task name. + sender (Optional[str]): Who requested workarea file items. Returns: list[FileItem]: List of workarea file items. @@ -905,12 +906,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): """Workfile info from database. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. Returns: @@ -921,7 +922,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): """Save workfile info to database. At this moment the only information which can be saved about @@ -932,7 +933,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. note (Union[str, None]): Note. """ diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 8fa9135bc0..1f66b363e1 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -410,9 +410,11 @@ class BaseWorkfileController( return self._workfiles_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workarea_file_items( - folder_id, task_id) + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workfiles_model.get_workarea_save_as_data( @@ -447,12 +449,14 @@ class BaseWorkfileController( return self._workfiles_model.get_published_file_items( folder_id, task_name) - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workfile_info( folder_id, task_id, filepath ) - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): + task_id = self._get_task_id(folder_id, task_name) self._workfiles_model.save_workfile_info( folder_id, task_id, filepath, note ) @@ -627,6 +631,17 @@ class BaseWorkfileController( def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") + def _get_task_id(self, folder_id, task_name, sender=None): + task_item = self._hierarchy_model.get_task_item_by_name( + self.get_current_project_name(), + folder_id, + task_name, + sender + ) + if not task_item: + return None + return task_item.id + # Expected selection # - expected selection is used to restore selection after refresh # or when current context should be used diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index c93bbb6637..a47e459a6e 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -173,7 +173,7 @@ class WorkareaModel: folder_mapping[task_id] = workdir return workdir - def get_file_items(self, folder_id, task_id): + def get_file_items(self, folder_id, task_id, task_name): items = [] if not folder_id or not task_id: return items @@ -192,7 +192,7 @@ class WorkareaModel: continue workfile_info = self._controller.get_workfile_info( - folder_id, task_id, filepath + folder_id, task_name, filepath ) modified = os.path.getmtime(filepath) items.append(FileItem( @@ -770,19 +770,21 @@ class WorkfilesModel: return self._workarea_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_id, task_name): """Workfile items for passed context from workarea. Args: folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. + task_name (Union[str, None]): Task name. Returns: list[FileItem]: List of file items matching workarea of passed context. """ - - return self._workarea_model.get_file_items(folder_id, task_id) + return self._workarea_model.get_file_items( + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workarea_model.get_workarea_save_as_data( diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index 5c102dcdd4..7f76b6a8ab 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -66,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): self._empty_item_used = False self._published_mode = False self._selected_folder_id = None - self._selected_task_id = None + self._selected_task_name = None self._add_missing_context_item() @@ -153,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] - self._selected_task_id = event["task_id"] + self._selected_task_name = event["task_name"] if not self._published_mode: self._fill_items() @@ -179,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _fill_items_impl(self): folder_id = self._selected_folder_id - task_id = self._selected_task_id - if not folder_id or not task_id: + task_name = self._selected_task_name + if not folder_id or not task_name: self._add_missing_context_item() return file_items = self._controller.get_workarea_file_items( - folder_id, task_id + folder_id, task_name ) root_item = self.invisibleRootItem() if not file_items: diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 53fdf0e0ac..7ba60b5544 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save = btn_note_save self._folder_id = None - self._task_id = None + self._task_name = None self._filepath = None self._orig_note = "" self._controller = controller @@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget): def _on_selection_change(self, event): folder_id = event["folder_id"] - task_id = event["task_id"] + task_name = event["task_name"] filepath = event["path"] - self._set_context(folder_id, task_id, filepath) + self._set_context(folder_id, task_name, filepath) def _on_note_change(self): text = self._note_input.toPlainText() @@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget): note = self._note_input.toPlainText() self._controller.save_workfile_info( self._folder_id, - self._task_id, + self._task_name, self._filepath, note ) self._orig_note = note self._btn_note_save.setEnabled(False) - def _set_context(self, folder_id, task_id, filepath): + def _set_context(self, folder_id, task_name, filepath): workfile_info = None # Check if folder, task and file are selected - if bool(folder_id) and bool(task_id) and bool(filepath): + if bool(folder_id) and bool(task_name) and bool(filepath): workfile_info = self._controller.get_workfile_info( - folder_id, task_id, filepath + folder_id, task_name, filepath ) enabled = workfile_info is not None @@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save.setEnabled(enabled) self._folder_id = folder_id - self._task_id = task_id + self._task_name = task_name self._filepath = filepath # Disable inputs and remove texts if any required arguments are From 43713d7ac01f021ff8a81a59e87c7acc5142037c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:16:08 +0200 Subject: [PATCH 088/101] define id of workfile entity for updates --- client/ayon_core/tools/workfiles/models/workfiles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index a47e459a6e..a268a9bd0e 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -1,6 +1,7 @@ import os import re import copy +import uuid import arrow import ayon_api @@ -587,6 +588,7 @@ class WorkfileEntitiesModel: username = self._get_current_username() workfile_info = { + "id": uuid.uuid4().hex, "path": rootless_path, "taskId": task_id, "attrib": { From ad23386d189453b1acaa1d8c86681d8a9185dfcd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:16:15 +0200 Subject: [PATCH 089/101] fix create new workfile --- client/ayon_core/tools/workfiles/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 1f66b363e1..fbd0bd3ac8 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -737,7 +737,7 @@ class BaseWorkfileController( self._host_save_workfile(dst_filepath) # Make sure workfile info exists - self.save_workfile_info(folder_id, task_id, dst_filepath, None) + self.save_workfile_info(folder_id, task_name, dst_filepath, None) # Create extra folders create_workdir_extra_folders( From 2d13f9787f3cfb4ee466f48b41841a303910aee2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 17:27:23 +0200 Subject: [PATCH 090/101] Update set_viewers_colorspace method to handle imageio_nuke dictionary, refactor ExporterReviewMov class to support different colorspaces for baking, and adjust settings in PublishPluginsModel. --- server_addon/nuke/client/ayon_nuke/api/lib.py | 58 ++++++------- .../nuke/client/ayon_nuke/api/plugin.py | 83 ++++++++++++------- .../nuke/server/settings/publish_plugins.py | 1 + 3 files changed, 83 insertions(+), 59 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/lib.py b/server_addon/nuke/client/ayon_nuke/api/lib.py index 09dab4687a..3906777c95 100644 --- a/server_addon/nuke/client/ayon_nuke/api/lib.py +++ b/server_addon/nuke/client/ayon_nuke/api/lib.py @@ -1509,36 +1509,30 @@ class WorkfileSettings(object): for filter in nodes_filter: return [n for n in self._nodes if filter in n.Class()] - def set_viewers_colorspace(self, viewer_dict): + def set_viewers_colorspace(self, imageio_nuke): ''' Adds correct colorspace to viewer Arguments: - viewer_dict (dict): adjustments from presets + imageio_nuke (dict): nuke colorspace configurations ''' - if not isinstance(viewer_dict, dict): - msg = "set_viewers_colorspace(): argument should be dictionary" - log.error(msg) - nuke.message(msg) - return + viewer_config = imageio_nuke["viewer"] + monitor_config = imageio_nuke["monitor"] filter_knobs = [ "viewerProcess", "wipe_position", "monitorOutOutputTransform" ] - - display, viewer = get_viewer_config_from_string( - viewer_dict["viewerProcess"] - ) viewer_process = create_viewer_profile_string( - viewer, display, path_like=False - ) - display, viewer = get_viewer_config_from_string( - viewer_dict["output_transform"] + viewer_config["view"], + viewer_config["display"], + path_like=False, ) output_transform = create_viewer_profile_string( - viewer, display, path_like=False + monitor_config["view"], + monitor_config["display"], + path_like=False, ) erased_viewers = [] for v in nuke.allNodes(filter="Viewer"): @@ -1547,8 +1541,10 @@ class WorkfileSettings(object): if viewer_process not in v["viewerProcess"].value(): copy_inputs = v.dependencies() - copy_knobs = {k: v[k].value() for k in v.knobs() - if k not in filter_knobs} + copy_knobs = { + k: v[k].value() for k in v.knobs() + if k not in filter_knobs + } # delete viewer with wrong settings erased_viewers.append(v["name"].value()) @@ -1590,12 +1586,12 @@ class WorkfileSettings(object): if not config_data: # no ocio config found and no custom path used if self._root_node["colorManagement"].value() \ - not in color_management: + not in color_management: self._root_node["colorManagement"].setValue(color_management) # second set ocio version if self._root_node["OCIO_config"].value() \ - not in native_ocio_config: + not in native_ocio_config: self._root_node["OCIO_config"].setValue(native_ocio_config) else: @@ -1623,21 +1619,25 @@ class WorkfileSettings(object): if correct_settings: self._set_ocio_config_path_to_workfile(config_data) + workfile_settings_output = {} # get monitor lut from settings respecting Nuke version differences monitor_lut_data = self._get_monitor_settings( workfile_settings["monitor_out_lut"], workfile_settings["monitor_lut"] ) - monitor_lut_data.update({ - "workingSpaceLUT": workfile_settings["working_space"], - "int8Lut": workfile_settings["int_8_lut"], - "int16Lut": workfile_settings["int_16_lut"], - "logLut": workfile_settings["log_lut"], - "floatLut": workfile_settings["float_lut"] - }) + workfile_settings_output |= monitor_lut_data + workfile_settings_output.update( + { + "workingSpaceLUT": workfile_settings["working_space"], + "int8Lut": workfile_settings["int_8_lut"], + "int16Lut": workfile_settings["int_16_lut"], + "logLut": workfile_settings["log_lut"], + "floatLut": workfile_settings["float_lut"], + } + ) # then set the rest - for knob, value_ in monitor_lut_data.items(): + for knob, value_ in workfile_settings_output.items(): # skip unfilled ocio config path # it will be dict in value if isinstance(value_, dict): @@ -1972,7 +1972,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. log.info("Setting colorspace to viewers...") try: - self.set_viewers_colorspace(nuke_colorspace["viewer"]) + self.set_viewers_colorspace(nuke_colorspace) except AttributeError as _error: msg = "Set Colorspace to viewer error: {}".format(_error) nuke.message(msg) diff --git a/server_addon/nuke/client/ayon_nuke/api/plugin.py b/server_addon/nuke/client/ayon_nuke/api/plugin.py index 4f05cd41b9..3e8da03af6 100644 --- a/server_addon/nuke/client/ayon_nuke/api/plugin.py +++ b/server_addon/nuke/client/ayon_nuke/api/plugin.py @@ -1,3 +1,4 @@ +import dis import nuke import re import os @@ -638,12 +639,18 @@ class ExporterReview(object): from . import lib as opnlib nuke_imageio = opnlib.get_nuke_imageio_settings() - # TODO: this is only securing backward compatibility lets remove - # this once all projects's anatomy are updated to newer config - if "baking" in nuke_imageio.keys(): - return nuke_imageio["baking"]["viewerProcess"] + if ( + "baking_target" in nuke_imageio.keys() + and nuke_imageio["baking_target"]["enabled"] + ): + return nuke_imageio["baking_target"] else: - return nuke_imageio["viewer"]["viewerProcess"] + # viewer is having display and view keys only and it is + # display_view type + return { + "type": "display_view", + "display_view": nuke_imageio["viewer"], + } class ExporterReviewLut(ExporterReview): @@ -861,16 +868,16 @@ class ExporterReviewMov(ExporterReview): bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] - viewer_process_override = kwargs[ - "viewer_process_override"] - baking_view_profile = ( - viewer_process_override or self.get_imageio_baking_profile()) + colorspace_override = kwargs["colorspace_override"] + + baking_colorspace = self.get_imageio_baking_profile() + if colorspace_override["enabled"]: + baking_colorspace = colorspace_override["colorspace"] fps = self.instance.context.data["fps"] - self.log.debug(">> baking_view_profile `{}`".format( - baking_view_profile)) + self.log.debug(f">> baking_view_profile `{baking_colorspace}`") add_custom_tags = kwargs.get("add_custom_tags", []) @@ -932,32 +939,50 @@ class ExporterReviewMov(ExporterReview): if not self.viewer_lut_raw: # OCIODisplay - dag_node = nuke.createNode("OCIODisplay") + if baking_colorspace["type"] == "display_view": + display_view = baking_colorspace["display_view"] - # assign display - display, viewer = get_viewer_config_from_string( - str(baking_view_profile) - ) - if display: - dag_node["display"].setValue(display) + message = "OCIODisplay... '{}'" + node = nuke.createNode("OCIODisplay") - # assign viewer - dag_node["view"].setValue(viewer) + # assign display + display = display_view["display"] + view = display_view["view"] - if config_data: - # convert display and view to colorspace - colorspace = get_display_view_colorspace_name( - config_path=config_data["path"], - display=display, - view=viewer + if display: + node["display"].setValue(display) + + # assign viewer + node["view"].setValue(view) + if config_data: + # convert display and view to colorspace + colorspace = get_display_view_colorspace_name( + config_path=config_data["path"], + display=display, view=view + ) + # OCIOColorSpace + elif baking_colorspace["type"] == "colorspace": + baking_colorspace = baking_colorspace["colorspace"] + node = nuke.createNode("OCIOColorSpace") + message = "OCIOColorSpace... '{}'" + node["in_colorspace"].setValue(colorspace) + node["out_colorspace"].setValue(baking_colorspace) + colorspace = baking_colorspace + + else: + raise ValueError( + "Invalid baking color space type: " + f"{baking_colorspace['type']}" ) self._connect_to_above_nodes( - dag_node, product_name, "OCIODisplay... `{}`" + node, product_name, message ) + # Write node write_node = nuke.createNode("Write") - self.log.debug("Path: {}".format(self.path)) + self.log.debug(f"Path: {self.path}") + write_node["file"].setValue(str(self.path)) write_node["file_type"].setValue(str(self.ext)) write_node["channels"].setValue(str(self.color_channels)) @@ -1020,8 +1045,6 @@ class ExporterReviewMov(ExporterReview): nuke.scriptSave() return self.data - - def _shift_to_previous_node_and_temp(self, product_name, node, message): self._temp_nodes[product_name].append(node) self.previous_node = node self.log.debug(message.format(self._temp_nodes[product_name])) diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 2c92d6f02a..c52c9e9c84 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -270,6 +270,7 @@ class PublishPluginsModel(BaseSettingsModel): section="Integrators" ) + DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "CollectInstanceData": { "sync_workfile_version_on_product_types": [ From f8e928f36c388b2335b421477fbea38fd2832ae5 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 13 Jun 2024 19:37:48 +0300 Subject: [PATCH 091/101] raise LoadError instead of RuntimeError --- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index fad9281c08..5738ba7fab 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os from ayon_core.pipeline import get_representation_path +from ayon_core.pipeline.load import LoadError from ayon_houdini.api import ( pipeline, plugin @@ -36,7 +37,7 @@ class HdaLoader(plugin.HoudiniLoader): # Get the type name from the HDA definition. hda_defs = hou.hda.definitionsInFile(file_path) if not hda_defs: - raise RuntimeError(f"No HDA definitions found in file: {file_path}") + raise LoadError(f"No HDA definitions found in file: {file_path}") type_name = hda_defs[0].nodeTypeName() hda_node = obj.createNode(type_name, node_name) From 5e4bc0b51b0c81b14c2859fda61acae1e23bf3be Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 22:55:36 +0200 Subject: [PATCH 092/101] Formatting of display and view profiles with anatomy data. Remove unused imports, refactor knob creation and updating functions, improve logging messages for better clarity, update --- server_addon/nuke/client/ayon_nuke/api/lib.py | 67 +++++++------------ .../nuke/client/ayon_nuke/api/plugin.py | 31 +++++++-- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/lib.py b/server_addon/nuke/client/ayon_nuke/api/lib.py index 3906777c95..490205dff3 100644 --- a/server_addon/nuke/client/ayon_nuke/api/lib.py +++ b/server_addon/nuke/client/ayon_nuke/api/lib.py @@ -1,5 +1,4 @@ import os -from pprint import pformat import re import json import six @@ -37,6 +36,7 @@ from ayon_core.pipeline import ( get_current_host_name, get_current_project_name, get_current_folder_path, + get_current_task_name, AYON_INSTANCE_ID, AVALON_INSTANCE_ID, ) @@ -154,15 +154,9 @@ def set_node_data(node, knobname, data): """ # if exists then update data if knobname in node.knobs(): - log.debug("Updating knobname `{}` on node `{}`".format( - knobname, node.name() - )) update_node_data(node, knobname, data) return - log.debug("Creating knobname `{}` on node `{}`".format( - knobname, node.name() - )) # else create new knob_value = JSON_PREFIX + json.dumps(data) knob = nuke.String_Knob(knobname) @@ -514,10 +508,8 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): try: # check if data available on the node test = node[DATA_GROUP_KEY].value() - log.debug("Only testing if data available: `{}`".format(test)) except NameError as e: # if it doesn't then create it - log.debug("Creating avalon knob: `{}`".format(e)) if create: node = set_avalon_knob_data(node) return get_avalon_knob_data(node) @@ -678,8 +670,6 @@ def get_imageio_node_setting(node_class, plugin_name, product_name): imageio_node = node break - log.debug("__ imageio_node: {}".format(imageio_node)) - if not imageio_node: return @@ -690,8 +680,6 @@ def get_imageio_node_setting(node_class, plugin_name, product_name): product_name, imageio_node["knobs"] ) - - log.info("ImageIO node: {}".format(imageio_node)) return imageio_node @@ -706,8 +694,6 @@ def get_imageio_node_override_setting( # find matching override node override_imageio_node = None for onode in override_nodes: - log.debug("__ onode: {}".format(onode)) - log.debug("__ productName: {}".format(product_name)) if node_class not in onode["nuke_node_class"]: continue @@ -727,7 +713,6 @@ def get_imageio_node_override_setting( override_imageio_node = onode break - log.debug("__ override_imageio_node: {}".format(override_imageio_node)) # add overrides to imageio_node if override_imageio_node: # get all knob names in imageio_node @@ -740,7 +725,6 @@ def get_imageio_node_override_setting( for knob in knobs_settings: # add missing knobs into imageio_node if oknob_name not in knob_names: - log.debug("_ adding knob: `{}`".format(oknob)) knobs_settings.append(oknob) knob_names.append(oknob_name) continue @@ -750,9 +734,6 @@ def get_imageio_node_override_setting( knob_type = knob["type"] # override matching knob name - log.debug( - "_ overriding knob: `{}` > `{}`".format(knob, oknob) - ) if not oknob_value: # remove original knob if no value found in oknob knobs_settings.remove(knob) @@ -923,7 +904,6 @@ def writes_version_sync(): new_version = "v" + str("{" + ":0>{}".format(padding) + "}").format( int(rootVersion) ) - log.debug("new_version: {}".format(new_version)) except Exception: return @@ -936,13 +916,11 @@ def writes_version_sync(): try: if avalon_knob_data["families"] not in ["render"]: - log.debug(avalon_knob_data["families"]) continue node_file = each["file"].value() node_version = "v" + get_version_from_path(node_file) - log.debug("node_version: {}".format(node_version)) node_new_file = node_file.replace(node_version, new_version) each["file"].setValue(node_new_file) @@ -1332,7 +1310,6 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): kwargs (dict)[optional]: keys for formattable knob settings """ for knob in knob_settings: - log.debug("__ knob: {}".format(pformat(knob))) knob_name = knob["name"] if knob_name not in node.knobs(): continue @@ -1486,13 +1463,17 @@ class WorkfileSettings(object): Context._project_entity = project_entity self._project_name = project_name self._folder_path = get_current_folder_path() + self._task_name = get_current_task_name() self._folder_entity = ayon_api.get_folder_by_path( project_name, self._folder_path ) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) - self.data = kwargs + context_data = get_template_data_with_names( + project_name, self._folder_path, self._task_name, "nuke" + ) + self.formatting_data = context_data def get_nodes(self, nodes=None, nodes_filter=None): @@ -1516,23 +1497,16 @@ class WorkfileSettings(object): imageio_nuke (dict): nuke colorspace configurations ''' - viewer_config = imageio_nuke["viewer"] - monitor_config = imageio_nuke["monitor"] - filter_knobs = [ "viewerProcess", "wipe_position", "monitorOutOutputTransform" ] - viewer_process = create_viewer_profile_string( - viewer_config["view"], - viewer_config["display"], - path_like=False, + viewer_process = self._display_and_view_formatted( + imageio_nuke["viewer"] ) - output_transform = create_viewer_profile_string( - monitor_config["view"], - monitor_config["display"], - path_like=False, + output_transform = self._display_and_view_formatted( + imageio_nuke["monitor"] ) erased_viewers = [] for v in nuke.allNodes(filter="Viewer"): @@ -1570,6 +1544,21 @@ class WorkfileSettings(object): "Attention! Viewer nodes {} were erased." "It had wrong color profile".format(erased_viewers)) + def _display_and_view_formatted(self, view_profile): + """ Format display and view profile string + + Args: + view_profile (dict): view and display profile + + Returns: + str: formatted display and view profile string + """ + display_view = create_viewer_profile_string( + view_profile["view"], view_profile["display"], path_like=False + ) + # format any template tokens used in the string + return StringTemplate(display_view).format_strict(self.formatting_data) + def set_root_colorspace(self, imageio_host): ''' Adds correct colorspace to root @@ -1646,7 +1635,6 @@ class WorkfileSettings(object): if not value_: continue self._root_node[knob].setValue(str(value_)) - log.debug("nuke.root()['{}'] changed to: {}".format(knob, value_)) def _get_monitor_settings(self, viewer_lut, monitor_lut): """ Get monitor settings from viewer and monitor lut @@ -1889,8 +1877,6 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. elif node_data: nuke_imageio_writes = get_write_node_template_attr(node) - log.debug("nuke_imageio_writes: `{}`".format(nuke_imageio_writes)) - if not nuke_imageio_writes: return @@ -1938,7 +1924,6 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. "to": future } - log.debug(changes) if changes: msg = "Read nodes are not set to correct colorspace:\n\n" for nname, knobs in changes.items(): @@ -2653,8 +2638,6 @@ class NukeDirmap(HostDirmap): def dirmap_routine(self, source_path, destination_path): source_path = source_path.lower().replace(os.sep, '/') destination_path = destination_path.lower().replace(os.sep, '/') - log.debug("Map: {} with: {}->{}".format(self.file_name, - source_path, destination_path)) if platform.system().lower() == "windows": self.file_name = self.file_name.lower().replace( source_path, destination_path) diff --git a/server_addon/nuke/client/ayon_nuke/api/plugin.py b/server_addon/nuke/client/ayon_nuke/api/plugin.py index 3e8da03af6..129ac45361 100644 --- a/server_addon/nuke/client/ayon_nuke/api/plugin.py +++ b/server_addon/nuke/client/ayon_nuke/api/plugin.py @@ -13,6 +13,7 @@ from ayon_core.lib import ( BoolDef, EnumDef ) +from ayon_core.lib import StringTemplate from ayon_core.pipeline import ( LoaderPlugin, CreatorError, @@ -39,7 +40,6 @@ from .lib import ( set_node_data, get_node_data, get_view_process_node, - get_viewer_config_from_string, get_filenames_without_hash, link_knobs ) @@ -797,6 +797,7 @@ class ExporterReviewMov(ExporterReview): self.viewer_lut_raw = klass.viewer_lut_raw self.write_colorspace = instance.data["colorspace"] self.color_channels = instance.data["color_channels"] + self.formatting_data = instance.data["anatomyData"] self.name = name or "baked" self.ext = ext or "mov" @@ -869,11 +870,11 @@ class ExporterReviewMov(ExporterReview): bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] - colorspace_override = kwargs["colorspace_override"] - baking_colorspace = self.get_imageio_baking_profile() + + colorspace_override = kwargs["colorspace_override"] if colorspace_override["enabled"]: - baking_colorspace = colorspace_override["colorspace"] + baking_colorspace = colorspace_override fps = self.instance.context.data["fps"] @@ -945,27 +946,41 @@ class ExporterReviewMov(ExporterReview): message = "OCIODisplay... '{}'" node = nuke.createNode("OCIODisplay") - # assign display + # assign display and view display = display_view["display"] view = display_view["view"] + # display could not be set in nuke_default config if display: + # format display string with anatomy data + display = StringTemplate(display).format_strict( + self.formatting_data + ) node["display"].setValue(display) + # format view string with anatomy data + view = StringTemplate(view).format_strict( + self.formatting_data) # assign viewer node["view"].setValue(view) + if config_data: # convert display and view to colorspace colorspace = get_display_view_colorspace_name( config_path=config_data["path"], display=display, view=view ) + # OCIOColorSpace elif baking_colorspace["type"] == "colorspace": baking_colorspace = baking_colorspace["colorspace"] + # format colorspace string with anatomy data + baking_colorspace = StringTemplate( + baking_colorspace).format_strict(self.formatting_data) node = nuke.createNode("OCIOColorSpace") message = "OCIOColorSpace... '{}'" - node["in_colorspace"].setValue(colorspace) + # no need to set input colorspace since it is driven by + # working colorspace node["out_colorspace"].setValue(baking_colorspace) colorspace = baking_colorspace @@ -1041,10 +1056,12 @@ class ExporterReviewMov(ExporterReview): self.log.debug("Representation... `{}`".format(self.data)) - self.clean_nodes(product_name) + # self.clean_nodes(product_name) nuke.scriptSave() return self.data + + def _shift_to_previous_node_and_temp(self, product_name, node, message): self._temp_nodes[product_name].append(node) self.previous_node = node self.log.debug(message.format(self._temp_nodes[product_name])) From eb672f534f8b2ec9ef439d833b68fd66bcea3974 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 23:16:28 +0200 Subject: [PATCH 093/101] linting fixes --- server_addon/nuke/client/ayon_nuke/api/lib.py | 4 ++-- server_addon/nuke/client/ayon_nuke/api/plugin.py | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/lib.py b/server_addon/nuke/client/ayon_nuke/api/lib.py index 490205dff3..e8c2023cf9 100644 --- a/server_addon/nuke/client/ayon_nuke/api/lib.py +++ b/server_addon/nuke/client/ayon_nuke/api/lib.py @@ -507,8 +507,8 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): # check if the node is avalon tracked try: # check if data available on the node - test = node[DATA_GROUP_KEY].value() - except NameError as e: + _ = node[DATA_GROUP_KEY].value() + except NameError: # if it doesn't then create it if create: node = set_avalon_knob_data(node) diff --git a/server_addon/nuke/client/ayon_nuke/api/plugin.py b/server_addon/nuke/client/ayon_nuke/api/plugin.py index 129ac45361..ed4be3c4a8 100644 --- a/server_addon/nuke/client/ayon_nuke/api/plugin.py +++ b/server_addon/nuke/client/ayon_nuke/api/plugin.py @@ -1,4 +1,3 @@ -import dis import nuke import re import os @@ -845,7 +844,7 @@ class ExporterReviewMov(ExporterReview): with maintained_selection(): self.log.info("Saving nodes as file... ") # create nk path - path = os.path.splitext(self.path)[0] + ".nk" + path = f"{os.path.splitext(self.path)[0]}.nk" # save file to the path if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) @@ -882,8 +881,7 @@ class ExporterReviewMov(ExporterReview): add_custom_tags = kwargs.get("add_custom_tags", []) - self.log.info( - "__ add_custom_tags: `{0}`".format(add_custom_tags)) + self.log.info(f"__ add_custom_tags: `{add_custom_tags}`") product_name = self.instance.data["productName"] self._temp_nodes[product_name] = [] @@ -1021,12 +1019,11 @@ class ExporterReviewMov(ExporterReview): self.log.info("`mov64_write_timecode` knob was not found") write_node["raw"].setValue(1) + # connect write_node.setInput(0, self.previous_node) self._temp_nodes[product_name].append(write_node) - self.log.debug("Write... `{}`".format( - self._temp_nodes[product_name]) - ) + self.log.debug(f"Write... `{self._temp_nodes[product_name]}`") # ---------- end nodes creation # ---------- render or save to nk @@ -1054,9 +1051,9 @@ class ExporterReviewMov(ExporterReview): colorspace=colorspace, ) - self.log.debug("Representation... `{}`".format(self.data)) + self.log.debug(f"Representation... `{self.data}`") - # self.clean_nodes(product_name) + self.clean_nodes(product_name) nuke.scriptSave() return self.data From 87ecae829e98d31a2d7b664ee3060074eee64ce2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 23:17:26 +0200 Subject: [PATCH 094/101] linting fix --- server_addon/nuke/server/settings/conversion.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/nuke/server/settings/conversion.py b/server_addon/nuke/server/settings/conversion.py index 3e1ca3a0a9..192b3f4d13 100644 --- a/server_addon/nuke/server/settings/conversion.py +++ b/server_addon/nuke/server/settings/conversion.py @@ -1,8 +1,6 @@ import re from typing import Any -from .publish_plugins import DEFAULT_PUBLISH_PLUGIN_SETTINGS - def _get_viewer_config_from_string(input_string): """Convert string to display and viewer string From d59ea421a08176c124d0f2e716ea5a377b24e4a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:32:29 +0200 Subject: [PATCH 095/101] set version dialog can change version for whole selection --- .../sceneinventory/select_version_dialog.py | 18 ++++- client/ayon_core/tools/sceneinventory/view.py | 75 ++++++++++++------- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/select_version_dialog.py b/client/ayon_core/tools/sceneinventory/select_version_dialog.py index 1945d71a6d..098bc724e2 100644 --- a/client/ayon_core/tools/sceneinventory/select_version_dialog.py +++ b/client/ayon_core/tools/sceneinventory/select_version_dialog.py @@ -67,8 +67,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox): self._combo_view = combo_view self._status_delegate = status_delegate self._items_by_id = {} + self._status_visible = True def paintEvent(self, event): + if not self._status_visible: + return super().paintEvent(event) + painter = QtWidgets.QStylePainter(self) option = QtWidgets.QStyleOptionComboBox() self.initStyleOption(option) @@ -120,6 +124,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox): self.setCurrentIndex(index) + def set_status_visible(self, visible): + header = self._combo_view.header() + header.setSectionHidden(1, not visible) + self._status_visible = visible + self.update() + def get_item_by_id(self, item_id): return self._items_by_id[item_id] @@ -195,10 +205,16 @@ class SelectVersionDialog(QtWidgets.QDialog): def select_index(self, index): self._versions_combobox.set_current_index(index) + def set_status_visible(self, visible): + self._versions_combobox.set_status_visible(visible) + @classmethod - def ask_for_version(cls, version_options, index=None, parent=None): + def ask_for_version( + cls, version_options, index=None, show_statuses=True, parent=None + ): dialog = cls(parent) dialog.set_versions(version_options) + dialog.set_status_visible(show_statuses) if index is not None: dialog.select_index(index) dialog.exec_() diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 770d0d903d..33fe3b516f 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -683,37 +683,51 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids ) + product_ids = { + repre_info.product_id + for repre_info in repre_info_by_id.values() + } active_repre_info = repre_info_by_id[active_repre_id] - active_product_id = active_repre_info.product_id active_version_id = active_repre_info.version_id - filtered_repre_info_by_id = { - repre_id: repre_info - for repre_id, repre_info in repre_info_by_id.items() - if repre_info.product_id == active_product_id - } - filtered_container_item_ids = { - item_id - for item_id, container_item in container_items_by_id.items() - if container_item.representation_id in filtered_repre_info_by_id - } - version_items_by_id = self._controller.get_version_items( - {active_product_id} - )[active_product_id] + active_product_id = active_repre_info.product_id + version_items_by_product_id = self._controller.get_version_items( + product_ids + ) + version_items = list( + version_items_by_product_id[active_product_id].values() + ) + versions = {version_item.version for version_item in version_items} + product_ids_by_version = collections.defaultdict(set) + for version_items_by_id in version_items_by_product_id.values(): + for version_item in version_items_by_id.values(): + version = version_item.version + _prod_version = version + if _prod_version < 0: + _prod_version = -1 + product_ids_by_version[_prod_version].add( + version_item.product_id + ) + if version in versions: + continue + versions.add(version) + version_items.append(version_item) def version_sorter(item): hero_value = 0 - version = item.version - if version < 0: + i_version = item.version + if i_version < 0: hero_value = 1 - version = abs(version) - return version, hero_value + i_version = abs(i_version) + return i_version, hero_value - version_items = list(version_items_by_id.values()) version_items.sort(key=version_sorter, reverse=True) - status_items_by_name = { - status_item.name: status_item - for status_item in self._controller.get_project_status_items() - } + show_statuses = len(product_ids) == 1 + status_items_by_name = {} + if show_statuses: + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items() + } version_options = [] active_version_idx = 0 @@ -743,17 +757,28 @@ class SceneInventoryView(QtWidgets.QTreeView): version_option = SelectVersionDialog.ask_for_version( version_options, active_version_idx, + show_statuses=show_statuses, parent=self ) if version_option is None: return - version = version_option.version + product_version = version = version_option.version if version < 0: + product_version = -1 version = HeroVersionType(version) + product_ids = product_ids_by_version[product_version] + + filtered_item_ids = set() + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + repre_info = repre_info_by_id[repre_id] + if repre_info.product_id in product_ids: + filtered_item_ids.add(container_item.item_id) + self._update_containers_to_version( - filtered_container_item_ids, version + filtered_item_ids, version ) def _show_switch_dialog(self, item_ids): From 8c5f46dc13c22c0612936374126c2db81e9c747f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:41:11 +0200 Subject: [PATCH 096/101] Refactor saving and opening workfiles, display push message. - Refactored saving and opening workfiles in `main()` function. - Display a message indicating the pushed workfile path after pushing to project. --- .../nuke/client/ayon_nuke/api/push_to_project.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/push_to_project.py b/server_addon/nuke/client/ayon_nuke/api/push_to_project.py index f145ed652b..852e5d0e31 100644 --- a/server_addon/nuke/client/ayon_nuke/api/push_to_project.py +++ b/server_addon/nuke/client/ayon_nuke/api/push_to_project.py @@ -70,7 +70,6 @@ def main(): anatomy, project_settings=project_settings ) - # Save current workfile. current_file = host.current_file() host.save_file(current_file) @@ -109,8 +108,11 @@ def main(): node["file"].setValue(path.replace("\\", "/")) # Save current workfile to new context. - basename = os.path.basename(current_file) - host.save_file(os.path.join(workdir, basename)) + pushed_workfile = os.path.join( + workdir, os.path.basename(current_file)) + host.save_file(pushed_workfile) - # Open current contex workfile. + # Open current context workfile. host.open_file(current_file) + + nuke.message(f"Pushed to project: \n{pushed_workfile}") From 9f85d4a2405648d672c6178a9ae5b693ebdb6a96 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:41:29 +0200 Subject: [PATCH 097/101] Remove unnecessary callback for workfile builder in pipeline.py. Improve script load settings. --- server_addon/nuke/client/ayon_nuke/api/pipeline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/pipeline.py b/server_addon/nuke/client/ayon_nuke/api/pipeline.py index 8a9b5cb666..edf1e2dc03 100644 --- a/server_addon/nuke/client/ayon_nuke/api/pipeline.py +++ b/server_addon/nuke/client/ayon_nuke/api/pipeline.py @@ -160,9 +160,6 @@ def add_nuke_callbacks(): # template builder callbacks nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") - # TODO: remove this callback once workfile builder will be removed - nuke.addOnCreate(process_workfile_builder, nodeClass="Root") - # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) From 4c8a6f07ff1df67c849b1d2f009c9be412494ddc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:42:42 +0200 Subject: [PATCH 098/101] Remove unused process_workfile_builder function from pipeline module The commit removes the unused process_workfile_builder function from the pipeline module. --- server_addon/nuke/client/ayon_nuke/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/pipeline.py b/server_addon/nuke/client/ayon_nuke/api/pipeline.py index edf1e2dc03..2ba430c272 100644 --- a/server_addon/nuke/client/ayon_nuke/api/pipeline.py +++ b/server_addon/nuke/client/ayon_nuke/api/pipeline.py @@ -37,8 +37,6 @@ from .lib import ( INSTANCE_DATA_KNOB, get_main_window, WorkfileSettings, - # TODO: remove this once workfile builder will be removed - process_workfile_builder, start_workfile_template_builder, launch_workfiles_app, check_inventory_versions, From 0e1c0c506fcb45ce4eddfc76312ce3b7f7e0d0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 14 Jun 2024 12:22:30 +0200 Subject: [PATCH 099/101] Update server_addon/nuke/client/ayon_nuke/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/nuke/client/ayon_nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/lib.py b/server_addon/nuke/client/ayon_nuke/api/lib.py index e8c2023cf9..905521255f 100644 --- a/server_addon/nuke/client/ayon_nuke/api/lib.py +++ b/server_addon/nuke/client/ayon_nuke/api/lib.py @@ -1614,7 +1614,7 @@ class WorkfileSettings(object): workfile_settings["monitor_out_lut"], workfile_settings["monitor_lut"] ) - workfile_settings_output |= monitor_lut_data + workfile_settings_output.update(monitor_lut_data) workfile_settings_output.update( { "workingSpaceLUT": workfile_settings["working_space"], From a33758792cdf228324280661c6759130706d12ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 14 Jun 2024 12:22:42 +0200 Subject: [PATCH 100/101] Update server_addon/nuke/client/ayon_nuke/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/nuke/client/ayon_nuke/api/plugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/plugin.py b/server_addon/nuke/client/ayon_nuke/api/plugin.py index ed4be3c4a8..fc30f328c7 100644 --- a/server_addon/nuke/client/ayon_nuke/api/plugin.py +++ b/server_addon/nuke/client/ayon_nuke/api/plugin.py @@ -638,10 +638,7 @@ class ExporterReview(object): from . import lib as opnlib nuke_imageio = opnlib.get_nuke_imageio_settings() - if ( - "baking_target" in nuke_imageio.keys() - and nuke_imageio["baking_target"]["enabled"] - ): + if nuke_imageio["baking_target"]["enabled"]: return nuke_imageio["baking_target"] else: # viewer is having display and view keys only and it is From 768afdb2d83ea5078ae731a5fe6d066254a2e4aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 12:25:11 +0200 Subject: [PATCH 101/101] Update imageio and viewer settings to version 0.2.3, refactor functions for consistency. --- server_addon/nuke/server/settings/conversion.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server_addon/nuke/server/settings/conversion.py b/server_addon/nuke/server/settings/conversion.py index 192b3f4d13..2e9e07e354 100644 --- a/server_addon/nuke/server/settings/conversion.py +++ b/server_addon/nuke/server/settings/conversion.py @@ -46,7 +46,7 @@ def _get_viewer_config_from_string(input_string): return (display, viewer) -def _convert_imageio_baking_0_2_2(overrides): +def _convert_imageio_baking_0_2_3(overrides): if "baking" not in overrides: return @@ -67,7 +67,7 @@ def _convert_imageio_baking_0_2_2(overrides): } -def _convert_viewers_0_2_2(overrides): +def _convert_viewers_0_2_3(overrides): if "viewer" not in overrides: return @@ -89,7 +89,7 @@ def _convert_viewers_0_2_2(overrides): } -def _convert_imageio_configs_0_2_2(overrides): +def _convert_imageio_configs_0_2_3(overrides): """Image IO settings had changed. 0.2.2. is the latest version using the old way. @@ -99,11 +99,11 @@ def _convert_imageio_configs_0_2_2(overrides): imageio_overrides = overrides["imageio"] - _convert_imageio_baking_0_2_2(imageio_overrides) - _convert_viewers_0_2_2(imageio_overrides) + _convert_imageio_baking_0_2_3(imageio_overrides) + _convert_viewers_0_2_3(imageio_overrides) -def _convert_extract_intermediate_files_0_2_2(publish_overrides): +def _convert_extract_intermediate_files_0_2_3(publish_overrides): """Extract intermediate files settings had changed. 0.2.2. is the latest version using the old way. @@ -131,13 +131,13 @@ def _convert_extract_intermediate_files_0_2_2(publish_overrides): def _convert_publish_plugins(overrides): if "publish" not in overrides: return - _convert_extract_intermediate_files_0_2_2(overrides["publish"]) + _convert_extract_intermediate_files_0_2_3(overrides["publish"]) def convert_settings_overrides( source_version: str, overrides: dict[str, Any], ) -> dict[str, Any]: - _convert_imageio_configs_0_2_2(overrides) + _convert_imageio_configs_0_2_3(overrides) _convert_publish_plugins(overrides) return overrides