From 507adfa9368f24d9eaff47e5d386cad08671ab9e Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 5 Jun 2023 18:39:32 +0200 Subject: [PATCH 001/298] Refactor: Blender new publisher --- openpype/hosts/blender/api/ops.py | 6 ++++- openpype/hosts/blender/api/plugin.py | 25 ++++++++++++++++--- .../blender/plugins/create/create_action.py | 2 +- .../plugins/create/create_animation.py | 2 +- .../blender/plugins/create/create_camera.py | 2 +- .../blender/plugins/create/create_layout.py | 2 +- .../blender/plugins/create/create_model.py | 2 +- .../plugins/create/create_pointcache.py | 2 +- .../blender/plugins/create/create_review.py | 2 +- .../blender/plugins/create/create_rig.py | 2 +- .../blender/plugins/publish/extract_blend.py | 5 +++- 11 files changed, 39 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 208c11cfe8..22c590d4bd 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -275,6 +275,10 @@ class LaunchCreator(LaunchQtApp): def before_window_show(self): self._window.refresh() + def execute(self, context): + host_tools.show_publisher(tab="create") + return {"FINISHED"} + class LaunchLoader(LaunchQtApp): """Launch Avalon Loader.""" @@ -299,7 +303,7 @@ class LaunchPublisher(LaunchQtApp): bl_label = "Publish..." def execute(self, context): - host_tools.show_publish() + host_tools.show_publisher(tab="publish") return {"FINISHED"} diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fb87d08cce..221c8d8936 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,7 +6,8 @@ from typing import Dict, List, Optional import bpy from openpype.pipeline import ( - LegacyCreator, + Creator, + CreatedInstance, LoaderPlugin, ) from .pipeline import AVALON_CONTAINERS @@ -134,10 +135,11 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class Creator(LegacyCreator): - """Base class for Creator plug-ins.""" +class BlenderCreator(Creator): + """Base class for Blender Creator plug-ins.""" defaults = ['Main'] + # Deprecated? def process(self): collection = bpy.data.collections.new(name=self.data["subset"]) bpy.context.scene.collection.children.link(collection) @@ -150,6 +152,23 @@ class Creator(LegacyCreator): return collection + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + """Override abstract method from Creator. + Create new instance and store it. + + Args: + subset_name(str): Subset name of created instance. + instance_data(dict): Base data for instance. + pre_create_data(dict): Data based on pre creation attributes. + Those may affect how creator works. + """ + instance = CreatedInstance( + self.family, subset_name, instance_data + ) + + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 0203ba74c0..effbccd430 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -7,7 +7,7 @@ import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib -class CreateAction(openpype.hosts.blender.api.plugin.Creator): +class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): """Action output for character rigs""" name = "actionMain" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index bc2840952b..1b9bbcacd9 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateAnimation(plugin.Creator): +class CreateAnimation(plugin.BlenderCreator): """Animation output for character rigs""" name = "animationMain" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 7a770a3e77..c72f2b92ff 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateCamera(plugin.Creator): +class CreateCamera(plugin.BlenderCreator): """Polygonal static geometry""" name = "cameraMain" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 73ed683256..ba75df6735 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateLayout(plugin.Creator): +class CreateLayout(plugin.BlenderCreator): """Layout output for character rigs""" name = "layoutMain" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 51fc6683f6..a7e71622ea 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateModel(plugin.Creator): +class CreateModel(plugin.BlenderCreator): """Polygonal static geometry""" name = "modelMain" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 65cf18472d..0555d956de 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreatePointcache(plugin.Creator): +class CreatePointcache(plugin.BlenderCreator): """Polygonal static geometry""" name = "pointcacheMain" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 914f249891..58c26e0324 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateReview(plugin.Creator): +class CreateReview(plugin.BlenderCreator): """Single baked camera""" name = "reviewDefault" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 08cc46ee3e..7a0393f0ba 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateRig(plugin.Creator): +class CreateRig(plugin.BlenderCreator): """Artist-friendly rig with controls to direct motion""" name = "rigMain" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index d4f26b4f3c..fba9a861a0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -5,7 +5,7 @@ import bpy from openpype.pipeline import publish -class ExtractBlend(publish.Extractor): +class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a blend file.""" label = "Extract Blend" @@ -16,6 +16,9 @@ class ExtractBlend(publish.Extractor): def process(self, instance): # Define extract output file path + if not self.is_active(instance.data): + return + stagingdir = self.staging_dir(instance) filename = f"{instance.name}.blend" filepath = os.path.join(stagingdir, filename) From e06dfbb8e8dbf1a92f2164a0b929968384d1aaff Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 6 Jun 2023 16:09:09 +0200 Subject: [PATCH 002/298] draft implementation for blender creator refactor --- openpype/hosts/blender/api/plugin.py | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 221c8d8936..39d0f5e662 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -152,6 +152,29 @@ class BlenderCreator(Creator): return collection + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators shared data. + + Create `blender_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `blender_cached_legacy_subsets` key and fill it with + all legacy subsets from this family as a value. # key or value? + + Args: + shared_data(Dict[str, Any]): Shared data. + + Return: + Dict[str, Any]: Shared data with cached subsets. + """ + if not shared_data.get('blender_cached_subsets'): + cache = {} + cache_legacy = {} + + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -168,6 +191,58 @@ class BlenderCreator(Creator): self.family, subset_name, instance_data ) + collection = bpy.data.collections.new(name=self.data['subset']) + bpy.context.scene.collection.children.link(collection) + + if (self.options or {}).get("useSelection"): + for obj in get_selection(): + collection.objects.link(obj) + + + def collect_instances(self): + """Override abstract method from BaseCreator. + Collect existing instances related to this creator plugin.""" + for ( + instance_data in self.cache_subsets( + self.collection_shared_data + ).get('blender_cached_subsets') + ): + # Process only instances that were created by this creator + creator_id = instance_data.get('creator_identifier') + + if creator_id == self.identifier: + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data, self + ) + + # Add instance to create context + self.add_instance_to_context(instance) + + + def update_instances(self, update_list): + """Override abstract method from BaseCreator. + Store changes of existing instances so they can be recollected. + + Args: + update_list(List[UpdateData]): Changed instances + and their changes, as a list of tuples.""" + for created_instance, _changes in update_list: + data = created_instance.data_to_store() + + # TODO + + + def remove_instances(self, instances: List[CreatedInstance]): + """Override abstract method from BaseCreator. + Method called when instances are removed. + + Args: + instance(List[CreatedInstance]): Instance objects to remove. + """ + for instance in instances: + self._remove_instance_from_context(instance) + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" From 3c198694a9be7dace95e5e027b604f23e3f2bdfe Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 6 Jun 2023 18:03:02 +0200 Subject: [PATCH 003/298] blender creator cache subsets --- openpype/hosts/blender/api/plugin.py | 59 +++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 39d0f5e662..9a982c45e7 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -10,7 +10,11 @@ from openpype.pipeline import ( CreatedInstance, LoaderPlugin, ) -from .pipeline import AVALON_CONTAINERS +from .pipeline import ( + AVALON_CONTAINERS, + AVALON_INSTANCES, + AVALON_PROPERTY, +) from .ops import ( MainThreadItem, execute_in_main_thread @@ -174,6 +178,44 @@ class BlenderCreator(Creator): cache = {} cache_legacy = {} + avalon_instances = bpy.data.collections.get(AVALON_INSTANCES) + if avalon_instances: + for obj in bpy.data.collections.get(AVALON_INSTANCES).objects: + avalon_prop = obj.get(AVALON_PROPERTY, {}) + if avalon_prop.get('id') == 'pyblish.avalon.instance': + creator_id = avalon_prop.get('creator_identifier') + + if creator_id: + # Creator instance + cache.setdefault(creator_id, []).append( + avalon_prop + ) + else: + family = avalon_prop.get('family') + if family: + # Legacy creator instance + cache_legacy.setdefault(family, []).append( + avalon_prop + ) + + for col in bpy.data.collections: + avalon_prop = col.get(AVALON_PROPERTY, {}) + if avalon_prop.get('id') == 'pyblish.avalon.instance': + creaor_id = avalon_prop.get('creator_identifier') + + if creator_id: + # Creator instance + cache.setdefault(creator_id, []).append(avalon_prop) + else: + family = avalon_prop.get('family') + if family: + cache_legacy.setdefault(family, []) + if family: + # Legacy creator instance + cache_legacy.setdefault(family, []).append( + avalon_prop + ) + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict @@ -202,11 +244,16 @@ class BlenderCreator(Creator): def collect_instances(self): """Override abstract method from BaseCreator. Collect existing instances related to this creator plugin.""" - for ( - instance_data in self.cache_subsets( - self.collection_shared_data - ).get('blender_cached_subsets') - ): + + # Cache subsets in shared data + self.cache_subsets(self.collection_shared_data) + + # Get cached subsets + cached_subsets = self.collection_shared_data.get('blender_cached_subsets') + if not cached_subsets: + return + + for instance_data in cached_subsets: # Process only instances that were created by this creator creator_id = instance_data.get('creator_identifier') From d4c030e77d38b7db3fc50c98d0d43ca6d6beae8e Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 16 Jun 2023 11:16:42 +0200 Subject: [PATCH 004/298] Add identifiers to blender creators. Fix wrong method name --- openpype/hosts/blender/api/plugin.py | 2 +- openpype/hosts/blender/plugins/create/create_action.py | 1 + openpype/hosts/blender/plugins/create/create_animation.py | 1 + openpype/hosts/blender/plugins/create/create_camera.py | 1 + openpype/hosts/blender/plugins/create/create_layout.py | 1 + openpype/hosts/blender/plugins/create/create_model.py | 1 + openpype/hosts/blender/plugins/create/create_pointcache.py | 1 + openpype/hosts/blender/plugins/create/create_review.py | 1 + openpype/hosts/blender/plugins/create/create_rig.py | 1 + 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 9a982c45e7..c13af363c5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -264,7 +264,7 @@ class BlenderCreator(Creator): ) # Add instance to create context - self.add_instance_to_context(instance) + self._add_instance_to_context(instance) def update_instances(self, update_list): diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index effbccd430..5f4ded3688 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api import lib class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): """Action output for character rigs""" + identifier = "io.openpype.creators.blender.action" name = "actionMain" label = "Action" family = "action" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 1b9bbcacd9..277c588610 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateAnimation(plugin.BlenderCreator): """Animation output for character rigs""" + identifier = "io.openpype.creators.blender.animation" name = "animationMain" label = "Animation" family = "animation" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c72f2b92ff..9086c44c5f 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateCamera(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.camera" name = "cameraMain" label = "Camera" family = "camera" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ba75df6735..ae567e6495 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateLayout(plugin.BlenderCreator): """Layout output for character rigs""" + identifier = "io.openpype.creators.blender.layout" name = "layoutMain" label = "Layout" family = "layout" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index a7e71622ea..46196ab383 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateModel(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.model" name = "modelMain" label = "Model" family = "model" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 0555d956de..4c434202c7 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreatePointcache(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.pointcache" name = "pointcacheMain" label = "Point Cache" family = "pointcache" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 58c26e0324..87774aed7a 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateReview(plugin.BlenderCreator): """Single baked camera""" + identifier = "io.openpype.creators.blender.review" name = "reviewDefault" label = "Review" family = "review" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 7a0393f0ba..84924a659b 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateRig(plugin.BlenderCreator): """Artist-friendly rig with controls to direct motion""" + identifier = "io.openpype.creators.blender.rig" name = "rigMain" label = "Rig" family = "rig" From efa294defcc625acdd36e7948bde3f75b15beb88 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 16 Jun 2023 17:54:28 +0200 Subject: [PATCH 005/298] fixed wrong variable name --- openpype/hosts/blender/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index c13af363c5..02436e3583 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -201,7 +201,7 @@ class BlenderCreator(Creator): for col in bpy.data.collections: avalon_prop = col.get(AVALON_PROPERTY, {}) if avalon_prop.get('id') == 'pyblish.avalon.instance': - creaor_id = avalon_prop.get('creator_identifier') + creator_id = avalon_prop.get('creator_identifier') if creator_id: # Creator instance From b681173dc5b2ee34862ffa41dbc3fc452c682a56 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 3 Jul 2023 17:44:36 +0200 Subject: [PATCH 006/298] draft blenderhost class implementation --- openpype/hosts/blender/api/__init__.py | 2 + openpype/hosts/blender/api/pipeline.py | 41 ++++++++++++++++++- .../blender/blender_addon/startup/init.py | 4 +- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index e15f1193a5..ce2b444997 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -10,6 +10,7 @@ from .pipeline import ( ls, publish, containerise, + BlenderHost, ) from .plugin import ( @@ -47,6 +48,7 @@ __all__ = [ "ls", "publish", "containerise", + "BlenderHost", "Creator", "Loader", diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 84af0904f0..935981da86 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -6,10 +6,14 @@ from typing import Callable, Dict, Iterator, List, Optional import bpy from . import lib -from . import ops +from . import ops, properties import pyblish.api +from openpype.host import( + HostBase, + IPublishHost, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( schema, @@ -47,6 +51,39 @@ IS_HEADLESS = bpy.app.background log = Logger.get_logger(__name__) +class BlenderHost(HostBase, IPublishHost): + name = "blender" + + def install(self): + """Override install method from HostBase. + Install Blender host functionality.""" + install() + + def ls(self) -> Iterator: + """List containers from active Blender scene.""" + return ls() + + def get_context_data(self): + """Override abstract method from IPublishHost. + Get global data related to creation-publishing from workfile. + + Returns: + dict: Context data stored using 'update_context_data'. + """ + return bpy.context.scene.openpype_context + + def update_context_data(self, data, changes): + """Override abstract method from IPublishHost. + Store global context data to workfile. + + Args: + data (dict): New data as are. + changes (dict): Only data that has been changed. Each value has + tuple with '(, )' value. + """ + bpy.context.scene.openpype_context.update(data) + + def pype_excepthook_handler(*args): traceback.print_exception(*args) @@ -72,6 +109,7 @@ def install(): if not IS_HEADLESS: ops.register() + properties.register() def uninstall(): @@ -86,6 +124,7 @@ def uninstall(): if not IS_HEADLESS: ops.unregister() + properties.unregister() def show_message(title, message): diff --git a/openpype/hosts/blender/blender_addon/startup/init.py b/openpype/hosts/blender/blender_addon/startup/init.py index 8dbff8a91d..603691675d 100644 --- a/openpype/hosts/blender/blender_addon/startup/init.py +++ b/openpype/hosts/blender/blender_addon/startup/init.py @@ -1,9 +1,9 @@ from openpype.pipeline import install_host -from openpype.hosts.blender import api +from openpype.hosts.blender.api import BlenderHost def register(): - install_host(api) + install_host(BlenderHost()) def unregister(): From 3b7ed1d71f86f6bd04029cc6ec0e369aff94c365 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 10:55:04 +0200 Subject: [PATCH 007/298] Fix update_container_data error --- openpype/hosts/blender/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 935981da86..968c70089b 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -81,7 +81,7 @@ class BlenderHost(HostBase, IPublishHost): changes (dict): Only data that has been changed. Each value has tuple with '(, )' value. """ - bpy.context.scene.openpype_context.update(data) + bpy.context.scene.openpype_context |= data def pype_excepthook_handler(*args): From 7bdbf0252211167c065aa8f3e622d48e725c0bf8 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 16:07:44 +0200 Subject: [PATCH 008/298] Added properties to blender host --- openpype/hosts/blender/api/properties.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 openpype/hosts/blender/api/properties.py diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py new file mode 100644 index 0000000000..ffc1dea733 --- /dev/null +++ b/openpype/hosts/blender/api/properties.py @@ -0,0 +1,23 @@ +import bpy +from bpy.utils import register_classes_factory + +class OpenpypeContext(bpy.types.PropertyGroup): + pass + +classes = [OpenpypeContext] + +factory_register, factory_unregister = register_classes_factory(classes) + +def register(): + """Register the properties.""" + factory_register() + + bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( + name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} + ) + +def unregister(): + """Unregister the properties.""" + factory_unregister() + + del bpy.types.Scene.openpype_context From d9960e84d44dab94ac5fec6bf45315ce79ab5d37 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 17:54:40 +0200 Subject: [PATCH 009/298] Fixed errors during create --- openpype/hosts/blender/api/pipeline.py | 2 +- openpype/hosts/blender/api/plugin.py | 6 +++--- openpype/hosts/blender/api/properties.py | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 968c70089b..935981da86 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -81,7 +81,7 @@ class BlenderHost(HostBase, IPublishHost): changes (dict): Only data that has been changed. Each value has tuple with '(, )' value. """ - bpy.context.scene.openpype_context |= data + bpy.context.scene.openpype_context.update(data) def pype_excepthook_handler(*args): diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 02436e3583..91068244c5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -230,13 +230,13 @@ class BlenderCreator(Creator): Those may affect how creator works. """ instance = CreatedInstance( - self.family, subset_name, instance_data + self.family, subset_name, instance_data, self ) - collection = bpy.data.collections.new(name=self.data['subset']) + collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - if (self.options or {}).get("useSelection"): + if pre_create_data.get("useSelection"): for obj in get_selection(): collection.objects.link(obj) diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py index ffc1dea733..c6b5ffe011 100644 --- a/openpype/hosts/blender/api/properties.py +++ b/openpype/hosts/blender/api/properties.py @@ -4,7 +4,7 @@ from bpy.utils import register_classes_factory class OpenpypeContext(bpy.types.PropertyGroup): pass -classes = [OpenpypeContext] +classes = [] # [OpenpypeContext] factory_register, factory_unregister = register_classes_factory(classes) @@ -12,9 +12,10 @@ def register(): """Register the properties.""" factory_register() - bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( - name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} - ) + bpy.types.Scene.openpype_context = {} + # bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( + # name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} + # ) def unregister(): """Unregister the properties.""" From cf66d12ef20feb2e1dcf326d4888434a3c506bdb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 5 Jul 2023 17:36:42 +0200 Subject: [PATCH 010/298] Fixes to base creator, creators compatibility --- openpype/hosts/blender/api/plugin.py | 3 + .../blender/plugins/create/create_action.py | 27 +++++++++ .../plugins/create/create_animation.py | 47 +++++++++++++++- .../blender/plugins/create/create_camera.py | 56 ++++++++++++++++++- .../blender/plugins/create/create_layout.py | 45 ++++++++++++++- .../blender/plugins/create/create_model.py | 45 ++++++++++++++- .../plugins/create/create_pointcache.py | 22 ++++++++ .../blender/plugins/create/create_review.py | 43 +++++++++++++- .../blender/plugins/create/create_rig.py | 45 ++++++++++++++- 9 files changed, 315 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 91068244c5..153897cb9a 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -216,6 +216,8 @@ class BlenderCreator(Creator): avalon_prop ) + return shared_data + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict @@ -232,6 +234,7 @@ class BlenderCreator(Creator): instance = CreatedInstance( self.family, subset_name, instance_data, self ) + self._add_instance_to_context(instance) collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 5f4ded3688..32e924d758 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -16,6 +16,33 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): family = "action" icon = "male" + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + + name = openpype.hosts.blender.api.plugin.asset_name( + instance_data["asset"], subset_name + ) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + instance_data['task'] = get_current_task_name() + lib.imprint(collection, instance_data) + + if pre_create_data.get("useSelection"): + for obj in lib.get_selection(): + if (obj.animation_data is not None + and obj.animation_data.action is not None): + + empty_obj = bpy.data.objects.new(name=name, + object_data=None) + empty_obj.animation_data_create() + empty_obj.animation_data.action = obj.animation_data.action + empty_obj.animation_data.action.name = name + collection.objects.link(empty_obj) + + return collection + + # Deprecated def process(self): asset = self.data["asset"] diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 277c588610..12478fd7e5 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -16,12 +16,53 @@ class CreateAnimation(plugin.BlenderCreator): family = "animation" icon = "male" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + # name = self.name + # if not name: + name = plugin.asset_name(instance_data["asset"], subset_name) + # asset_group = bpy.data.objects.new(name=name, object_data=None) + # asset_group.empty_display_type = 'SINGLE_ARROW' + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif pre_create_data.get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 9086c44c5f..6b9d1b7f73 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -16,12 +16,62 @@ class CreateCamera(plugin.BlenderCreator): family = "camera" icon = "video-camera" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + else: + plugin.deselect_all() + camera = bpy.data.cameras.new(subset_name) + camera_obj = bpy.data.objects.new(subset_name, camera) + + instances.objects.link(camera_obj) + + camera_obj.select_set(True) + asset_group.select_set(True) + bpy.context.view_layer.objects.active = asset_group + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ae567e6495..24aa277349 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -16,12 +16,51 @@ class CreateLayout(plugin.BlenderCreator): family = "layout" icon = "cubes" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 46196ab383..2e713dd661 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -16,12 +16,51 @@ class CreateModel(plugin.BlenderCreator): family = "model" icon = "cube" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 4c434202c7..5932315bc8 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -16,6 +16,28 @@ class CreatePointcache(plugin.BlenderCreator): family = "pointcache" icon = "gears" + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + + name = openpype.hosts.blender.api.plugin.asset_name( + instance_data["asset"], subset_name + ) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + instance_data['task'] = get_current_task_name() + lib.imprint(collection, instance_data) + + if pre_create_data.get("useSelection"): + objects = lib.get_selection() + for obj in objects: + collection.objects.link(obj) + if obj.type == 'EMPTY': + objects.extend(obj.children) + + return collection + + # Deprecated def process(self): """ Run the creator on Blender main thread""" mti = ops.MainThreadItem(self._process) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 87774aed7a..4b5cfb4c35 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -16,12 +16,49 @@ class CreateReview(plugin.BlenderCreator): family = "review" icon = "video-camera" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif pre_create_data.get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 84924a659b..5ebf952d4a 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -16,12 +16,51 @@ class CreateRig(plugin.BlenderCreator): family = "rig" icon = "wheelchair" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: From e0e7c965856795776e86cfab83a582b2fd4c134d Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 10 Jul 2023 14:59:31 +0200 Subject: [PATCH 011/298] blender creator instances added to context --- openpype/hosts/blender/plugins/create/create_action.py | 5 ++++- openpype/hosts/blender/plugins/create/create_animation.py | 6 +++++- openpype/hosts/blender/plugins/create/create_camera.py | 6 +++++- openpype/hosts/blender/plugins/create/create_layout.py | 6 +++++- openpype/hosts/blender/plugins/create/create_model.py | 6 +++++- openpype/hosts/blender/plugins/create/create_pointcache.py | 7 +++++-- openpype/hosts/blender/plugins/create/create_review.py | 6 +++++- openpype/hosts/blender/plugins/create/create_rig.py | 6 +++++- 8 files changed, 39 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 32e924d758..6c2c6d98ce 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib @@ -19,6 +19,9 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) name = openpype.hosts.blender.api.plugin.asset_name( instance_data["asset"], subset_name diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 12478fd7e5..ec77569889 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateAnimation(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 6b9d1b7f73..55ea07d90f 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateCamera(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 24aa277349..60812d7fc9 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateLayout(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 2e713dd661..59bbae5bc4 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateModel(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 5932315bc8..bb843c5a22 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -19,8 +19,11 @@ class CreatePointcache(plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) - name = openpype.hosts.blender.api.plugin.asset_name( + name = plugin.asset_name( instance_data["asset"], subset_name ) collection = bpy.data.collections.new(name=name) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 4b5cfb4c35..1191fb95f1 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateReview(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 5ebf952d4a..752f3f7b18 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateRig(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) From 6eae2c264d57b4ed5395de03950e09bc54ae7acb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 12 Jul 2023 15:54:41 +0200 Subject: [PATCH 012/298] Add dict to imprint, type hints in blenderhost, imprint in create --- openpype/hosts/blender/api/lib.py | 2 +- openpype/hosts/blender/api/pipeline.py | 4 ++-- openpype/hosts/blender/api/plugin.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 9bb560c364..76bef09ceb 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -188,7 +188,7 @@ def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict): # Support values evaluated at imprint value = value() - if not isinstance(value, (int, float, bool, str, list)): + if not isinstance(value, (int, float, bool, str, list, dict)): raise TypeError(f"Unsupported type: {type(value)}") imprint_data[key] = value diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 935981da86..4c06b1959e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -63,7 +63,7 @@ class BlenderHost(HostBase, IPublishHost): """List containers from active Blender scene.""" return ls() - def get_context_data(self): + def get_context_data(self) -> dict: """Override abstract method from IPublishHost. Get global data related to creation-publishing from workfile. @@ -72,7 +72,7 @@ class BlenderHost(HostBase, IPublishHost): """ return bpy.context.scene.openpype_context - def update_context_data(self, data, changes): + def update_context_data(self, data: dict, changes: dict): """Override abstract method from IPublishHost. Store global context data to workfile. diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 153897cb9a..fe1e4ce81c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -238,6 +238,7 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) + imprint(collection, instance_data) if pre_create_data.get("useSelection"): for obj in get_selection(): From 7617d1e0ad63042d39a3c626f6e614100d2bd879 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 19 Jul 2023 20:28:45 +0200 Subject: [PATCH 013/298] Set shared data, edit loop in cached_subsets --- openpype/hosts/blender/api/plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fe1e4ce81c..97a3dfa6a6 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -216,6 +216,9 @@ class BlenderCreator(Creator): avalon_prop ) + shared_data["blender_cached_subsets"] = cache + shared_data["blender_cached_legacy_subsets"] = cache_legacy + return shared_data @@ -257,14 +260,17 @@ class BlenderCreator(Creator): if not cached_subsets: return - for instance_data in cached_subsets: + for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator + data = dict() + for key, value in instance_data.items(): + data[key] = value creator_id = instance_data.get('creator_identifier') if creator_id == self.identifier: # Create instance object from existing data instance = CreatedInstance.from_existing( - instance_data, self + data, self ) # Add instance to create context From aeebd78cc71fb3edbe51157c60da849f62e727f3 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 20 Jul 2023 15:31:23 +0200 Subject: [PATCH 014/298] Collect instances functional --- openpype/hosts/blender/api/plugin.py | 4 ++-- openpype/hosts/blender/plugins/create/create_action.py | 3 +++ openpype/hosts/blender/plugins/create/create_animation.py | 3 +++ openpype/hosts/blender/plugins/create/create_camera.py | 3 +++ openpype/hosts/blender/plugins/create/create_layout.py | 3 +++ openpype/hosts/blender/plugins/create/create_model.py | 3 +++ openpype/hosts/blender/plugins/create/create_pointcache.py | 3 +++ openpype/hosts/blender/plugins/create/create_review.py | 3 +++ openpype/hosts/blender/plugins/create/create_rig.py | 3 +++ 9 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 97a3dfa6a6..5dd352ff6c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -262,10 +262,10 @@ class BlenderCreator(Creator): for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator - data = dict() + data = {} for key, value in instance_data.items(): data[key] = value - creator_id = instance_data.get('creator_identifier') + creator_id = data.get('creator_identifier') if creator_id == self.identifier: # Create instance object from existing data diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 6c2c6d98ce..2c0d248994 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -29,6 +29,9 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index ec77569889..538c0455ac 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -47,6 +47,9 @@ class CreateAnimation(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 55ea07d90f..ae85273353 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -45,6 +45,9 @@ class CreateCamera(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 60812d7fc9..cce422b229 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -44,6 +44,9 @@ class CreateLayout(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 59bbae5bc4..57f9e79aa1 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -44,6 +44,9 @@ class CreateModel(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index bb843c5a22..f95f79ae78 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -29,6 +29,9 @@ class CreatePointcache(plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 1191fb95f1..8472600c2f 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -43,6 +43,9 @@ class CreateReview(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 752f3f7b18..a60f2a72ee 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -44,6 +44,9 @@ class CreateRig(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance From 4d03c1c0e60b8e32248a4bb96a326c7c3a3762fb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 20 Jul 2023 18:26:59 +0200 Subject: [PATCH 015/298] Set subset in inst data, use update to set it --- .../hosts/blender/plugins/create/create_action.py | 13 +++++++++---- .../blender/plugins/create/create_animation.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_camera.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_layout.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_model.py | 13 +++++++++---- .../blender/plugins/create/create_pointcache.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_review.py | 13 +++++++++---- openpype/hosts/blender/plugins/create/create_rig.py | 13 +++++++++---- 8 files changed, 72 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 2c0d248994..6951e86c46 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -28,10 +28,15 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 538c0455ac..9e7dfbaf84 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -46,10 +46,15 @@ class CreateAnimation(plugin.BlenderCreator): # asset_group.empty_display_type = 'SINGLE_ARROW' asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index ae85273353..c5987779c0 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -44,10 +44,15 @@ class CreateCamera(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index cce422b229..cb61799f4a 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -43,10 +43,15 @@ class CreateLayout(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 57f9e79aa1..ccf3668d98 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -43,10 +43,15 @@ class CreateModel(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index f95f79ae78..e27cb22389 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -28,10 +28,15 @@ class CreatePointcache(plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 8472600c2f..afeaea951b 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -42,10 +42,15 @@ class CreateReview(plugin.BlenderCreator): name = plugin.asset_name(instance_data["asset"], subset_name) asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index a60f2a72ee..2db766f7ed 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -43,10 +43,15 @@ class CreateRig(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance From 8236e46ca93b562feb66df51a81ec74c6785586f Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 31 Jul 2023 17:16:40 +0200 Subject: [PATCH 016/298] implement IWorkfileHost for Blender host --- openpype/hosts/blender/api/pipeline.py | 70 +++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 4c06b1959e..c3f8f06694 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -12,6 +12,7 @@ import pyblish.api from openpype.host import( HostBase, + IWorkfileHost, IPublishHost, ) from openpype.client import get_asset_by_name @@ -33,6 +34,14 @@ from openpype.lib import ( ) import openpype.hosts.blender from openpype.settings import get_project_settings +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root, +) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) @@ -51,7 +60,7 @@ IS_HEADLESS = bpy.app.background log = Logger.get_logger(__name__) -class BlenderHost(HostBase, IPublishHost): +class BlenderHost(HostBase, IWorkfileHost, IPublishHost): name = "blender" def install(self): @@ -63,6 +72,65 @@ class BlenderHost(HostBase, IPublishHost): """List containers from active Blender scene.""" return ls() + def get_workfile_extensions(self) -> List[str]: + """Override get_workfile_extensions method from IWorkfileHost. + Get workfile possible extensions. + + Returns: + List[str]: Workfile extensions. + """ + return file_extensions() + + def save_workfile(self, dst_path: str = None): + """Override save_workfile method from IWorkfileHost. + Save currently opened workfile. + + Args: + dst_path (str): Where the current scene should be saved. Or use + current path if `None` is passed. + """ + save_file(dst_path if dst_path else bpy.data.filepath) + + def open_workfile(self, filepath: str): + """Override open_workfile method from IWorkfileHost. + Open workfile at specified filepath in the host. + + Args: + filepath (str): Path to workfile. + """ + open_file(filepath) + + def get_current_workfile(self) -> str: + """Override get_current_workfile method from IWorkfileHost. + Retrieve currently opened workfile path. + + Returns: + str: Path to currently opened workfile. + """ + return current_file() + + def workfile_has_unsaved_changes(self) -> bool: + """Override wokfile_has_unsaved_changes method from IWorkfileHost. + Returns True if opened workfile has no unsaved changes. + + Returns: + bool: True if scene is saved and False if it has unsaved + modifications. + """ + return has_unsaved_changes() + + def work_root(self, session) -> str: + """Override work_root method from IWorkfileHost. + Modify workdir per host. + + Args: + session (dict): Session context data. + + Returns: + str: Path to new workdir. + """ + return work_root(session) + def get_context_data(self) -> dict: """Override abstract method from IPublishHost. Get global data related to creation-publishing from workfile. From e9d022bab37f67391267788c3de940c1db0f955d Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 2 Aug 2023 16:13:34 +0200 Subject: [PATCH 017/298] Handling optional plugins --- openpype/hosts/blender/plugins/publish/extract_abc.py | 2 +- .../blender/plugins/publish/extract_abc_animation.py | 5 ++++- .../blender/plugins/publish/extract_blend_animation.py | 5 ++++- .../blender/plugins/publish/extract_camera_abc.py | 2 +- .../blender/plugins/publish/extract_camera_fbx.py | 2 +- openpype/hosts/blender/plugins/publish/extract_fbx.py | 2 +- .../blender/plugins/publish/extract_fbx_animation.py | 5 ++++- .../hosts/blender/plugins/publish/extract_layout.py | 2 +- .../hosts/blender/plugins/publish/extract_playblast.py | 2 +- .../plugins/publish/increment_workfile_version.py | 6 +++++- .../blender/plugins/publish/integrate_animation.py | 6 +++++- .../blender/plugins/publish/validate_mesh_has_uv.py | 10 ++++++++-- .../blender/plugins/publish/validate_object_mode.py | 6 +++++- 13 files changed, 41 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 7b6c4d7ae7..5af8104344 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractABC(publish.Extractor): +class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as ABC.""" label = "Extract ABC" diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 44b2ba3761..0b6b93b7a5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -6,7 +6,10 @@ from openpype.pipeline import publish from openpype.hosts.blender.api import plugin -class ExtractAnimationABC(publish.Extractor): +class ExtractAnimationABC( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract as ABC.""" label = "Extract Animation ABC" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 477411b73d..3a5b788c9e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -5,7 +5,10 @@ import bpy from openpype.pipeline import publish -class ExtractBlendAnimation(publish.Extractor): +class ExtractBlendAnimation( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract a blend file.""" label = "Extract Blend" diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 036be7bf3c..2a327f4d65 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractCameraABC(publish.Extractor): +class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract camera as ABC.""" label = "Extract Camera (ABC)" diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 315994140e..8e5e4d37d4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -6,7 +6,7 @@ from openpype.pipeline import publish from openpype.hosts.blender.api import plugin -class ExtractCamera(publish.Extractor): +class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as the camera as FBX.""" label = "Extract Camera (FBX)" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 0ad797c226..8ace6a43a7 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractFBX(publish.Extractor): +class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as FBX.""" label = "Extract FBX" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 062b42e99d..04f50f8207 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -10,7 +10,10 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractAnimationFBX(publish.Extractor): +class ExtractAnimationFBX( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract as animation.""" label = "Extract FBX" diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index f2d04f1178..8445560bba 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractLayout(publish.Extractor): +class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a layout.""" label = "Extract Layout" diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 196e75b8cc..82ddbc1fc2 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -9,7 +9,7 @@ from openpype.hosts.blender.api import capture from openpype.hosts.blender.api.lib import maintained_time -class ExtractPlayblast(publish.Extractor): +class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): """ Extract viewport playblast. diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 6ace14d77c..176668f366 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -1,8 +1,12 @@ import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.blender.api.workio import save_file -class IncrementWorkfileVersion(pyblish.api.ContextPlugin): +class IncrementWorkfileVersion( + pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin +): """Increment current workfile version.""" order = pyblish.api.IntegratorOrder + 0.9 diff --git a/openpype/hosts/blender/plugins/publish/integrate_animation.py b/openpype/hosts/blender/plugins/publish/integrate_animation.py index d9a85bc79b..b7e5423fa8 100644 --- a/openpype/hosts/blender/plugins/publish/integrate_animation.py +++ b/openpype/hosts/blender/plugins/publish/integrate_animation.py @@ -1,9 +1,13 @@ import json import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin -class IntegrateAnimation(pyblish.api.InstancePlugin): +class IntegrateAnimation( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Generate a JSON file for animation.""" label = "Integrate Animation" diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index edf47193be..687371b362 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -4,11 +4,17 @@ import bpy import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, +) import openpype.hosts.blender.api.action -class ValidateMeshHasUvs(pyblish.api.InstancePlugin): +class ValidateMeshHasUvs( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Validate that the current mesh has UV's.""" order = ValidateContentsOrder diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index ac60e00f89..d8d2e3c8bf 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -3,10 +3,14 @@ from typing import List import bpy import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin import openpype.hosts.blender.api.action -class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): +class ValidateObjectIsInObjectMode( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Validate that the objects in the instance are in Object Mode.""" order = pyblish.api.ValidatorOrder - 0.01 From 656a42dd525317f83b22a232f8dc68756d2c389a Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 9 Aug 2023 11:45:58 +0200 Subject: [PATCH 018/298] Handling instance noce --- openpype/hosts/blender/api/plugin.py | 13 ++++++++++--- .../hosts/blender/plugins/create/create_action.py | 8 ++++++++ .../blender/plugins/create/create_animation.py | 6 ++++++ .../hosts/blender/plugins/create/create_camera.py | 6 ++++++ .../hosts/blender/plugins/create/create_layout.py | 6 ++++++ .../hosts/blender/plugins/create/create_model.py | 6 ++++++ .../blender/plugins/create/create_pointcache.py | 6 ++++++ .../hosts/blender/plugins/create/create_review.py | 6 ++++++ openpype/hosts/blender/plugins/create/create_rig.py | 6 ++++++ 9 files changed, 60 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 5dd352ff6c..fe0d53f84e 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -234,13 +234,20 @@ class BlenderCreator(Creator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ + collection = bpy.data.collections.new(name=subset_name) + bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + + instance_data["instance_node"] = instance_node + instance = CreatedInstance( self.family, subset_name, instance_data, self ) self._add_instance_to_context(instance) - collection = bpy.data.collections.new(name=subset_name) - bpy.context.scene.collection.children.link(collection) imprint(collection, instance_data) if pre_create_data.get("useSelection"): @@ -287,7 +294,7 @@ class BlenderCreator(Creator): for created_instance, _changes in update_list: data = created_instance.data_to_store() - # TODO + imprint(data.get("instance_node", {}), data) def remove_instances(self, instances: List[CreatedInstance]): diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 6951e86c46..a43258082c 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -28,6 +28,11 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -35,8 +40,11 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) + from pprint import pprint + pprint(instance_data) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 9e7dfbaf84..842292f0f9 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -46,6 +46,11 @@ class CreateAnimation(plugin.BlenderCreator): # asset_group.empty_display_type = 'SINGLE_ARROW' asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -53,6 +58,7 @@ class CreateAnimation(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c5987779c0..8360abbc7d 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -44,6 +44,11 @@ class CreateCamera(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -51,6 +56,7 @@ class CreateCamera(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index cb61799f4a..b4b127f32c 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -43,6 +43,11 @@ class CreateLayout(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateLayout(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index ccf3668d98..5cb2de0fae 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -43,6 +43,11 @@ class CreateModel(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateModel(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index e27cb22389..a95ae547c8 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -28,6 +28,11 @@ class CreatePointcache(plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -35,6 +40,7 @@ class CreatePointcache(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(collection, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index afeaea951b..1bcafbc265 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -42,6 +42,11 @@ class CreateReview(plugin.BlenderCreator): name = plugin.asset_name(instance_data["asset"], subset_name) asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -49,6 +54,7 @@ class CreateReview(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 2db766f7ed..e93f4a171f 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -43,6 +43,11 @@ class CreateRig(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateRig(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) From 5f7847c6973d378779e3b27a0ef25ed3fa5a9e1c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 9 Aug 2023 12:04:06 +0200 Subject: [PATCH 019/298] Removed useless pprint --- openpype/hosts/blender/plugins/create/create_action.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index a43258082c..ee7ca092b5 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -43,8 +43,6 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): "instance_node": instance_node, } ) - from pprint import pprint - pprint(instance_data) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): From f52c9f0a38de8c9d3f50d6a1a721f20afdfa1b5f Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 10 Aug 2023 10:37:46 +0200 Subject: [PATCH 020/298] Enhance instance_node, wip remove_inst, rm extra empty lines --- openpype/hosts/blender/api/plugin.py | 22 ++++++++++++++----- .../blender/plugins/create/create_action.py | 8 ++++--- .../plugins/create/create_animation.py | 12 ++++++---- .../blender/plugins/create/create_camera.py | 12 ++++++---- .../blender/plugins/create/create_layout.py | 12 ++++++---- .../blender/plugins/create/create_model.py | 12 ++++++---- .../plugins/create/create_pointcache.py | 8 ++++--- .../blender/plugins/create/create_review.py | 12 ++++++---- .../blender/plugins/create/create_rig.py | 12 ++++++---- 9 files changed, 75 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fe0d53f84e..9967f9479c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -155,7 +155,6 @@ class BlenderCreator(Creator): return collection - @staticmethod def cache_subsets(shared_data): """Cache instances for Creators shared data. @@ -221,7 +220,6 @@ class BlenderCreator(Creator): return shared_data - def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -254,7 +252,6 @@ class BlenderCreator(Creator): for obj in get_selection(): collection.objects.link(obj) - def collect_instances(self): """Override abstract method from BaseCreator. Collect existing instances related to this creator plugin.""" @@ -283,7 +280,6 @@ class BlenderCreator(Creator): # Add instance to create context self._add_instance_to_context(instance) - def update_instances(self, update_list): """Override abstract method from BaseCreator. Store changes of existing instances so they can be recollected. @@ -296,7 +292,6 @@ class BlenderCreator(Creator): imprint(data.get("instance_node", {}), data) - def remove_instances(self, instances: List[CreatedInstance]): """Override abstract method from BaseCreator. Method called when instances are removed. @@ -305,6 +300,23 @@ class BlenderCreator(Creator): instance(List[CreatedInstance]): Instance objects to remove. """ for instance in instances: + outliner_entity = instance.data.get("instance_node", {}).get( + "datablock" + ) + if not outliner_entity: + continue + + if isinstance(outliner_entity, bpy.types.Collection): + for children in outliner_entity.children_recursive: + if isinstance(children, bpy.types.Collection): + bpy.data.collections.remove(children) + else: + bpy.data.objects.remove(children) + + bpy.data.collections.remove(outliner_entity) + elif isinstance(outliner_entity, bpy.types.Object): + bpy.data.objects.remove(outliner_entity) + self._remove_instance_from_context(instance) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index ee7ca092b5..7404e7e037 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -5,6 +5,7 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): @@ -29,9 +30,10 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection[AVALON_PROPERTY] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 842292f0f9..c2f24250e1 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateAnimation(plugin.BlenderCreator): @@ -47,9 +50,10 @@ class CreateAnimation(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 8360abbc7d..a83124cbe7 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateCamera(plugin.BlenderCreator): @@ -45,9 +48,10 @@ class CreateCamera(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index b4b127f32c..4fb76ef41e 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateLayout(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateLayout(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 5cb2de0fae..45f1d66ad9 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateModel(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateModel(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index a95ae547c8..7aa3b22466 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -5,6 +5,7 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY class CreatePointcache(plugin.BlenderCreator): @@ -29,9 +30,10 @@ class CreatePointcache(plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection[AVALON_PROPERTY] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 1bcafbc265..a6ca5b1b92 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateReview(plugin.BlenderCreator): @@ -43,9 +46,10 @@ class CreateReview(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index e93f4a171f..f7c99b0b03 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateRig(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateRig(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { From 371d7405ec10257fe52c1259c383604862b080f7 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 10 Aug 2023 11:19:05 +0200 Subject: [PATCH 021/298] Apply creator change to bl base creator --- openpype/hosts/blender/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 9967f9479c..060d229eb5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -235,9 +235,10 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection["instance_node"] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data["instance_node"] = instance_node From e832cf59ccb2bbf6a436184f1143ebf53231c5df Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 12 Sep 2023 15:49:33 +0200 Subject: [PATCH 022/298] Fix Cannot pickle object error --- openpype/hosts/blender/api/plugin.py | 5 +---- openpype/hosts/blender/plugins/create/create_action.py | 1 - openpype/hosts/blender/plugins/create/create_animation.py | 1 - openpype/hosts/blender/plugins/create/create_camera.py | 1 - openpype/hosts/blender/plugins/create/create_layout.py | 1 - openpype/hosts/blender/plugins/create/create_model.py | 1 - openpype/hosts/blender/plugins/create/create_pointcache.py | 1 - openpype/hosts/blender/plugins/create/create_review.py | 1 - openpype/hosts/blender/plugins/create/create_rig.py | 1 - 9 files changed, 1 insertion(+), 12 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 060d229eb5..73d8fc0ed5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -237,7 +237,6 @@ class BlenderCreator(Creator): collection["instance_node"] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data["instance_node"] = instance_node @@ -267,9 +266,7 @@ class BlenderCreator(Creator): for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator - data = {} - for key, value in instance_data.items(): - data[key] = value + data = instance_data.to_dict() creator_id = data.get('creator_identifier') if creator_id == self.identifier: diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 7404e7e037..d766fce038 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -32,7 +32,6 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index c2f24250e1..88ae9e5996 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -52,7 +52,6 @@ class CreateAnimation(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index a83124cbe7..026b5739d6 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -50,7 +50,6 @@ class CreateCamera(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 4fb76ef41e..f46ae58a43 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -49,7 +49,6 @@ class CreateLayout(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 45f1d66ad9..069b78626b 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -49,7 +49,6 @@ class CreateModel(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 7aa3b22466..3054b81ef5 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -32,7 +32,6 @@ class CreatePointcache(plugin.BlenderCreator): collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index a6ca5b1b92..10a96c94fd 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -48,7 +48,6 @@ class CreateReview(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index f7c99b0b03..8daffe638d 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -49,7 +49,6 @@ class CreateRig(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( From 78b33370dc3418083d323d465332ff28aa32fbeb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 18 Sep 2023 17:59:33 +0200 Subject: [PATCH 023/298] Fix collectreview --- openpype/hosts/blender/plugins/publish/collect_review.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 3bf2e39e24..66a3d7b5e8 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -16,10 +16,14 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug(f"instance: {instance}") + datablock = bpy.data.collections.get( + instance.data.get("instance_node", {}).get("name", "") + ) + # get cameras cameras = [ obj - for obj in instance + for obj in datablock.all_objects if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA" ] From a526260b461a85e951a1fc87697a56df70afa7d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 17:57:09 +0200 Subject: [PATCH 024/298] 'get_assets' function can find folders by path and by name --- openpype/client/server/entities.py | 57 ++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 16223d3d91..c1e27eabb9 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -183,6 +183,20 @@ def get_asset_by_name(project_name, asset_name, fields=None): return None + +def _folders_query(project_name, con, fields, **kwargs): + if fields is None or "tasks" in fields: + folders = get_folders_with_tasks( + con, project_name, fields=fields, **kwargs + ) + + else: + folders = con.get_folders(project_name, fields=fields, **kwargs) + + for folder in folders: + yield folder + + def get_assets( project_name, asset_ids=None, @@ -199,22 +213,51 @@ def get_assets( active = None con = get_server_api_connection() + fields = folder_fields_v3_to_v4(fields, con) kwargs = dict( folder_ids=asset_ids, - folder_names=asset_names, parent_ids=parent_ids, active=active, - fields=fields ) + if not asset_names: + for folder in _folders_query(project_name, con, fields, **kwargs): + yield convert_v4_folder_to_v3(folder, project_name) + return - if fields is None or "tasks" in fields: - folders = get_folders_with_tasks(con, project_name, **kwargs) + new_asset_names = set() + folder_paths = set() + if asset_names: + for name in asset_names: + if "/" in name: + folder_paths.add(name) + else: + new_asset_names.add(name) - else: - folders = con.get_folders(project_name, **kwargs) + if folder_paths: + for folder in _folders_query( + project_name, con, fields, folder_paths=folder_paths, **kwargs + ): + yield convert_v4_folder_to_v3(folder, project_name) - for folder in folders: + if not new_asset_names: + return + + folders_by_name = collections.defaultdict(list) + for folder in _folders_query( + project_name, con, fields, folder_names=new_asset_names, **kwargs + ): + folders_by_name[folder["name"]].append(folder) + + for name, folders in folders_by_name.items(): + folder = next( + ( + folder + for folder in folders + if folder["path"] == name + ), + folders[0] + ) yield convert_v4_folder_to_v3(folder, project_name) From 331c4833ade7acf5ab0db7952e9821b817354d2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 17:58:34 +0200 Subject: [PATCH 025/298] added helper function 'get_asset_name_identifier' to receive folder path in AYON mode and asset name in openpype --- openpype/client/__init__.py | 4 ++++ openpype/client/entities.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 7831afd8ad..a313d6b3cc 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -43,6 +43,8 @@ from .entities import ( get_thumbnail_id_from_source, get_workfile_info, + + get_asset_name_identifier, ) from .entity_links import ( @@ -105,4 +107,6 @@ __all__ = ( "get_linked_representation_id", "create_project", + + "get_asset_name_identifier", ) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 5d9654c611..d085f90028 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -4,3 +4,22 @@ if not AYON_SERVER_ENABLED: from .mongo.entities import * else: from .server.entities import * + + +def get_asset_name_identifier(asset_doc): + """Get asset name identifier by asset document. + + This function is added because of AYON implementation where name + identifier is not just a name but full path. + + Asset document must have "name" key, and "data.parents" when in AYON mode. + + Args: + asset_doc (dict[str, Any]): Asset document. + """ + + if not AYON_SERVER_ENABLED: + return asset_doc["name"] + parents = list(asset_doc["data"]["parents"]) + parents.append(asset_doc["name"]) + return "/".join(parents) From 4c6ec4b9bcb245892421b14bee26d7564f33f971 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:00:58 +0200 Subject: [PATCH 026/298] instances in AYON mode have 'folderPath' instead of 'asset' --- openpype/pipeline/create/context.py | 37 ++++++++++++++++--- .../publish/collect_from_create_context.py | 3 ++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 25f03ddd3b..333ab25f54 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -11,7 +11,12 @@ from contextlib import contextmanager import pyblish.logic import pyblish.api -from openpype.client import get_assets, get_asset_by_name +from openpype import AYON_SERVER_ENABLED +from openpype.client import ( + get_assets, + get_asset_by_name, + get_asset_name_identifier, +) from openpype.settings import ( get_system_settings, get_project_settings @@ -922,9 +927,19 @@ class CreatedInstance: self._orig_data = copy.deepcopy(data) # Pop family and subset to prevent unexpected changes + # TODO change to 'productType' and 'productName' in AYON data.pop("family", None) data.pop("subset", None) + if AYON_SERVER_ENABLED: + asset_name = data.pop("asset", None) + if "folderPath" not in data: + data["folderPath"] = asset_name + + elif "folderPath" in data: + asset_name = data.pop("folderPath").split("/")[-1] + if "asset" not in data: + data["asset"] = asset_name # QUESTION Does it make sense to have data stored as ordered dict? self._data = collections.OrderedDict() @@ -1268,6 +1283,8 @@ class CreatedInstance: def has_set_asset(self): """Asset name is set in data.""" + if AYON_SERVER_ENABLED: + return "folderPath" in self._data return "asset" in self._data @property @@ -2229,7 +2246,10 @@ class CreateContext: task_names_by_asset_name = {} for instance in instances: task_name = instance.get("task") - asset_name = instance.get("asset") + if AYON_SERVER_ENABLED: + asset_name = instance.get("folderPath") + else: + asset_name = instance.get("asset") if asset_name: task_names_by_asset_name[asset_name] = set() if task_name: @@ -2240,15 +2260,18 @@ class CreateContext: for asset_name in task_names_by_asset_name.keys() if asset_name is not None ] + fields = {"name", "data.tasks"} + if AYON_SERVER_ENABLED: + fields |= {"data.parents"} asset_docs = list(get_assets( self.project_name, asset_names=asset_names, - fields=["name", "data.tasks"] + fields=fields )) task_names_by_asset_name = {} for asset_doc in asset_docs: - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) tasks = asset_doc.get("data", {}).get("tasks") or {} task_names_by_asset_name[asset_name] = set(tasks.keys()) @@ -2256,7 +2279,11 @@ class CreateContext: if not instance.has_valid_asset or not instance.has_valid_task: continue - asset_name = instance["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance["folderPath"] + else: + asset_name = instance["asset"] + if asset_name not in task_names_by_asset_name: instance.set_asset_invalid(True) continue diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 8806a13ca0..84f6141069 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -4,6 +4,7 @@ import os import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.host import IPublishHost from openpype.pipeline import legacy_io, registered_host from openpype.pipeline.create import CreateContext @@ -38,6 +39,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): for created_instance in create_context.instances: instance_data = created_instance.data_to_store() + if AYON_SERVER_ENABLED: + instance_data["asset"] = instance_data.pop("folderPath") if instance_data["active"]: thumbnail_path = thumbnail_paths_by_instance_id.get( created_instance.id From 0ddd95eacf0fda0b058fccc3388d0816aa9d4145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:02:41 +0200 Subject: [PATCH 027/298] use 'get_asset_name_identifier' in pipeline logic --- openpype/pipeline/context_tools.py | 5 ++--- openpype/pipeline/create/utils.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13630ae7ca..e20099759a 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -17,6 +17,7 @@ from openpype.client import ( get_asset_by_id, get_asset_by_name, version_is_latest, + get_asset_name_identifier, ) from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager @@ -568,14 +569,12 @@ def compute_session_changes( Dict[str, str]: Changes in the Session dictionary. """ - changes = {} - # Get asset document and asset if not asset_doc: task_name = None asset_name = None else: - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) # Detect any changes compared session mapping = { diff --git a/openpype/pipeline/create/utils.py b/openpype/pipeline/create/utils.py index 2ef1f02bd6..ce4af8f474 100644 --- a/openpype/pipeline/create/utils.py +++ b/openpype/pipeline/create/utils.py @@ -1,6 +1,11 @@ import collections -from openpype.client import get_assets, get_subsets, get_last_versions +from openpype.client import ( + get_assets, + get_subsets, + get_last_versions, + get_asset_name_identifier, +) def get_last_versions_for_instances( @@ -52,10 +57,10 @@ def get_last_versions_for_instances( asset_docs = get_assets( project_name, asset_names=subset_names_by_asset_name.keys(), - fields=["name", "_id"] + fields=["name", "_id", "data.parents"] ) asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] + asset_doc["_id"]: get_asset_name_identifier(asset_doc) for asset_doc in asset_docs } if not asset_names_by_id: From 922c481d95b6e377a8d7433819360c45179fcffa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:03:02 +0200 Subject: [PATCH 028/298] use 'get_asset_name_identifier' for 'AVALON_ASSET' on app start --- openpype/lib/applications.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index ff5e27c122..4d75a01e1d 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -12,6 +12,7 @@ from abc import ABCMeta, abstractmethod import six from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR +from openpype.client import get_asset_name_identifier from openpype.settings import ( get_system_settings, get_project_settings, @@ -1728,7 +1729,9 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): "AVALON_APP_NAME": app.full_name } if asset_doc: - context_env["AVALON_ASSET"] = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) + context_env["AVALON_ASSET"] = asset_name + if task_name: context_env["AVALON_TASK"] = task_name From f38c3f395e68df0a33a3b1ba135dbbcf0cf7f899 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:03:20 +0200 Subject: [PATCH 029/298] use 'get_asset_name_identifier' in global plugins --- .../publish/collect_anatomy_instance_data.py | 20 +++++++++++-------- openpype/plugins/publish/collect_audio.py | 10 ++++++---- .../publish/extract_hierarchy_to_ayon.py | 4 ++-- .../publish/validate_editorial_asset_name.py | 9 ++++++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index b4f4d6a16a..cc6da9b2c3 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -30,7 +30,8 @@ import pyblish.api from openpype.client import ( get_assets, get_subsets, - get_last_versions + get_last_versions, + get_asset_name_identifier, ) from openpype.pipeline.version_start import get_versioning_start @@ -60,6 +61,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self.log.debug("Querying asset documents for instances.") context_asset_doc = context.data.get("assetEntity") + context_asset_name = None + if context_asset_doc: + context_asset_name = get_asset_name_identifier(context_asset_doc) instances_with_missing_asset_doc = collections.defaultdict(list) for instance in context: @@ -68,15 +72,15 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # There is possibility that assetEntity on instance is already set # which can happen in standalone publisher - if ( - instance_asset_doc - and instance_asset_doc["name"] == _asset_name - ): - continue + if instance_asset_doc: + instance_asset_name = get_asset_name_identifier( + instance_asset_doc) + if instance_asset_name == _asset_name: + continue # Check if asset name is the same as what is in context # - they may be different, e.g. in NukeStudio - if context_asset_doc and context_asset_doc["name"] == _asset_name: + if context_asset_name and context_asset_name == _asset_name: instance.data["assetEntity"] = context_asset_doc else: @@ -93,7 +97,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): asset_docs = get_assets(project_name, asset_names=asset_names) asset_docs_by_name = { - asset_doc["name"]: asset_doc + get_asset_name_identifier(asset_doc): asset_doc for asset_doc in asset_docs } diff --git a/openpype/plugins/publish/collect_audio.py b/openpype/plugins/publish/collect_audio.py index 6aaadfc568..734a625852 100644 --- a/openpype/plugins/publish/collect_audio.py +++ b/openpype/plugins/publish/collect_audio.py @@ -6,6 +6,7 @@ from openpype.client import ( get_subsets, get_last_versions, get_representations, + get_asset_name_identifier, ) from openpype.pipeline.load import get_representation_path_with_anatomy @@ -121,12 +122,13 @@ class CollectAudio(pyblish.api.ContextPlugin): asset_docs = get_assets( project_name, asset_names=asset_names, - fields=["_id", "name"] + fields=["_id", "name", "data.parents"] ) - asset_id_by_name = {} - for asset_doc in asset_docs: - asset_id_by_name[asset_doc["name"]] = asset_doc["_id"] + asset_id_by_name = { + get_asset_name_identifier(asset_doc): asset_doc["_id"] + for asset_doc in asset_docs + } asset_ids = set(asset_id_by_name.values()) # Query subsets with name define by 'audio_subset_name' attr diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index 0d9131718b..fe8cb40ad2 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -8,7 +8,7 @@ from ayon_api import slugify_string from ayon_api.entity_hub import EntityHub from openpype import AYON_SERVER_ENABLED -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_name_identifier from openpype.pipeline.template_data import ( get_asset_template_data, get_task_template_data, @@ -58,7 +58,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): project_name, asset_names=instances_by_asset_name.keys() ) asset_docs_by_name = { - asset_doc["name"]: asset_doc + get_asset_name_identifier(asset_doc): asset_doc for asset_doc in asset_docs } for asset_name, instances in instances_by_asset_name.items(): diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index fca0d8e7f5..b5afc49f2e 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -2,7 +2,7 @@ from pprint import pformat import pyblish.api -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_name_identifier class ValidateEditorialAssetName(pyblish.api.ContextPlugin): @@ -34,8 +34,11 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): [str(p) for p in e["data"]["parents"]] - for e in db_assets} + get_asset_name_identifier(asset_doc): list( + asset_doc["data"]["parents"] + ) + for asset_doc in db_assets + } self.log.debug("__ project_entities: {}".format( pformat(asset_db_docs))) From c505513b056e092ac8dd8c3652776a238046980c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:04:31 +0200 Subject: [PATCH 030/298] use folder path in ayon tools to define current context --- openpype/tools/ayon_launcher/models/actions.py | 6 +++--- openpype/tools/ayon_loader/control.py | 2 +- openpype/tools/ayon_sceneinventory/control.py | 11 ++--------- openpype/tools/ayon_workfiles/control.py | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py index 93ec115734..d7c4219dc2 100644 --- a/openpype/tools/ayon_launcher/models/actions.py +++ b/openpype/tools/ayon_launcher/models/actions.py @@ -402,12 +402,12 @@ class ActionsModel: ) def _prepare_session(self, project_name, folder_id, task_id): - folder_name = None + folder_path = None if folder_id: folder = self._controller.get_folder_entity( project_name, folder_id) if folder: - folder_name = folder["name"] + folder_path = folder["path"] task_name = None if task_id: @@ -417,7 +417,7 @@ class ActionsModel: return { "AVALON_PROJECT": project_name, - "AVALON_ASSET": folder_name, + "AVALON_ASSET": folder_path, "AVALON_TASK": task_name, } diff --git a/openpype/tools/ayon_loader/control.py b/openpype/tools/ayon_loader/control.py index 2b779f5c2e..d2fae35f32 100644 --- a/openpype/tools/ayon_loader/control.py +++ b/openpype/tools/ayon_loader/control.py @@ -289,7 +289,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name = context.get("project_name") asset_name = context.get("asset_name") if project_name and asset_name: - folder = ayon_api.get_folder_by_name( + folder = ayon_api.get_folder_by_path( project_name, asset_name, fields=["id"] ) if folder: diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py index e98b0e307b..6111d7e43b 100644 --- a/openpype/tools/ayon_sceneinventory/control.py +++ b/openpype/tools/ayon_sceneinventory/control.py @@ -70,19 +70,12 @@ class SceneInventoryController: context = self.get_current_context() project_name = context["project_name"] - folder_path = context.get("folder_path") folder_name = context.get("asset_name") folder_id = None - if folder_path: - folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder_name: + folder = ayon_api.get_folder_by_path(project_name, folder_name) if folder: folder_id = folder["id"] - elif folder_name: - for folder in ayon_api.get_folders( - project_name, folder_names=[folder_name] - ): - folder_id = folder["id"] - break self._current_folder_id = folder_id self._current_folder_set = True diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index 3784959caf..d86b04badb 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -427,7 +427,7 @@ class BaseWorkfileController( task_name = context["task_name"] folder_id = None if folder_name: - folder = ayon_api.get_folder_by_name(project_name, folder_name) + folder = ayon_api.get_folder_by_path(project_name, folder_name) if folder: folder_id = folder["id"] From 1ac66764d49af599915a592de0917a4b67977358 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:05:16 +0200 Subject: [PATCH 031/298] use folder path in publisher --- openpype/tools/publisher/control.py | 6 ++- .../tools/publisher/widgets/assets_widget.py | 42 ++++++++++++++++--- openpype/tools/publisher/widgets/widgets.py | 17 ++++++-- openpype/tools/utils/assets_widget.py | 1 + 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a6264303d5..ad87bdf607 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -12,10 +12,12 @@ from abc import ABCMeta, abstractmethod import six import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_asset_by_id, get_subsets, + get_asset_name_identifier, ) from openpype.lib.events import EventSystem from openpype.lib.attribute_definitions import ( @@ -73,6 +75,8 @@ class AssetDocsCache: "data.visualParent": True, "data.tasks": True } + if AYON_SERVER_ENABLED: + projection["data.parents"] = True def __init__(self, controller): self._controller = controller @@ -105,7 +109,7 @@ class AssetDocsCache: elif "tasks" not in asset_doc["data"]: asset_doc["data"]["tasks"] = {} - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) asset_tasks = asset_doc["data"]["tasks"] task_names_by_asset_name[asset_name] = list(asset_tasks.keys()) asset_docs_by_name[asset_name] = asset_doc diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index c536f93c9b..5f74b79c99 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -11,7 +11,8 @@ from openpype.tools.utils import ( from openpype.tools.utils.assets_widget import ( SingleSelectAssetsWidget, ASSET_ID_ROLE, - ASSET_NAME_ROLE + ASSET_NAME_ROLE, + ASSET_PATH_ROLE, ) @@ -31,6 +32,15 @@ class CreateWidgetAssetsWidget(SingleSelectAssetsWidget): self._last_filter_height = None + def get_selected_asset_name(self): + if AYON_SERVER_ENABLED: + selection_model = self._view.selectionModel() + indexes = selection_model.selectedRows() + for index in indexes: + return index.data(ASSET_PATH_ROLE) + return None + return super(CreateWidgetAssetsWidget, self).get_selected_asset_name() + def _check_header_height(self): """Catch header height changes. @@ -100,21 +110,24 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): self._controller = controller self._items_by_name = {} + self._items_by_path = {} self._items_by_asset_id = {} def reset(self): self.clear() self._items_by_name = {} + self._items_by_path = {} self._items_by_asset_id = {} assets_by_parent_id = self._controller.get_asset_hierarchy() items_by_name = {} + items_by_path = {} items_by_asset_id = {} _queue = collections.deque() - _queue.append((self.invisibleRootItem(), None)) + _queue.append((self.invisibleRootItem(), None, None)) while _queue: - parent_item, parent_id = _queue.popleft() + parent_item, parent_id, parent_path = _queue.popleft() children = assets_by_parent_id.get(parent_id) if not children: continue @@ -127,6 +140,9 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): for name in sorted(children_by_name.keys()): child = children_by_name[name] child_id = child["_id"] + child_path = name + if parent_path: + child_path = "{}/{}".format(parent_path, child_path) has_children = bool(assets_by_parent_id.get(child_id)) icon = get_asset_icon(child, has_children) @@ -138,15 +154,18 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): item.setData(icon, QtCore.Qt.DecorationRole) item.setData(child_id, ASSET_ID_ROLE) item.setData(name, ASSET_NAME_ROLE) + item.setData(child_path, ASSET_PATH_ROLE) items_by_name[name] = item + items_by_path[child_path] = item items_by_asset_id[child_id] = item items.append(item) - _queue.append((item, child_id)) + _queue.append((item, child_id, child_path)) parent_item.appendRows(items) self._items_by_name = items_by_name + self._items_by_path = items_by_path self._items_by_asset_id = items_by_asset_id def get_index_by_asset_id(self, asset_id): @@ -156,12 +175,20 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() def get_index_by_asset_name(self, asset_name): - item = self._items_by_name.get(asset_name) + item = None + if AYON_SERVER_ENABLED: + item = self._items_by_path.get(asset_name) + + if item is None: + item = self._items_by_name.get(asset_name) + if item is None: return QtCore.QModelIndex() return item.index() def name_is_valid(self, item_name): + if AYON_SERVER_ENABLED and item_name in self._items_by_path: + return True return item_name in self._items_by_name @@ -296,7 +323,10 @@ class AssetsDialog(QtWidgets.QDialog): index = self._asset_view.currentIndex() asset_name = None if index.isValid(): - asset_name = index.data(ASSET_NAME_ROLE) + if AYON_SERVER_ENABLED: + asset_name = index.data(ASSET_PATH_ROLE) + else: + asset_name = index.data(ASSET_NAME_ROLE) self._selected_asset = asset_name self.done(1) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 1bbe73381f..cc0f7a9f97 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -538,6 +538,7 @@ class AssetsField(BaseClickableFrame): Does not change selected items (assets). """ self._name_input.setText(text) + self._name_input.end(False) def set_selected_items(self, asset_names=None): """Set asset names for selection of instances. @@ -1162,7 +1163,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): invalid_tasks = False for instance in self._current_instances: new_variant_value = instance.get("variant") - new_asset_name = instance.get("asset") + if AYON_SERVER_ENABLED: + new_asset_name = instance.get("folderPath") + else: + new_asset_name = instance.get("asset") new_task_name = instance.get("task") if variant_value is not None: new_variant_value = variant_value @@ -1193,7 +1197,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance["variant"] = variant_value if asset_name is not None: - instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + instance["folderPath"] = asset_name + else: + instance["asset"] = asset_name + instance.set_asset_invalid(False) if task_name is not None: @@ -1282,7 +1290,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) - asset_name = instance.get("asset") or self.unknown_value + if AYON_SERVER_ENABLED: + asset_name = instance.get("folderPath") or self.unknown_value + else: + asset_name = instance.get("asset") or self.unknown_value task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index a45d762c73..b83f4dfcaf 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -36,6 +36,7 @@ ASSET_ID_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 ASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3 ASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4 +ASSET_PATH_ROLE = QtCore.Qt.UserRole + 5 class AssetsView(TreeViewSpinner, DeselectableTreeView): From 69792a5c64446bf4c5b17333f2da14a2b9368e3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:06 +0200 Subject: [PATCH 032/298] use 'get_asset_name_identifier' in aftereffecs --- .../plugins/create/workfile_creator.py | 22 ++++++++++++++++--- .../plugins/publish/collect_workfile.py | 6 ++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py index 2e7b9d4a7e..5dc3d6592d 100644 --- a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED import openpype.hosts.aftereffects.api as api from openpype.client import get_asset_by_name from openpype.pipeline import ( @@ -43,6 +44,14 @@ class AEWorkfileCreator(AutoCreator): task_name = context.get_current_task_name() host_name = context.host_name + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + + if existing_asset_name is None: + existing_asset_name = existing_instance["asset"] + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -50,10 +59,13 @@ class AEWorkfileCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -68,7 +80,7 @@ class AEWorkfileCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -76,6 +88,10 @@ class AEWorkfileCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index dc557f67fc..58d2757840 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -1,6 +1,8 @@ import os import pyblish.api + +from openpype.client import get_asset_name_identifier from openpype.pipeline.create import get_subset_name @@ -48,9 +50,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] + asset_name = get_asset_name_identifier(asset_entity) + instance_data = { "active": True, - "asset": asset_entity["name"], + "asset": asset_name, "task": task, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], From 18fcfa4a4133ac71873ed5ac67e498891b514f96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:17 +0200 Subject: [PATCH 033/298] use 'get_asset_name_identifier' in fusion --- .../fusion/plugins/create/create_workfile.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 8acaaa172f..8063e56413 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -1,6 +1,7 @@ from openpype.hosts.fusion.api import ( get_current_comp ) +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, @@ -68,6 +69,15 @@ class FusionWorkfileCreator(AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.data.get( + "folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.data.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -75,10 +85,13 @@ class FusionWorkfileCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -91,7 +104,7 @@ class FusionWorkfileCreator(AutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -99,6 +112,9 @@ class FusionWorkfileCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name From 279ab08dfacf6f0916d0162f35c11e72567565e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:27 +0200 Subject: [PATCH 034/298] use 'get_asset_name_identifier' in celaction --- .../plugins/publish/collect_celaction_instances.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py index c815c1edd4..875f15fcc5 100644 --- a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py +++ b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py @@ -1,6 +1,8 @@ import os import pyblish.api +from openpype.client import get_asset_name_identifier + class CollectCelactionInstances(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ @@ -17,8 +19,10 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] + asset_name = get_asset_name_identifier(asset_entity) + shared_instance_data = { - "asset": asset_entity["name"], + "asset": asset_name, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], From a5056ea3fbca99c74f63ccafe409bbc650ec612b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:09:43 +0200 Subject: [PATCH 035/298] modified maya to follow new usage of 'asset' value --- openpype/hosts/maya/api/plugin.py | 11 ++++++---- .../maya/plugins/create/create_review.py | 7 +++++- .../maya/plugins/create/create_workfile.py | 22 +++++++++++++++---- .../validate_unreal_staticmesh_naming.py | 3 ++- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3b54954c8a..6c8b5b6b78 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -7,6 +7,7 @@ import six from maya import cmds from maya.app.renderSetup.model import renderSetup +from openpype import AYON_SERVER_ENABLED from openpype.lib import BoolDef, Logger from openpype.settings import get_project_settings from openpype.pipeline import ( @@ -463,14 +464,16 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): # this instance will not have the `instance_node` data yet # until it's been saved/persisted at least once. project_name = self.create_context.get_current_project_name() - + asset_name = self.create_context.get_current_asset_name() instance_data = { - "asset": self.create_context.get_current_asset_name(), "task": self.create_context.get_current_task_name(), "variant": layer.name(), } - asset_doc = get_asset_by_name(project_name, - instance_data["asset"]) + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( layer.name(), instance_data["task"], diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index f60e2406bc..18d661b186 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -2,6 +2,7 @@ import json from maya import cmds +from openpype import AYON_SERVER_ENABLED from openpype.hosts.maya.api import ( lib, plugin @@ -43,7 +44,11 @@ class CreateReview(plugin.MayaCreator): members = cmds.ls(selection=True) project_name = self.project_name - asset_doc = get_asset_by_name(project_name, instance_data["asset"]) + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(project_name, asset_name) task_name = instance_data["task"] preset = lib.get_capture_preset( task_name, diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index d84753cd7f..74629776af 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator -from openpype.client import get_asset_by_name +from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.hosts.maya.api import plugin from maya import cmds @@ -29,16 +30,27 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name + data.update( self.get_dynamic_data( variant, task_name, asset_doc, @@ -50,14 +62,16 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) + asset_name = get_asset_name_identifier(asset_doc) + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 58fa9d02bd..42d3dc3ac8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -102,7 +102,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, cl_r = re.compile(regex_collision) - mesh_name = "{}{}".format(instance.data["asset"], + asset_name = instance.data["assetEntity"]["name"] + mesh_name = "{}{}".format(asset_name, instance.data.get("variant", [])) for obj in collision_set: From 82b1e0b205428dd0496be71bb269bd7d179b67c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:12:08 +0200 Subject: [PATCH 036/298] modified houdini to follow new asset naming --- .../houdini/plugins/create/create_workfile.py | 23 +++++++++--- .../plugins/publish/collect_usd_bootstrap.py | 35 ++++++++++++------- .../plugins/publish/validate_subset_name.py | 10 +++--- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index cc45a6c2a8..04a844bdf5 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.hosts.houdini.api import plugin from openpype.hosts.houdini.api.lib import read, imprint from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER @@ -30,16 +31,27 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.host_name + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderpath"] = asset_name + else: + data["asset"] = asset_name + data.update( self.get_dynamic_data( variant, task_name, asset_doc, @@ -51,15 +63,18 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py index 14a8e3c056..462cf99b9c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py +++ b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py @@ -1,6 +1,10 @@ import pyblish.api -from openpype.client import get_subset_by_name, get_asset_by_name +from openpype.client import ( + get_subset_by_name, + get_asset_by_name, + get_asset_name_identifier, +) import openpype.lib.usdlib as usdlib @@ -51,8 +55,9 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): self.log.debug("Add bootstrap for: %s" % bootstrap) project_name = instance.context.data["projectName"] - asset = get_asset_by_name(project_name, instance.data["asset"]) - assert asset, "Asset must exist: %s" % asset + asset_name = instance.data["asset"] + asset_doc = get_asset_by_name(project_name, asset_name) + assert asset_doc, "Asset must exist: %s" % asset_name # Check which are not about to be created and don't exist yet required = {"shot": ["usdShot"], "asset": ["usdAsset"]}.get(bootstrap) @@ -67,19 +72,21 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): required += list(layers) self.log.debug("Checking required bootstrap: %s" % required) - for subset in required: - if self._subset_exists(project_name, instance, subset, asset): + for subset_name in required: + if self._subset_exists( + project_name, instance, subset_name, asset_doc + ): continue self.log.debug( "Creating {0} USD bootstrap: {1} {2}".format( - bootstrap, asset["name"], subset + bootstrap, asset_name, subset_name ) ) - new = instance.context.create_instance(subset) - new.data["subset"] = subset - new.data["label"] = "{0} ({1})".format(subset, asset["name"]) + new = instance.context.create_instance(subset_name) + new.data["subset"] = subset_name + new.data["label"] = "{0} ({1})".format(subset_name, asset_name) new.data["family"] = "usd.bootstrap" new.data["comment"] = "Automated bootstrap USD file." new.data["publishFamilies"] = ["usd"] @@ -91,21 +98,23 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): for key in ["asset"]: new.data[key] = instance.data[key] - def _subset_exists(self, project_name, instance, subset, asset): + def _subset_exists(self, project_name, instance, subset_name, asset_doc): """Return whether subset exists in current context or in database.""" # Allow it to be created during this publish session context = instance.context + + asset_doc_name = get_asset_name_identifier(asset_doc) for inst in context: if ( - inst.data["subset"] == subset - and inst.data["asset"] == asset["name"] + inst.data["subset"] == subset_name + and inst.data["asset"] == asset_doc_name ): return True # Or, if they already exist in the database we can # skip them too. if get_subset_by_name( - project_name, subset, asset["_id"], fields=["_id"] + project_name, subset_name, asset_doc["_id"], fields=["_id"] ): return True return False diff --git a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py index bb3648f361..7bed74ebb1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py @@ -54,12 +54,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check subset name + asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], - asset_doc=instance.data["assetEntity"], - dynamic_data={"asset": instance.data["asset"]} + asset_doc=asset_doc, + dynamic_data={"asset": asset_doc["name"]} ) if instance.data.get("subset") != subset_name: @@ -76,12 +77,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check subset name + asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], - asset_doc=instance.data["assetEntity"], - dynamic_data={"asset": instance.data["asset"]} + asset_doc=asset_doc, + dynamic_data={"asset": asset_doc["name"]} ) instance.data["subset"] = subset_name From 23c41fe12aedeedd567cd9d290e243a5871c07bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:13:00 +0200 Subject: [PATCH 037/298] modified photoshop to follow new 'asset' name usage --- openpype/hosts/photoshop/lib.py | 22 ++++++++++++++++--- .../plugins/create/create_flatten_image.py | 22 +++++++++++++++---- .../plugins/publish/collect_auto_image.py | 3 ++- .../plugins/publish/collect_auto_review.py | 4 +++- .../plugins/publish/collect_auto_workfile.py | 3 ++- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/photoshop/lib.py b/openpype/hosts/photoshop/lib.py index 9f603a70d2..654528410a 100644 --- a/openpype/hosts/photoshop/lib.py +++ b/openpype/hosts/photoshop/lib.py @@ -1,5 +1,6 @@ import re +from openpype import AYON_SERVER_ENABLED import openpype.hosts.photoshop.api as api from openpype.client import get_asset_by_name from openpype.lib import prepare_template_data @@ -43,6 +44,15 @@ class PSAutoCreator(AutoCreator): asset_name = context.get_current_asset_name() task_name = context.get_current_task_name() host_name = context.host_name + + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.get("folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -50,10 +60,13 @@ class PSAutoCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -70,7 +83,7 @@ class PSAutoCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -78,7 +91,10 @@ class PSAutoCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index afde77fdb4..942f8f4989 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -1,5 +1,6 @@ from openpype.pipeline import CreatedInstance +from openpype import AYON_SERVER_ENABLED from openpype.lib import BoolDef import openpype.hosts.photoshop.api as api from openpype.hosts.photoshop.lib import PSAutoCreator, clean_subset_name @@ -37,6 +38,14 @@ class AutoImageCreator(PSAutoCreator): host_name = context.host_name asset_doc = get_asset_by_name(project_name, asset_name) + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.get("folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: subset_name = self.get_subset_name( self.default_variant, task_name, asset_doc, @@ -44,9 +53,12 @@ class AutoImageCreator(PSAutoCreator): ) data = { - "asset": asset_name, "task": task_name, } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name if not self.active_on_create: data["active"] = False @@ -62,15 +74,17 @@ class AutoImageCreator(PSAutoCreator): new_instance.data_to_store()) elif ( # existing instance from different context - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): subset_name = self.get_subset_name( self.default_variant, task_name, asset_doc, project_name, host_name ) - - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index 77f1a3e91f..038ae9ff6c 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -1,5 +1,6 @@ import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -27,7 +28,7 @@ class CollectAutoImage(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) auto_creator = proj_settings.get( "photoshop", {}).get( diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py index 82ba0ac09c..37e9e8bae8 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -7,6 +7,7 @@ Provides: """ import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -65,7 +66,8 @@ class CollectAutoReview(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + + asset_name = get_asset_name_identifier(asset_doc) subset_name = get_subset_name( family, diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py index 01dc50af40..be5a641d51 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -69,8 +70,8 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) subset_name = get_subset_name( family, variant, From 7c7f9f175c041292e8a11306763a0863cc6bae95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:13:23 +0200 Subject: [PATCH 038/298] modified substance to follow new 'asset' usage --- .../plugins/create/create_workfile.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index d7f31f9dcf..8aa696f11d 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name @@ -41,6 +42,13 @@ class CreateWorkfile(AutoCreator): if instance.creator_identifier == self.identifier ), None) + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: self.log.info("Auto-creating workfile instance...") asset_doc = get_asset_by_name(project_name, asset_name) @@ -48,22 +56,28 @@ class CreateWorkfile(AutoCreator): variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name current_instance = self.create_instance_in_context(subset_name, data) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name From 7d366371a5eec266083721b1ac651813103978fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:17:35 +0200 Subject: [PATCH 039/298] modified most of code in traypublisher to follow new asset usage --- openpype/hosts/traypublisher/api/plugin.py | 11 ++++++++--- .../plugins/create/create_colorspace_look.py | 7 ++++++- .../traypublisher/plugins/create/create_editorial.py | 6 +++++- .../plugins/create/create_movie_batch.py | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 36e041a32c..14c66fa08f 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,7 +1,9 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_subsets, get_last_versions, + get_asset_name_identifier, ) from openpype.lib.attribute_definitions import ( FileDef, @@ -114,7 +116,10 @@ class SettingsCreator(TrayPublishCreator): # Fill 'version_to_use' if version control is enabled if self.allow_version_control: - asset_name = data["asset"] + if AYON_SERVER_ENABLED: + asset_name = data["folderPath"] + else: + asset_name = data["asset"] subset_docs_by_asset_id = self._prepare_next_versions( [asset_name], [subset_name]) version = subset_docs_by_asset_id[asset_name].get(subset_name) @@ -162,10 +167,10 @@ class SettingsCreator(TrayPublishCreator): asset_docs = get_assets( self.project_name, asset_names=asset_names, - fields=["_id", "name"] + fields=["_id", "name", "data.parents"] ) asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] + asset_doc["_id"]: get_asset_name_identifier(asset_doc) for asset_doc in asset_docs } subset_docs = list(get_subsets( diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 5628d0973f..ac4c72a0ce 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -6,6 +6,7 @@ production type `ociolook`. All files are published as representation. """ from pathlib import Path +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.lib.attribute_definitions import ( FileDef, EnumDef, TextDef, UISeparatorDef @@ -54,8 +55,12 @@ This creator publishes color space look file (LUT). # this should never happen raise CreatorError("Missing files from representation") + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] asset_doc = get_asset_by_name( - self.project_name, instance_data["asset"]) + self.project_name, asset_name) subset_name = self.get_subset_name( variant=instance_data["variant"], diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8640500b18..23cf066362 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,6 +1,7 @@ import os from copy import deepcopy import opentimelineio as otio +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_asset_by_name, get_project @@ -215,7 +216,10 @@ or updating already created. Publishing will create OTIO file. ] } # Create otio editorial instance - asset_name = instance_data["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) if pre_create_data["fps"] == "from_selection": diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index 3454b6e135..8fa65c7fff 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -2,6 +2,8 @@ import copy import os import re +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_asset_name_identifier from openpype.lib import ( FileDef, BoolDef, @@ -64,8 +66,13 @@ class BatchMovieCreator(TrayPublishCreator): subset_name, task_name = self._get_subset_and_task( asset_doc, data["variant"], self.project_name) + asset_name = get_asset_name_identifier(asset_doc) + instance_data["task"] = task_name - instance_data["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name # Create new instance new_instance = CreatedInstance(self.family, subset_name, From 8360c321bdf3713df75a7b0225d4cb59ac22b02f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:39:04 +0200 Subject: [PATCH 040/298] modified tvpaint to use new 'asset' handling --- .../tvpaint/plugins/create/create_render.py | 54 +++++++++++++++---- .../tvpaint/plugins/create/create_review.py | 21 ++++++-- .../tvpaint/plugins/create/create_workfile.py | 20 +++++-- .../plugins/publish/validate_asset_name.py | 12 ++++- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index b7a7c208d9..667103432e 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -37,7 +37,8 @@ Todos: import collections from typing import Any, Optional, Union -from openpype.client import get_asset_by_name +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.lib import ( prepare_template_data, AbstractAttrDef, @@ -784,18 +785,25 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): project_name, host_name=self.create_context.host_name, ) + asset_name = get_asset_name_identifier(asset_doc) if existing_instance is not None: - existing_instance["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name return existing_instance instance_data: dict[str, str] = { - "asset": asset_doc["name"], "task": task_name, "family": creator.family, "variant": variant } + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name pre_create_data: dict[str, str] = { "group_id": group_id, "mark_for_review": mark_for_review @@ -820,6 +828,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): for layer_name in render_pass["layer_names"]: render_pass_by_layer_name[layer_name] = render_pass + asset_name = get_asset_name_identifier(asset_doc) + for layer in layers: layer_name = layer["name"] variant = layer_name @@ -838,17 +848,25 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ) if render_pass is not None: - render_pass["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + render_pass["folderPath"] = asset_name + else: + render_pass["asset"] = asset_name + render_pass["task"] = task_name render_pass["subset"] = subset_name continue instance_data: dict[str, str] = { - "asset": asset_doc["name"], "task": task_name, "family": creator.family, "variant": variant } + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name + pre_create_data: dict[str, Any] = { "render_layer_instance_id": render_layer_instance.id, "layer_names": [layer_name], @@ -882,9 +900,13 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def create(self, subset_name, instance_data, pre_create_data): project_name: str = self.create_context.get_current_project_name() - asset_name: str = instance_data["asset"] + if AYON_SERVER_ENABLED: + asset_name: str = instance_data["folderPath"] + else: + asset_name: str = instance_data["asset"] task_name: str = instance_data["task"] - asset_doc: dict[str, Any] = get_asset_by_name(project_name, asset_name) + asset_doc: dict[str, Any] = get_asset_by_name( + project_name, asset_name) render_layers_by_group_id: dict[int, CreatedInstance] = {} render_passes_by_render_layer_id: dict[int, list[CreatedInstance]] = ( @@ -1061,7 +1083,6 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant, "creator_attributes": { @@ -1073,6 +1094,10 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): self.default_pass_name ) } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name if not self.active_on_create: data["active"] = False @@ -1101,8 +1126,14 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_name = None + if AYON_SERVER_ENABLED: + existing_name = existing_instance.get("folderPath") + if existing_name is None: + existing_name = existing_instance["asset"] + if ( - existing_instance["asset"] != asset_name + existing_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -1114,7 +1145,10 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 7bb7510a8e..265cef00ef 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import CreatedInstance from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -33,6 +34,13 @@ class TVPaintReviewCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + if existing_asset_name is None: + existing_asset_name = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -43,10 +51,14 @@ class TVPaintReviewCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name + if not self.active_on_create: data["active"] = False @@ -59,7 +71,7 @@ class TVPaintReviewCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -71,6 +83,9 @@ class TVPaintReviewCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index c3982c0eca..eec0f8483f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import CreatedInstance from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -29,6 +30,13 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + if existing_asset_name is None: + existing_asset_name = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -39,10 +47,13 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name new_instance = CreatedInstance( self.family, subset_name, data, self @@ -53,7 +64,7 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -65,6 +76,9 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 9347960d3f..dc29e6c278 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin, @@ -24,12 +25,19 @@ class FixAssetNames(pyblish.api.Action): old_instance_items = list_instances() new_instance_items = [] for instance_item in old_instance_items: - instance_asset_name = instance_item.get("asset") + if AYON_SERVER_ENABLED: + instance_asset_name = instance_item.get("folderPath") + else: + instance_asset_name = instance_item.get("asset") + if ( instance_asset_name and instance_asset_name != context_asset_name ): - instance_item["asset"] = context_asset_name + if AYON_SERVER_ENABLED: + instance_item["folderPath"] = context_asset_name + else: + instance_item["asset"] = context_asset_name new_instance_items.append(instance_item) write_instances(new_instance_items) From 5ada46f2a05dc71b621a7cffe077cb9dcff0e997 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:41:37 +0200 Subject: [PATCH 041/298] modified some parts of hiero to follow new 'asset' handling --- openpype/hosts/hiero/api/plugin.py | 6 ++---- .../publish/collect_frame_tag_instances.py | 7 ++++++- .../plugins/publish/precollect_instances.py | 1 - .../plugins/publish/precollect_workfile.py | 17 ++++++++++------- .../collect_assetbuilds.py | 18 ++++++++++-------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 52f96261b2..0e0632e032 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -11,7 +11,6 @@ import qargparse from openpype.settings import get_current_project_settings from openpype.lib import Logger from openpype.pipeline import LoaderPlugin, LegacyCreator -from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.load import get_representation_path_from_context from . import lib @@ -494,9 +493,8 @@ class ClipLoader: joint `data` key with asset.data dict into the representation """ - asset_name = self.context["representation"]["context"]["asset"] - asset_doc = get_current_project_asset(asset_name) - log.debug("__ asset_doc: {}".format(pformat(asset_doc))) + + asset_doc = self.context["asset"] self.data["assetData"] = asset_doc["data"] def _make_track_item(self, source_bin_item, audio=False): diff --git a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py index 982a34efd6..79bf67b336 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py +++ b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -5,6 +5,8 @@ import json import pyblish.api +from openpype.client import get_asset_name_identifier + class CollectFrameTagInstances(pyblish.api.ContextPlugin): """Collect frames from tags. @@ -99,6 +101,9 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): # first collect all available subset tag frames subset_data = {} + context_asset_doc = context.data["assetEntity"] + context_asset_name = get_asset_name_identifier(context_asset_doc) + for tag_data in sequence_tags: frame = int(tag_data["start"]) @@ -115,7 +120,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): subset_data[subset] = { "frames": [frame], "format": tag_data["format"], - "asset": context.data["assetEntity"]["name"] + "asset": context_asset_name } return subset_data diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 3f9da2cf60..65b8fed49c 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -178,7 +178,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def create_shot_instance(self, context, **data): master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") - asset = data.get("asset") item = data.get("item") clip_name = item.name() diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 5a66581531..1d6bdc0257 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,6 +7,7 @@ from qtpy.QtGui import QPixmap import hiero.ui +from openpype import AYON_SERVER_ENABLED from openpype.hosts.hiero.api.otio import hiero_export @@ -17,9 +18,10 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.491 def process(self, context): + asset_name = context.data["asset"] + if AYON_SERVER_ENABLED: + asset_name = asset_name.split("/")[-1] - asset = context.data["asset"] - subset = "workfile" active_timeline = hiero.ui.activeSequence() project = active_timeline.project() fps = active_timeline.framerate().toFloat() @@ -59,13 +61,14 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): 'files': base_name, "stagingDir": staging_dir, } - + family = "workfile" instance_data = { - "name": "{}_{}".format(asset, subset), - "asset": asset, - "subset": "{}{}".format(asset, subset.capitalize()), + "name": "{}_{}".format(asset_name, family), + "asset": context.data["asset"], + # TODO use 'get_subset_name' + "subset": "{}{}".format(asset_name, family.capitalize()), "item": project, - "family": "workfile", + "family": family, "families": [], "representations": [workfile_representation, thumb_representation] } diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py index 767f7c30f7..37370497a5 100644 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py +++ b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py @@ -1,5 +1,6 @@ from pyblish import api -from openpype.client import get_assets + +from openpype.client import get_assets, get_asset_name_identifier class CollectAssetBuilds(api.ContextPlugin): @@ -19,10 +20,13 @@ class CollectAssetBuilds(api.ContextPlugin): def process(self, context): project_name = context.data["projectName"] asset_builds = {} - for asset in get_assets(project_name): - if asset["data"]["entityType"] == "AssetBuild": - self.log.debug("Found \"{}\" in database.".format(asset)) - asset_builds[asset["name"]] = asset + for asset_doc in get_assets(project_name): + if asset_doc["data"].get("entityType") != "AssetBuild": + continue + + asset_name = get_asset_name_identifier(asset_doc) + self.log.debug("Found \"{}\" in database.".format(asset_doc)) + asset_builds[asset_name] = asset_doc for instance in context: if instance.data["family"] != "clip": @@ -50,9 +54,7 @@ class CollectAssetBuilds(api.ContextPlugin): # Collect asset builds. data = {"assetbuilds": []} for name in asset_names: - data["assetbuilds"].append( - asset_builds[name] - ) + data["assetbuilds"].append(asset_builds[name]) self.log.debug( "Found asset builds: {}".format(data["assetbuilds"]) ) From 51bebd0f1e24264e72358d58ef55f77062aaba3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:42:14 +0200 Subject: [PATCH 042/298] modified part of flame to follow new 'asset' naming --- .../hosts/flame/plugins/publish/collect_timeline_otio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index f8cfa9e963..20ac048986 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -1,5 +1,6 @@ import pyblish.api +from openpype.client import get_asset_name_identifier import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export from openpype.pipeline.create import get_subset_name @@ -33,13 +34,15 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): project_settings=context.data["project_settings"] ) + asset_name = get_asset_name_identifier(asset_doc) + # adding otio timeline to context with opfapi.maintained_segment_selection(sequence) as selected_seg: otio_timeline = flame_export.create_otio_timeline(sequence) instance_data = { "name": subset_name, - "asset": asset_doc["name"], + "asset": asset_name, "subset": subset_name, "family": "workfile", "families": [] From 9d617db64cac3e06ea373e05511bce4fc0728c19 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:46:35 +0200 Subject: [PATCH 043/298] use only folder name to create instance name --- .../hosts/resolve/plugins/publish/precollect_workfile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index a2f3eaed7a..28b2350f01 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -15,6 +15,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): asset = get_current_asset_name() + # AYON compatibility split name and use last piece + _asset_name = asset.split("/")[-1] subset = "workfile" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") @@ -24,9 +26,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { - "name": "{}_{}".format(asset, subset), + "name": "{}_{}".format(_asset_name, subset), "asset": asset, - "subset": "{}{}".format(asset, subset.capitalize()), + "subset": "{}{}".format(_asset_name, subset.capitalize()), "item": project, "family": "workfile", "families": [] From e34c0b9ecd1505657969676bb0d9a062498c6c52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:50:54 +0200 Subject: [PATCH 044/298] avoid unnecessary call to database --- openpype/hosts/resolve/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 8381f81acb..a3d533d3d7 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,10 +1,11 @@ import re import uuid +import copy + import qargparse from qtpy import QtWidgets, QtCore from openpype.settings import get_current_project_settings -from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( LegacyCreator, LoaderPlugin, @@ -379,8 +380,8 @@ class ClipLoader: joint `data` key with asset.data dict into the representation """ - asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = get_current_project_asset(asset_name)["data"] + + self.data["assetData"] = copy.deepcopy(self.context["asset"]["data"]) def load(self, files): """Load clip into timeline From 053cc44891343624d73de6a03ed3c560103ff118 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 11:40:26 +0200 Subject: [PATCH 045/298] removed unnecessary line --- openpype/client/server/entities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index c1e27eabb9..c735c558d5 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -183,7 +183,6 @@ def get_asset_by_name(project_name, asset_name, fields=None): return None - def _folders_query(project_name, con, fields, **kwargs): if fields is None or "tasks" in fields: folders = get_folders_with_tasks( From 7942e33a117e487d5d86c15e8d919fc5dc947ff6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 12:01:58 +0200 Subject: [PATCH 046/298] be expicit about source of asset and folder path --- .../hosts/fusion/plugins/create/create_workfile.py | 14 ++++++-------- .../houdini/plugins/create/create_workfile.py | 12 ++++++------ .../hosts/maya/plugins/create/create_workfile.py | 12 ++++++------ .../plugins/create/create_flatten_image.py | 13 ++++++------- .../plugins/create/create_workfile.py | 12 ++++++------ 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 8063e56413..4092086ea4 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -69,14 +69,12 @@ class FusionWorkfileCreator(AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.data.get( - "folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.data.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 04a844bdf5..f8ee68ebc9 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -31,12 +31,12 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.host_name - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index 74629776af..7282fc6b8b 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -30,12 +30,12 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index 942f8f4989..24be9df0e0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -38,13 +38,12 @@ class AutoImageCreator(PSAutoCreator): host_name = context.host_name asset_doc = get_asset_by_name(project_name, asset_name) - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.get("folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: subset_name = self.get_subset_name( diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 8aa696f11d..c73277e405 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -42,12 +42,12 @@ class CreateWorkfile(AutoCreator): if instance.creator_identifier == self.identifier ), None) - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: self.log.info("Auto-creating workfile instance...") From a12d599b442540a46b88212fcdc873ad1c85db46 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 25 Oct 2023 14:04:00 +0200 Subject: [PATCH 047/298] change creator labels --- openpype/hosts/blender/plugins/create/create_action.py | 2 +- openpype/hosts/blender/plugins/create/create_animation.py | 2 +- openpype/hosts/blender/plugins/create/create_camera.py | 2 +- openpype/hosts/blender/plugins/create/create_layout.py | 2 +- openpype/hosts/blender/plugins/create/create_model.py | 2 +- openpype/hosts/blender/plugins/create/create_pointcache.py | 2 +- openpype/hosts/blender/plugins/create/create_review.py | 2 +- openpype/hosts/blender/plugins/create/create_rig.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index d766fce038..e7b689c54e 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -38,7 +38,7 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 88ae9e5996..8b4214ceda 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -58,7 +58,7 @@ class CreateAnimation(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 026b5739d6..4747e50b2e 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -56,7 +56,7 @@ class CreateCamera(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index f46ae58a43..0c97d57af3 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -55,7 +55,7 @@ class CreateLayout(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 069b78626b..3c8e9c4900 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -55,7 +55,7 @@ class CreateModel(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 3054b81ef5..a40bd5af61 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -38,7 +38,7 @@ class CreatePointcache(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 10a96c94fd..8c9a8d5927 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -54,7 +54,7 @@ class CreateReview(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 8daffe638d..110a9f5c8e 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -55,7 +55,7 @@ class CreateRig(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, From c3ae2a3a09bdd72824f950682faf418931651ff7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 15:20:28 +0200 Subject: [PATCH 048/298] be expicit about source of asset and folder path in photoshop --- openpype/hosts/photoshop/lib.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/lib.py b/openpype/hosts/photoshop/lib.py index 654528410a..5c8dff947d 100644 --- a/openpype/hosts/photoshop/lib.py +++ b/openpype/hosts/photoshop/lib.py @@ -45,13 +45,12 @@ class PSAutoCreator(AutoCreator): task_name = context.get_current_task_name() host_name = context.host_name - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.get("folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 803fb616492f5e34feec7a3a3bdd7e7599dcc8d8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 22:31:27 +0800 Subject: [PATCH 049/298] validate loaded plugins tweaks in 3dsmax --- .../plugins/publish/validate_loaded_plugin.py | 69 +++++++++++++++++++ .../plugins/publish/validate_usd_plugin.py | 49 ------------- .../defaults/project_settings/max.json | 5 ++ .../schemas/schema_max_publish.json | 25 +++++++ .../max/server/settings/publishers.py | 18 ++++- server_addon/max/server/version.py | 2 +- 6 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_loaded_plugin.py delete mode 100644 openpype/hosts/max/plugins/publish/validate_usd_plugin.py diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py new file mode 100644 index 0000000000..10cbdf22fb --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""Validator for USD plugin.""" +from pyblish.api import InstancePlugin, ValidatorOrder +from pymxs import runtime as rt + +from openpype.pipeline.publish import ( + RepairAction, + OptionalPyblishPluginMixin, + PublishValidationError +) +from openpype.hosts.max.api.lib import get_plugins + + +class ValidateLoadedPlugin(OptionalPyblishPluginMixin, + InstancePlugin): + """Validates if the specific plugin is loaded in 3ds max. + User can add the plugins they want to check through""" + + order = ValidatorOrder + hosts = ["max"] + label = "Validate Loaded Plugin" + optional = True + actions = [RepairAction] + + def get_invalid(self, instance): + """Plugin entry point.""" + invalid = [] + # display all DLL loaded plugins in Max + plugin_info = get_plugins() + project_settings = instance.context.data[ + "project_settings"]["max"]["publish"] + target_plugins = project_settings[ + "ValidateLoadedPlugin"]["plugins_for_check"] + for plugin in target_plugins: + if plugin.lower() not in plugin_info: + invalid.append( + f"Plugin {plugin} not exists in 3dsMax Plugin List.") + for i, _ in enumerate(plugin_info): + if plugin.lower() == rt.pluginManager.pluginDllName(i): + if not rt.pluginManager.isPluginDllLoaded(i): + invalid.append( + f"Plugin {plugin} not loaded.") + return invalid + + def process(self, instance): + invalid_plugins = self.get_invalid(instance) + if invalid_plugins: + bullet_point_invalid_statement = "\n".join( + "- {}".format(invalid) for invalid in invalid_plugins + ) + report = ( + "Required plugins fails to load.\n\n" + f"{bullet_point_invalid_statement}\n\n" + "You can use repair action to load the plugin." + ) + raise PublishValidationError(report, title="Required Plugins unloaded") + + @classmethod + def repair(cls, instance): + plugin_info = get_plugins() + project_settings = instance.context.data[ + "project_settings"]["max"]["publish"] + target_plugins = project_settings[ + "ValidateLoadedPlugin"]["plugins_for_check"] + for plugin in target_plugins: + for i, _ in enumerate(plugin_info): + if plugin == rt.pluginManager.pluginDllName(i): + if not rt.pluginManager.isPluginDllLoaded(i): + rt.pluginManager.loadPluginDll(i) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py deleted file mode 100644 index 36c4291925..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validator for USD plugin.""" -from pyblish.api import InstancePlugin, ValidatorOrder -from pymxs import runtime as rt - -from openpype.pipeline import ( - OptionalPyblishPluginMixin, - PublishValidationError -) - - -def get_plugins() -> list: - """Get plugin list from 3ds max.""" - manager = rt.PluginManager - count = manager.pluginDllCount - plugin_info_list = [] - for p in range(1, count + 1): - plugin_info = manager.pluginDllName(p) - plugin_info_list.append(plugin_info) - - return plugin_info_list - - -class ValidateUSDPlugin(OptionalPyblishPluginMixin, - InstancePlugin): - """Validates if USD plugin is installed or loaded in 3ds max.""" - - order = ValidatorOrder - 0.01 - families = ["model"] - hosts = ["max"] - label = "Validate USD Plugin loaded" - optional = True - - def process(self, instance): - """Plugin entry point.""" - - for sc in ValidateUSDPlugin.__subclasses__(): - self.log.info(sc) - - if not self.is_active(instance.data): - return - - plugin_info = get_plugins() - usd_import = "usdimport.dli" - if usd_import not in plugin_info: - raise PublishValidationError(f"USD Plugin {usd_import} not found") - usd_export = "usdexport.dle" - if usd_export not in plugin_info: - raise PublishValidationError(f"USD Plugin {usd_export} not found") diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..45246fdf2b 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -36,6 +36,11 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateLoadedPlugin": { + "enabled": false, + "optional": true, + "plugins_for_check": [] } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index ea08c735a6..4490c5353d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -28,6 +28,31 @@ "label": "Active" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateLoadedPlugin", + "label": "Validate Loaded Plugin", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "list", + "key": "plugins_for_check", + "label": "Plugins Needed For Check", + "object_type": "text" + } + ] } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a695b85e89..8a28224a07 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -3,6 +3,14 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +class ValidateLoadedPluginModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateLoadedPlugin") + optional: bool = Field(title="Optional") + plugins_for_check: list[str] = Field( + default_factory=list, title="Plugins Needed For Check" + ) + + class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") @@ -15,12 +23,20 @@ class PublishersModel(BaseSettingsModel): title="Validate Frame Range", section="Validators" ) - + ValidateLoadedPlugin: ValidateLoadedPluginModel = Field( + default_factory=ValidateLoadedPluginModel, + title="Validate Loaded Plugin" + ) DEFAULT_PUBLISH_SETTINGS = { "ValidateFrameRange": { "enabled": True, "optional": True, "active": True + }, + "ValidateLoadedPlugin": { + "enabled": False, + "optional": True, + "plugins_for_check": [] } } diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 77776d0943ae8750876f98521a9e493dbcdf584e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 22:53:28 +0800 Subject: [PATCH 050/298] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 10cbdf22fb..44343bada2 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -53,7 +53,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, f"{bullet_point_invalid_statement}\n\n" "You can use repair action to load the plugin." ) - raise PublishValidationError(report, title="Required Plugins unloaded") + raise PublishValidationError( + report, title="Required Plugins unloaded") @classmethod def repair(cls, instance): From a43b842097b48924345cb8be98f8ca380a2b73a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 15:49:28 +0800 Subject: [PATCH 051/298] add missing codes for switching on/off the loaded plugin validator --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 44343bada2..0090c69269 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -24,6 +24,9 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, def get_invalid(self, instance): """Plugin entry point.""" + if not self.is_active(instance.data): + self.log.debug("Skipping Validate Loaded Plugin...") + return invalid = [] # display all DLL loaded plugins in Max plugin_info = get_plugins() From cb4dd2559b9d51469870a3962cf69435f7d290b4 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 26 Oct 2023 10:44:34 +0200 Subject: [PATCH 052/298] wip create blend scene create render --- .../plugins/create/create_blendScene.py | 41 +++++++++++++++---- .../blender/plugins/create/create_render.py | 35 ++++++++++++---- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 63bcf212ff..ee8e52d3c5 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateBlendScene(plugin.Creator): @@ -15,12 +18,18 @@ class CreateBlendScene(plugin.Creator): family = "blendScene" icon = "cubes" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -28,14 +37,28 @@ class CreateBlendScene(plugin.Creator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) + asset = instance_data.get("asset") + name = plugin.asset_name(asset, subset_name) asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) + + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name + } + + instance_data.update( + { + "id": "publish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) + + lib.imprint(asset_group, instance_data) # Add selected objects to instance if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index f938a21808..ab3119b32e 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -4,10 +4,13 @@ import bpy from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.render_lib import prepare_rendering -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) -class CreateRenderlayer(plugin.Creator): +class CreateRenderlayer(plugin.BlenderCreator): """Single baked camera""" name = "renderingMain" @@ -15,7 +18,9 @@ class CreateRenderlayer(plugin.Creator): family = "render" icon = "eye" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -23,15 +28,29 @@ class CreateRenderlayer(plugin.Creator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) + asset = instance_data.get("asset") + name = plugin.asset_name(asset, subset_name) asset_group = bpy.data.collections.new(name=name) try: instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) + + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name + } + + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) + + lib.imprint(asset_group, instance_data) prepare_rendering(asset_group) except Exception: From 5d87d08ab83b81815e6bc47bddcd7300a30fcc60 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:04:00 +0800 Subject: [PATCH 053/298] clean up the code of validate loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 0090c69269..a8bdf7f903 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Validator for USD plugin.""" -from pyblish.api import InstancePlugin, ValidatorOrder +"""Validator for Loaded Plugin.""" +from pyblish.api import ContextPlugin, ValidatorOrder from pymxs import runtime as rt from openpype.pipeline.publish import ( - RepairAction, + RepairContextAction, OptionalPyblishPluginMixin, PublishValidationError ) @@ -12,7 +12,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - InstancePlugin): + ContextPlugin): """Validates if the specific plugin is loaded in 3ds max. User can add the plugins they want to check through""" @@ -20,29 +20,38 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, hosts = ["max"] label = "Validate Loaded Plugin" optional = True - actions = [RepairAction] + actions = [RepairContextAction] - def get_invalid(self, instance): + def get_invalid(self, context): """Plugin entry point.""" - if not self.is_active(instance.data): + if not self.is_active(context.data): self.log.debug("Skipping Validate Loaded Plugin...") return invalid = [] - # display all DLL loaded plugins in Max - plugin_info = get_plugins() - project_settings = instance.context.data[ - "project_settings"]["max"]["publish"] - target_plugins = project_settings[ - "ValidateLoadedPlugin"]["plugins_for_check"] - for plugin in target_plugins: - if plugin.lower() not in plugin_info: + # get all DLL loaded plugins in Max and their plugin index + available_plugins = { + plugin_name.lower(): index for index, plugin_name in enumerate(\ + get_plugins()) + } + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + for plugin in required_plugins: + plugin_name = plugin.lower() + + plugin_index = available_plugins.get(plugin_name) + + if plugin_index is None: invalid.append( - f"Plugin {plugin} not exists in 3dsMax Plugin List.") - for i, _ in enumerate(plugin_info): - if plugin.lower() == rt.pluginManager.pluginDllName(i): - if not rt.pluginManager.isPluginDllLoaded(i): - invalid.append( - f"Plugin {plugin} not loaded.") + f"Plugin {plugin} not exists in 3dsMax Plugin List." + ) + continue + + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + invalid.append( + f"Plugin {plugin} not loaded.") + return invalid def process(self, instance): @@ -60,14 +69,18 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, report, title="Required Plugins unloaded") @classmethod - def repair(cls, instance): - plugin_info = get_plugins() - project_settings = instance.context.data[ - "project_settings"]["max"]["publish"] - target_plugins = project_settings[ - "ValidateLoadedPlugin"]["plugins_for_check"] - for plugin in target_plugins: - for i, _ in enumerate(plugin_info): - if plugin == rt.pluginManager.pluginDllName(i): - if not rt.pluginManager.isPluginDllLoaded(i): - rt.pluginManager.loadPluginDll(i) + def repair(cls, context): + # get all DLL loaded plugins in Max and their plugin index + available_plugins = { + plugin_name.lower(): index for index, plugin_name in enumerate( + get_plugins()) + } + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + for plugin in required_plugins: + plugin_name = plugin.lower() + plugin_index = available_plugins.get(plugin_name) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) From 6c24e55d9697bd28d6d7e7cac813f2d0756aa6a8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:05:10 +0800 Subject: [PATCH 054/298] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index a8bdf7f903..564cfd0e67 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -30,7 +30,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, invalid = [] # get all DLL loaded plugins in Max and their plugin index available_plugins = { - plugin_name.lower(): index for index, plugin_name in enumerate(\ + plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } required_plugins = ( From b0a12848b92a825e7d979fa9c63416310ec6d528 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:47:40 +0800 Subject: [PATCH 055/298] clean up code and add condition to make sure the plugin not erroring out during validation --- .../plugins/publish/validate_loaded_plugin.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 564cfd0e67..49f0f3041b 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -18,7 +18,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, order = ValidatorOrder hosts = ["max"] - label = "Validate Loaded Plugin" + label = "Validate Loaded Plugins" optional = True actions = [RepairContextAction] @@ -27,16 +27,23 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not self.is_active(context.data): self.log.debug("Skipping Validate Loaded Plugin...") return + + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + + if not required_plugins: + return + invalid = [] + # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] - ) + for plugin in required_plugins: plugin_name = plugin.lower() @@ -49,8 +56,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append( - f"Plugin {plugin} not loaded.") + invalid.append(f"Plugin {plugin} not loaded.") return invalid @@ -82,5 +88,10 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, for plugin in required_plugins: plugin_name = plugin.lower() plugin_index = available_plugins.get(plugin_name) + + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {plugin}") + continue + if not rt.pluginManager.isPluginDllLoaded(plugin_index): rt.pluginManager.loadPluginDll(plugin_index) From a8c4c05b7329b6ccf22309f14aa3990f18b96844 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:58:03 +0800 Subject: [PATCH 056/298] Docstring edit --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 49f0f3041b..9602d0f313 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -14,7 +14,8 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, ContextPlugin): """Validates if the specific plugin is loaded in 3ds max. - User can add the plugins they want to check through""" + Studio Admin(s) can add the plugins they want to check in validation + via studio defined project settings""" order = ValidatorOrder hosts = ["max"] From ca2ff805910510a6ecf75e0ae233b8b818665924 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Oct 2023 12:43:13 +0200 Subject: [PATCH 057/298] nuke: updating colorspace defaults --- .../defaults/project_settings/nuke.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 1cadedd797..20df0ad5c2 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -19,16 +19,16 @@ "rules": {} }, "viewer": { - "viewerProcess": "sRGB" + "viewerProcess": "sRGB (default)" }, "baking": { - "viewerProcess": "rec709" + "viewerProcess": "rec709 (default)" }, "workfile": { - "colorManagement": "Nuke", + "colorManagement": "OCIO", "OCIO_config": "nuke-default", - "workingSpaceLUT": "linear", - "monitorLut": "sRGB" + "workingSpaceLUT": "scene_linear", + "monitorLut": "sRGB (default)" }, "nodes": { "requiredNodes": [ @@ -76,7 +76,7 @@ { "type": "text", "name": "colorspace", - "value": "linear" + "value": "scene_linear" }, { "type": "bool", @@ -129,7 +129,7 @@ { "type": "text", "name": "colorspace", - "value": "linear" + "value": "scene_linear" }, { "type": "bool", @@ -177,7 +177,7 @@ { "type": "text", "name": "colorspace", - "value": "sRGB" + "value": "texture_paint" }, { "type": "bool", @@ -193,7 +193,7 @@ "inputs": [ { "regex": "(beauty).*(?=.exr)", - "colorspace": "linear" + "colorspace": "scene_linear" } ] } From 1ecd96acf6acb98f9fb27aa70a345ad5014343b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 21:18:28 +0800 Subject: [PATCH 058/298] use context.data instead of instance data --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 9602d0f313..69f72ccf1d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -61,8 +61,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, return invalid - def process(self, instance): - invalid_plugins = self.get_invalid(instance) + def process(self, context): + invalid_plugins = self.get_invalid(context) if invalid_plugins: bullet_point_invalid_statement = "\n".join( "- {}".format(invalid) for invalid in invalid_plugins From ae2c4bd5548c6ba81e7937320b0b91554ea614cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Oct 2023 17:09:34 +0200 Subject: [PATCH 059/298] nuke: aligning server addon settings with openpype --- server_addon/nuke/server/settings/imageio.py | 16 ++++++++-------- server_addon/nuke/server/version.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 15ccd4e89a..19ad5ff24a 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -213,16 +213,16 @@ class ImageIOSettings(BaseSettingsModel): DEFAULT_IMAGEIO_SETTINGS = { "viewer": { - "viewerProcess": "sRGB" + "viewerProcess": "sRGB (default)" }, "baking": { - "viewerProcess": "rec709" + "viewerProcess": "rec709 (default)" }, "workfile": { - "color_management": "Nuke", + "color_management": "OCIO", "native_ocio_config": "nuke-default", - "working_space": "linear", - "thumbnail_space": "sRGB", + "working_space": "scene_linear", + "thumbnail_space": "sRGB (default)", }, "nodes": { "required_nodes": [ @@ -269,7 +269,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "linear" + "text": "scene_linear" }, { "type": "boolean", @@ -321,7 +321,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "linear" + "text": "scene_linear" }, { "type": "boolean", @@ -368,7 +368,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "sRGB" + "text": "texture_paint" }, { "type": "boolean", diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From b6694606876eba68f3e0cb24928f93f7093e1c70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:06:47 +0200 Subject: [PATCH 060/298] Removed unnecessary condition. --- openpype/client/server/entities.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index c735c558d5..fcb5ec2383 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -226,12 +226,11 @@ def get_assets( new_asset_names = set() folder_paths = set() - if asset_names: - for name in asset_names: - if "/" in name: - folder_paths.add(name) - else: - new_asset_names.add(name) + for name in asset_names: + if "/" in name: + folder_paths.add(name) + else: + new_asset_names.add(name) if folder_paths: for folder in _folders_query( From 130693b798be8c53e18ec02bcacde7b8b6f45fdd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 18:24:42 +0200 Subject: [PATCH 061/298] use 'AYON_SERVER_ENABLED' in resolve plugin --- .../plugins/publish/precollect_workfile.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 28b2350f01..39c28e29f5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,6 +1,7 @@ import pyblish.api from pprint import pformat +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name from openpype.hosts.resolve import api as rapi from openpype.hosts.resolve.otio import davinci_export @@ -13,10 +14,13 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): + current_asset = get_current_asset_name() + if AYON_SERVER_ENABLED: + # AYON compatibility split name and use last piece + asset_name = current_asset.split("/")[-1] + else: + asset_name = current_asset - asset = get_current_asset_name() - # AYON compatibility split name and use last piece - _asset_name = asset.split("/")[-1] subset = "workfile" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") @@ -26,9 +30,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { - "name": "{}_{}".format(_asset_name, subset), - "asset": asset, - "subset": "{}{}".format(_asset_name, subset.capitalize()), + "name": "{}_{}".format(asset_name, subset), + "asset": current_asset, + "subset": "{}{}".format(asset_name, subset.capitalize()), "item": project, "family": "workfile", "families": [] From e920c014307e0fcb543f129b6e72b4ba49433cfb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 18:41:47 +0200 Subject: [PATCH 062/298] unify conditions --- .../hosts/tvpaint/plugins/create/create_review.py | 12 ++++++------ .../hosts/tvpaint/plugins/create/create_workfile.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 265cef00ef..5caf20f27d 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -34,12 +34,12 @@ class TVPaintReviewCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() - existing_asset_name = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_asset_name = existing_instance.get("folderPath") - if existing_asset_name is None: - existing_asset_name = existing_instance.get("asset") + if existing_instance is None: + existing_asset_name = None + elif AYON_SERVER_ENABLED: + existing_asset_name = existing_instance["folderPath"] + else: + existing_asset_name = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index eec0f8483f..4ce5d7fc96 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -30,12 +30,12 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() - existing_asset_name = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_asset_name = existing_instance.get("folderPath") - if existing_asset_name is None: - existing_asset_name = existing_instance.get("asset") + if existing_instance is None: + existing_asset_name = None + elif AYON_SERVER_ENABLED: + existing_asset_name = existing_instance["folderPath"] + else: + existing_asset_name = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 0b0cfb4116ed6067b0416bb34ecd8a3dd7e19805 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 14:24:37 +0200 Subject: [PATCH 063/298] add slash at the beginning of path --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index d085f90028..cbaa943743 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -22,4 +22,4 @@ def get_asset_name_identifier(asset_doc): return asset_doc["name"] parents = list(asset_doc["data"]["parents"]) parents.append(asset_doc["name"]) - return "/".join(parents) + return "/" + "/".join(parents) From dd72d45ce7881b46c90c6972f27cc101b02f7696 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 14:36:22 +0200 Subject: [PATCH 064/298] fix path in assets widget --- openpype/tools/publisher/widgets/assets_widget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 5f74b79c99..32be514dd7 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -140,9 +140,11 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): for name in sorted(children_by_name.keys()): child = children_by_name[name] child_id = child["_id"] - child_path = name if parent_path: - child_path = "{}/{}".format(parent_path, child_path) + child_path = "{}/{}".format(parent_path, name) + else: + child_path = "/{}".format(name) + has_children = bool(assets_by_parent_id.get(child_id)) icon = get_asset_icon(child, has_children) From 4f658d2f51cd3357a3d31ba7d607354debabaa19 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 27 Oct 2023 16:05:01 +0200 Subject: [PATCH 065/298] update blender creator --- openpype/hosts/blender/api/plugin.py | 14 ++++++++++++-- .../blender/plugins/create/create_blendScene.py | 2 +- .../hosts/blender/plugins/create/create_model.py | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 73d8fc0ed5..dbd9f25d68 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( Creator, CreatedInstance, LoaderPlugin, + get_current_task_name, ) from .pipeline import ( AVALON_CONTAINERS, @@ -235,11 +236,20 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - collection["instance_node"] = instance_node = { + collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, } - instance_data["instance_node"] = instance_node + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) instance = CreatedInstance( self.family, subset_name, instance_data, self diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index ee8e52d3c5..23ff991654 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -21,7 +21,7 @@ class CreateBlendScene(plugin.Creator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 3c8e9c4900..761d9fca9f 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -22,7 +22,7 @@ class CreateModel(plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" self._add_instance_to_context( CreatedInstance(self.family, subset_name, instance_data, self) ) From 3f8b250510e1cb0e6f8edae6afde87e5d2718c23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 17:05:00 +0200 Subject: [PATCH 066/298] removed unncessary path filtering --- openpype/client/server/entities.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index fcb5ec2383..9e86dfdd63 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -241,21 +241,9 @@ def get_assets( if not new_asset_names: return - folders_by_name = collections.defaultdict(list) for folder in _folders_query( project_name, con, fields, folder_names=new_asset_names, **kwargs ): - folders_by_name[folder["name"]].append(folder) - - for name, folders in folders_by_name.items(): - folder = next( - ( - folder - for folder in folders - if folder["path"] == name - ), - folders[0] - ) yield convert_v4_folder_to_v3(folder, project_name) From 4bde7b7fd94ab4ca183f90d83ef347e738d8b843 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 18:01:04 +0200 Subject: [PATCH 067/298] hiero: adding folderPath to creator - some minor typos fixes - modules sorting --- openpype/hosts/hiero/api/plugin.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 0e0632e032..dc90012b0f 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -31,7 +31,7 @@ def load_stylesheet(): class CreatorWidget(QtWidgets.QDialog): # output items - items = dict() + items = {} def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) @@ -642,8 +642,8 @@ class PublishClip: Returns: hiero.core.TrackItem: hiero track item object with pype tag """ - vertical_clip_match = dict() - tag_data = dict() + vertical_clip_match = {} + tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -705,9 +705,10 @@ class PublishClip: self._create_parents() def convert(self): - # solve track item data and add them to tag data - self._convert_to_tag_data() + tag_hierarchy_data = self._convert_to_tag_data() + + self.tag_data.update(tag_hierarchy_data) # if track name is in review track name and also if driving track name # is not in review track name: skip tag creation @@ -721,16 +722,28 @@ class PublishClip: if self.rename: # rename track item self.track_item.setName(new_name) - self.tag_data["asset"] = new_name + self.tag_data["asset_name"] = new_name else: - self.tag_data["asset"] = self.ti_name + self.tag_data["asset_name"] = self.ti_name self.tag_data["hierarchyData"]["shot"] = self.ti_name + # AYON unique identifier + folder_path = "/{}/{}".format( + tag_hierarchy_data["hierarchy"], + self.tag_data["asset_name"] + ) + self.tag_data["folderPath"] = folder_path + + # TODO: remove debug print + log.debug("___ folder_path: {}".format( + folder_path)) + if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: self.tag_data.update({"reviewTrack": None}) + # TODO: remove debug print log.debug("___ self.tag_data: {}".format( pformat(self.tag_data) )) @@ -889,7 +902,7 @@ class PublishClip: tag_hierarchy_data = hero_data # add data to return data dict - self.tag_data.update(tag_hierarchy_data) + return tag_hierarchy_data def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ From 4e9173fa71b9e6b4c19994356ce704a7d3bd29f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 18:02:11 +0200 Subject: [PATCH 068/298] Hiero: adding asset_name and processing folderPath - refactor labels --- .../plugins/publish/precollect_instances.py | 60 +++++++++++++------ .../plugins/publish/precollect_workfile.py | 15 +++-- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 65b8fed49c..1acbbb3d88 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -1,9 +1,12 @@ import pyblish + +from openpype import AYON_SERVER_ENABLED from openpype.pipeline.editorial import is_overlapping_otio_ranges + from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export -import hiero +import hiero # # developer reload modules from pprint import pformat @@ -80,25 +83,24 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["asset"] + asset, asset_name = self._get_asset_data(tag_data) + subset = tag_data["subset"] # insert family into families - family = tag_data["family"] families = [str(f) for f in tag_data["families"]] - families.insert(0, str(family)) # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({})".format(clip_name) label += " {}".format(subset) - label += " {}".format("[" + ", ".join(families) + "]") data.update({ "name": "{}_{}".format(asset, subset), "label": label, "asset": asset, + "asset_name": asset_name, "item": track_item, "families": families, "publish": tag_data["publish"], @@ -176,6 +178,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) def create_shot_instance(self, context, **data): + subset = "shotMain" master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") item = data.get("item") @@ -188,23 +191,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "shotMain" + asset_name = data["asset_name"] # insert family into families family = "shot" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": [] }) @@ -214,7 +215,34 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) + def _get_asset_data(self, data): + folder_path = ( + data.pop("folderPath") if data.get("folderPath") else None) + + if data.get("asset_name"): + asset_name = data["asset_name"] + else: + asset_name = data["asset"] + + # backward compatibility for clip tags + # which are missing folderPath key + # TODO remove this in future versions + if not folder_path: + hierarchy_path = data["hierarchy"] + folder_path = "/{}/{}".format( + hierarchy_path, + asset_name + ) + + if AYON_SERVER_ENABLED: + asset = folder_path + else: + asset = asset_name + + return asset, asset_name + def create_audio_instance(self, context, **data): + subset = "audioMain" master_layer = data.get("heroTrack") if not master_layer: @@ -229,23 +257,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "audioMain" + asset_name = data["asset_name"] # insert family into families family = "audio" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": ["clip"] }) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 1d6bdc0257..8abb0885c6 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -18,7 +18,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.491 def process(self, context): - asset_name = context.data["asset"] + asset = context.data["asset"] + asset_name = asset if AYON_SERVER_ENABLED: asset_name = asset_name.split("/")[-1] @@ -29,7 +30,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): # adding otio timeline to context otio_timeline = hiero_export.create_otio_timeline() - # get workfile thumnail paths + # get workfile thumbnail paths tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") thumbnail_name = "workfile_thumbnail.png" thumbnail_path = os.path.join(tmp_staging, thumbnail_name) @@ -51,8 +52,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } # get workfile paths - curent_file = project.path() - staging_dir, base_name = os.path.split(curent_file) + current_file = project.path() + staging_dir, base_name = os.path.split(current_file) # creating workfile representation workfile_representation = { @@ -63,10 +64,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } family = "workfile" instance_data = { + "label": "{} - {}Main".format( + asset, family), "name": "{}_{}".format(asset_name, family), "asset": context.data["asset"], # TODO use 'get_subset_name' - "subset": "{}{}".format(asset_name, family.capitalize()), + "subset": "{}{}Main".format(asset_name, family.capitalize()), "item": project, "family": family, "families": [], @@ -81,7 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "activeProject": project, "activeTimeline": active_timeline, "otioTimeline": otio_timeline, - "currentFile": curent_file, + "currentFile": current_file, "colorspace": self.get_colorspace(project), "fps": fps } From efef9e2fd35abc0e8fe7989776bbaeeef87b8336 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 18:06:54 +0200 Subject: [PATCH 069/298] small fix in timers manager --- openpype/modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 43286f7da4..674d834a1d 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -247,7 +247,7 @@ class TimersManager( return { "project_name": project_name, "asset_id": str(asset_doc["_id"]), - "asset_name": asset_doc["name"], + "asset_name": asset_name, "task_name": task_name, "task_type": task_type, "hierarchy": hierarchy_items From d9a35e7804c5b747d2316d8b6a91b8ee0866809e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 22:51:08 +0200 Subject: [PATCH 070/298] fixing extract hierarchy to ayon --- .../publish/extract_hierarchy_to_ayon.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index fe8cb40ad2..ef69369d67 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -191,15 +191,15 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): """ # filter only the active publishing instances - active_folder_names = set() + active_folder_paths = set() for instance in context: if instance.data.get("publish") is not False: - active_folder_names.add(instance.data.get("asset")) + active_folder_paths.add(instance.data.get("asset")) - active_folder_names.discard(None) + active_folder_paths.discard(None) - self.log.debug("Active folder names: {}".format(active_folder_names)) - if not active_folder_names: + self.log.debug("Active folder paths: {}".format(active_folder_paths)) + if not active_folder_paths: return None project_item = None @@ -230,12 +230,13 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): if not children_context: continue - for asset_name, asset_info in children_context.items(): + for asset, asset_info in children_context.items(): if ( - asset_name not in active_folder_names + asset not in active_folder_paths and not asset_info.get("childs") ): continue + asset_name = asset.split("/")[-1] item_id = uuid.uuid4().hex new_item = copy.deepcopy(asset_info) new_item["name"] = asset_name @@ -252,7 +253,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): items_by_id[item_id] = new_item parent_id_by_item_id[item_id] = parent_id - if asset_name in active_folder_names: + if asset in active_folder_paths: valid_ids.add(item_id) hierarchy_queue.append((item_id, new_children_context)) From dcf5855a477f35fd9fc160c8830974ec7cd2b8ed Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 22:51:24 +0200 Subject: [PATCH 071/298] hound --- openpype/hosts/hiero/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index dc90012b0f..f72d27fed5 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -902,7 +902,7 @@ class PublishClip: tag_hierarchy_data = hero_data # add data to return data dict - return tag_hierarchy_data + return tag_hierarchy_data def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ From 0550668d3d3356a91f6b9c0fc47d6ebc27000ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 30 Oct 2023 10:24:01 +0100 Subject: [PATCH 072/298] Update openpype/hosts/hiero/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/hiero/api/plugin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index f72d27fed5..b0c73e41fb 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -733,11 +733,6 @@ class PublishClip: self.tag_data["asset_name"] ) self.tag_data["folderPath"] = folder_path - - # TODO: remove debug print - log.debug("___ folder_path: {}".format( - folder_path)) - if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: From 05ab5e2e0fe53f4d585db7d4050ab6f6051b4f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:14:55 +0100 Subject: [PATCH 073/298] use 'asset' from context instead of from anatomy data --- .../plugins/publish/create_publish_royalrender_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 3eb49a39ee..e13bf97e54 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -189,7 +189,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, environment = RREnvList({ "AVALON_PROJECT": anatomy_data["project"]["name"], - "AVALON_ASSET": anatomy_data["asset"], + "AVALON_ASSET": instance.context.data["asset"], "AVALON_TASK": anatomy_data["task"]["name"], "OPENPYPE_USERNAME": anatomy_data["user"] }) From f27a25d2440671b19293bfba0c84180657178753 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:15:27 +0100 Subject: [PATCH 074/298] add 'folder' key to template data in harmony collectors --- .../plugins/publish/collect_harmony_scenes.py | 3 +++ .../plugins/publish/collect_harmony_zips.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py index 48c36aa067..c435ca2096 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py @@ -60,6 +60,9 @@ class CollectHarmonyScenes(pyblish.api.InstancePlugin): # updating hierarchy data anatomy_data_new.update({ "asset": asset_data["name"], + "folder": { + "name": asset_data["name"], + }, "task": { "name": task, "type": task_type, diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py index 40a969f8df..d90215e767 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py @@ -56,6 +56,9 @@ class CollectHarmonyZips(pyblish.api.InstancePlugin): anatomy_data_new.update( { "asset": asset_data["name"], + "folder": { + "name": asset_data["name"], + }, "task": { "name": task, "type": task_type, From 4c55f515dd8513382fc8b9b7ac7710716708f6c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:16:07 +0100 Subject: [PATCH 075/298] do not use 'instance.data["asset"]' to prepare template data --- .../publish/collect_anatomy_instance_data.py | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index cc6da9b2c3..1b4b44e40e 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -187,35 +187,29 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self.log.debug("Storing anatomy data to instance data.") project_doc = context.data["projectEntity"] - context_asset_doc = context.data.get("assetEntity") - project_task_types = project_doc["config"]["tasks"] for instance in context: + asset_doc = instance.data.get("assetEntity") anatomy_updates = { - "asset": instance.data["asset"], - "folder": { - "name": instance.data["asset"], - }, "family": instance.data["family"], "subset": instance.data["subset"], } - - # Hierarchy - asset_doc = instance.data.get("assetEntity") - if ( - asset_doc - and ( - not context_asset_doc - or asset_doc["_id"] != context_asset_doc["_id"] - ) - ): + if asset_doc: parents = asset_doc["data"].get("parents") or list() parent_name = project_doc["name"] if parents: parent_name = parents[-1] - anatomy_updates["hierarchy"] = "/".join(parents) - anatomy_updates["parent"] = parent_name + + hierarchy = "/".join(parents) + anatomy_updates.update({ + "asset": asset_doc["name"], + "hierarchy": hierarchy, + "parent": parent_name, + "folder": { + "name": asset_doc["name"], + }, + }) # Task task_type = None From 319a236bb2bfaf59dcaa2568685fb30bc1a04e8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 16:07:14 +0100 Subject: [PATCH 076/298] do not strip asset name --- openpype/tools/creator/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 47f27a262a..117519e1d7 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -214,7 +214,7 @@ class CreatorWindow(QtWidgets.QDialog): asset_name = self._asset_name_input.text() # Early exit if no asset name - if not asset_name.strip(): + if not asset_name: self._build_menu() self.echo("Asset name is required ..") self._set_valid_state(False) From 027cced5f58bb6af9b476f96744bf0909c5b6ebc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 16:40:46 +0100 Subject: [PATCH 077/298] show full folder path in look assigner --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 8 +++++--- openpype/hosts/maya/tools/mayalookassigner/widgets.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index 5cc4f84931..86df502ecd 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -4,7 +4,7 @@ from collections import defaultdict import maya.cmds as cmds -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_name_identifier from openpype.pipeline import ( remove_container, registered_host, @@ -128,7 +128,8 @@ def create_items_from_nodes(nodes): project_name = get_current_project_name() asset_ids = set(id_hashes.keys()) - asset_docs = get_assets(project_name, asset_ids, fields=["name"]) + fields = {"_id", "name", "data.parents"} + asset_docs = get_assets(project_name, asset_ids, fields=fields) asset_docs_by_id = { str(asset_doc["_id"]): asset_doc for asset_doc in asset_docs @@ -156,8 +157,9 @@ def create_items_from_nodes(nodes): namespace = get_namespace_from_node(node) namespaces.add(namespace) + label = get_asset_name_identifier(asset_doc) asset_view_items.append({ - "label": asset_doc["name"], + "label": label, "asset": asset_doc, "looks": looks, "namespaces": namespaces diff --git a/openpype/hosts/maya/tools/mayalookassigner/widgets.py b/openpype/hosts/maya/tools/mayalookassigner/widgets.py index 82c37e2104..ef29a4c726 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/widgets.py +++ b/openpype/hosts/maya/tools/mayalookassigner/widgets.py @@ -3,6 +3,7 @@ from collections import defaultdict from qtpy import QtWidgets, QtCore +from openpype.client import get_asset_name_identifier from openpype.tools.utils.models import TreeModel from openpype.tools.utils.lib import ( preserve_expanded_rows, @@ -126,7 +127,7 @@ class AssetOutliner(QtWidgets.QWidget): asset_namespaces = defaultdict(set) for item in items: asset_id = str(item["asset"]["_id"]) - asset_name = item["asset"]["name"] + asset_name = get_asset_name_identifier(item["asset"]) asset_namespaces[asset_name].add(item.get("namespace")) if asset_name in assets: From 3606f312b0f671404cea584dfdca2d9af749e18c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:33:06 +0800 Subject: [PATCH 078/298] fix the wrong aspect ratio and viewport doesn't maximize to 1 during context --- openpype/hosts/max/api/preview_animation.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 1bf99b86d0..bef5741343 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -23,8 +23,8 @@ def play_preview_when_done(has_autoplay): @contextlib.contextmanager -def viewport_camera(camera): - """Set viewport camera during context +def viewport_layout_and_camera(camera): + """Set viewport layout and camera during context ***For 3dsMax 2024+ Args: camera (str): viewport camera @@ -36,9 +36,12 @@ def viewport_camera(camera): original = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: + if rt.viewport.getLayout() != rt.Name("layout_1"): + rt.viewport.setLayout(rt.Name("layout_1")) rt.viewport.setCamera(review_camera) yield finally: + rt.viewport.ResetAllViews() rt.viewport.setCamera(original) @@ -162,6 +165,7 @@ def _render_preview_animation_max_pre_2024( Returns: list: Created filepaths """ + # get the screenshot percent = percentSize / 100.0 res_width = int(round(rt.renderWidth * percent)) @@ -190,7 +194,7 @@ def _render_preview_animation_max_pre_2024( widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) - src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) + src_box_value = rt.Box2(0, leftEdge, widthCrop, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) # copy the bitmap and close it rt.copy(tempImage_bmp, preview_res) @@ -243,7 +247,7 @@ def render_preview_animation( if viewport_options is None: viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): - with viewport_camera(camera): + with viewport_layout_and_camera(camera): with render_resolution(width, height): if int(get_max_version()) < 2024: with viewport_preference_setting( From e120b55fa47d7b716aa71122f4ecc3aa93ff6a06 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:13:24 +0800 Subject: [PATCH 079/298] setLayout in regards to original layout --- openpype/hosts/max/api/preview_animation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bef5741343..22de298175 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -29,11 +29,12 @@ def viewport_layout_and_camera(camera): Args: camera (str): viewport camera """ - original = rt.viewport.getCamera() - if not original: + original_camera = rt.viewport.getCamera() + original_layout = rt.viewport.getLayout() + if not original_camera: # if there is no original camera # use the current camera as original - original = rt.getNodeByName(camera) + original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: if rt.viewport.getLayout() != rt.Name("layout_1"): @@ -41,8 +42,8 @@ def viewport_layout_and_camera(camera): rt.viewport.setCamera(review_camera) yield finally: - rt.viewport.ResetAllViews() - rt.viewport.setCamera(original) + rt.viewport.setLayout(original_layout) + rt.viewport.setCamera(original_camera) @contextlib.contextmanager From dadd258cf1f1938ef609ecec689d08bb19815198 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:26:25 +0800 Subject: [PATCH 080/298] make the viewport_layout_and_camera reuseable --- openpype/hosts/max/api/preview_animation.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 22de298175..dcf243d31e 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -23,11 +23,13 @@ def play_preview_when_done(has_autoplay): @contextlib.contextmanager -def viewport_layout_and_camera(camera): +def viewport_layout_and_camera(camera, layout="layout_1"): """Set viewport layout and camera during context ***For 3dsMax 2024+ Args: camera (str): viewport camera + layout (str): layout to use in viewport, defaults to `layout_1` + Use None to not change viewport layout during context. """ original_camera = rt.viewport.getCamera() original_layout = rt.viewport.getLayout() @@ -37,8 +39,10 @@ def viewport_layout_and_camera(camera): original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: - if rt.viewport.getLayout() != rt.Name("layout_1"): - rt.viewport.setLayout(rt.Name("layout_1")) + if layout is not None: + layout = rt.Name(layout) + if rt.viewport.getLayout() != layout: + rt.viewport.setLayout(layout) rt.viewport.setCamera(review_camera) yield finally: From 6daa1b898a2cfe13509c71861816fad221778816 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:47:23 +0000 Subject: [PATCH 081/298] Use collections as asset group for blendscene family --- .../plugins/create/create_blendScene.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 63bcf212ff..96e63924d3 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -31,21 +31,11 @@ class CreateBlendScene(plugin.Creator): asset = self.data["asset"] subset = self.data["subset"] name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) + + # Create the new asset group as collection + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - return asset_group From 1c45fa139b38ea8f2c9055613a5f536f2b9fa40e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:48:05 +0000 Subject: [PATCH 082/298] Include collections asset group to get unique number --- openpype/hosts/blender/api/plugin.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fb87d08cce..45b0c60b3b 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -9,7 +9,10 @@ from openpype.pipeline import ( LegacyCreator, LoaderPlugin, ) -from .pipeline import AVALON_CONTAINERS +from .pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) from .ops import ( MainThreadItem, execute_in_main_thread @@ -40,9 +43,16 @@ def get_unique_number( avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) if not avalon_container: return "01" - asset_groups = avalon_container.all_objects - - container_names = [c.name for c in asset_groups if c.type == 'EMPTY'] + # Check the names of both object and collection containers + obj_asset_groups = avalon_container.all_objects + obj_group_names = [ + c.name for c in obj_asset_groups + if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)] + coll_asset_groups = avalon_container.children_recursive + coll_group_names = [ + c.name for c in coll_asset_groups + if c.get(AVALON_PROPERTY)] + container_names = obj_group_names + coll_group_names count = 1 name = f"{asset}_{count:0>2}_{subset}" while name in container_names: From a180a276a3838dd9b433e03c7e52b46bd39d5886 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:49:17 +0000 Subject: [PATCH 083/298] Add check that instance is object to pack images --- .../hosts/blender/plugins/publish/extract_blend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index c8eeef7fd7..4b6d9e7c69 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -25,11 +25,11 @@ class ExtractBlend(publish.Extractor): data_blocks = set() - for obj in instance: - data_blocks.add(obj) - # Pack used images in the blend files. - if obj.type == 'MESH': - for material_slot in obj.material_slots: + for data in instance: + data_blocks.add(data) + if isinstance(data, bpy.types.Object) and data.type == 'MESH': + # Pack used images in the blend files. + for material_slot in data.material_slots: mat = material_slot.material if mat and mat.use_nodes: tree = mat.node_tree From fd30a0426cb2a10b93384e8a6ce898e57b7d5114 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:49:58 +0000 Subject: [PATCH 084/298] Separate blendscene loader from blend loader --- .../hosts/blender/plugins/load/load_blend.py | 2 +- .../blender/plugins/load/load_blendscene.py | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/blender/plugins/load/load_blendscene.py diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 25d6568889..0719c5c97d 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import ( class BlendLoader(plugin.AssetLoader): """Load assets from a .blend file.""" - families = ["model", "rig", "layout", "camera", "blendScene"] + families = ["model", "rig", "layout", "camera"] representations = ["blend"] label = "Append Blend" diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py new file mode 100644 index 0000000000..8c43f40bdd --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -0,0 +1,215 @@ +from typing import Dict, List, Optional +from pathlib import Path + +import bpy + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID, +) +from openpype.pipeline.create import get_legacy_creator_by_name +from openpype.hosts.blender.api import plugin +from openpype.hosts.blender.api.lib import imprint +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class BlendSceneLoader(plugin.AssetLoader): + """Load assets from a .blend file.""" + + families = ["blendScene"] + representations = ["blend"] + + label = "Append Blend" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_asset_container(collections): + for coll in collections: + parents = [c for c in collections if c.user_of_id(coll)] + if coll.get(AVALON_PROPERTY) and not parents: + return coll + + return None + + def _process_data(self, libpath, group_name, family): + # Append all the data from the .blend file + with bpy.data.libraries.load( + libpath, link=False, relative=False + ) as (data_from, data_to): + for attr in dir(data_to): + setattr(data_to, attr, getattr(data_from, attr)) + + members = [] + + # Rename the object to add the asset name + for attr in dir(data_to): + for data in getattr(data_to, attr): + data.name = f"{group_name}:{data.name}" + members.append(data) + + container = self._get_asset_container( + data_to.collections) + assert container, "No asset group found" + + container.name = group_name + + # Link the group to the scene + bpy.context.scene.collection.children.link(container) + + # Remove the library from the blend file + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + + return container, members + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.filepath_from_context(context) + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + container, members = self._process_data(libpath, group_name, family) + + avalon_container.children.link(container) + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name, + "members": members, + } + + container[AVALON_PROPERTY] = data + + objects = [ + obj for obj in bpy.data.objects + if obj.name.startswith(f"{group_name}:") + ] + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """ + Update the loaded asset. + """ + group_name = container["objectName"] + asset_group = bpy.data.collections.get(group_name) + libpath = Path(get_representation_path(representation)).as_posix() + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}" + ) + + collection_parents = {} + members = asset_group.get(AVALON_PROPERTY).get("members", []) + loaded_collections = {c for c in bpy.data.collections if c in members} + loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS)) + for member in members: + if isinstance(member, bpy.types.Object): + member_parents = set(member.users_collection) + elif isinstance(member, bpy.types.Collection): + member_parents = { + c for c in bpy.data.collections if c.user_of_id(member)} + else: + continue + + member_parents = member_parents.difference(loaded_collections) + if member_parents: + collection_parents[member.name] = list(member_parents) + + old_data = dict(asset_group.get(AVALON_PROPERTY)) + + self.exec_remove(container) + + family = container["family"] + asset_group, members = self._process_data(libpath, group_name, family) + + for member in members: + if member.name in collection_parents: + for parent in collection_parents[member.name]: + if isinstance(member, bpy.types.Object): + parent.objects.link(member) + elif isinstance(member, bpy.types.Collection): + parent.children.link(member) + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + avalon_container.children.link(asset_group) + + # Restore the old data, but reset memebers, as they don't exist anymore + # This avoids a crash, because the memory addresses of those members + # are not valid anymore + old_data["members"] = [] + asset_group[AVALON_PROPERTY] = old_data + + new_data = { + "libpath": libpath, + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "members": members, + } + + imprint(asset_group, new_data) + + def exec_remove(self, container: Dict) -> bool: + """ + Remove an existing container from a Blender scene. + """ + group_name = container["objectName"] + asset_group = bpy.data.collections.get(group_name) + + attrs = [ + attr for attr in dir(bpy.data) + if isinstance( + getattr(bpy.data, attr), + bpy.types.bpy_prop_collection + ) + ] + + members = asset_group.get(AVALON_PROPERTY).get("members", []) + + for attr in attrs: + for data in getattr(bpy.data, attr): + if data in members: + # Skip the asset group + if data == asset_group: + continue + getattr(bpy.data, attr).remove(data) + + bpy.data.collections.remove(asset_group) From 6ec87aa06df1dbb7f9ae28fae286e61e73ba3355 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 11:10:20 +0000 Subject: [PATCH 085/298] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 8c43f40bdd..fe7afb3119 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -7,7 +7,6 @@ from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID, ) -from openpype.pipeline.create import get_legacy_creator_by_name from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.lib import imprint from openpype.hosts.blender.api.pipeline import ( From 881340b60a1dd2eba9d76331082679b9e26e6df9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:27:22 +0800 Subject: [PATCH 086/298] supports families check before the validation of loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 59 +++++++++++++------ .../plugins/publish/collect_scene_version.py | 1 + openpype/settings/ayon_settings.py | 13 ++++ .../defaults/project_settings/max.json | 2 +- .../schemas/schema_max_publish.json | 12 ++-- .../max/server/settings/publishers.py | 12 +++- 6 files changed, 74 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 69f72ccf1d..e8284aeedd 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """Validator for Loaded Plugin.""" -from pyblish.api import ContextPlugin, ValidatorOrder +import os +from pyblish.api import InstancePlugin, ValidatorOrder from pymxs import runtime as rt from openpype.pipeline.publish import ( - RepairContextAction, + RepairAction, OptionalPyblishPluginMixin, PublishValidationError ) @@ -12,7 +13,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - ContextPlugin): + InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings""" @@ -21,17 +22,17 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, hosts = ["max"] label = "Validate Loaded Plugins" optional = True - actions = [RepairContextAction] + actions = [RepairAction] - def get_invalid(self, context): + def get_invalid(self, instance): """Plugin entry point.""" - if not self.is_active(context.data): + if not self.is_active(instance.data): self.log.debug("Skipping Validate Loaded Plugin...") return required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] + instance.context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["family_plugins_mapping"] ) if not required_plugins: @@ -45,9 +46,21 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, get_plugins()) } - for plugin in required_plugins: - plugin_name = plugin.lower() + for families, plugin in required_plugins.items(): + families_list = families.split(",") + excluded_families = [family for family in families_list + if instance.data["family"]!=family + and family!="_"] + if excluded_families: + self.log.debug("The {} instance is not part of {}.".format( + instance.data["family"], excluded_families + )) + return + if not plugin: + return + + plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) if plugin_index is None: @@ -61,8 +74,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, return invalid - def process(self, context): - invalid_plugins = self.get_invalid(context) + def process(self, instance): + invalid_plugins = self.get_invalid(instance) if invalid_plugins: bullet_point_invalid_statement = "\n".join( "- {}".format(invalid) for invalid in invalid_plugins @@ -76,18 +89,30 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, report, title="Required Plugins unloaded") @classmethod - def repair(cls, context): + def repair(cls, instance): # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] + instance.context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["family_plugins_mapping"] ) - for plugin in required_plugins: - plugin_name = plugin.lower() + for families, plugin in required_plugins.items(): + families_list = families.split(",") + excluded_families = [family for family in families_list + if instance.data["family"]!=family + and family!="_"] + if excluded_families: + cls.log.debug("The {} instance is not part of {}.".format( + instance.data["family"], excluded_families + )) + continue + if not plugin: + continue + + plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) if plugin_index is None: diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 7920c1e82b..f870ae9ad7 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -24,6 +24,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "hiero", "houdini", "maya", + "max", "nuke", "photoshop", "resolve", diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..0cc2abdda4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -640,6 +640,19 @@ def _convert_3dsmax_project_settings(ayon_settings, output): } ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + ayon_publish = ayon_max["publish"] + if "ValidateLoadedPlugin" in ayon_publish: + family_plugin_mapping = ( + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] + ) + new_family_plugin_mapping = { + item["families"]: item["plugins"] + for item in family_plugin_mapping + } + ayon_max["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( + new_family_plugin_mapping + ) + output["max"] = ayon_max diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 45246fdf2b..78eba08750 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -40,7 +40,7 @@ "ValidateLoadedPlugin": { "enabled": false, "optional": true, - "plugins_for_check": [] + "family_plugins_mapping": {} } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index 4490c5353d..74c06f8156 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -47,10 +47,14 @@ "label": "Optional" }, { - "type": "list", - "key": "plugins_for_check", - "label": "Plugins Needed For Check", - "object_type": "text" + "type": "dict-modifiable", + "collapsible": true, + "key": "family_plugins_mapping", + "label": "Family Plugins Mapping", + "use_label_wrap": true, + "object_type": { + "type": "text" + } } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index 8a28224a07..3cf3ecf2a5 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -3,11 +3,17 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +class FamilyPluginsMappingModel(BaseSettingsModel): + _layout = "compact" + families: str = Field(title="Families") + plugins: str = Field(title="Plugins") + + class ValidateLoadedPluginModel(BaseSettingsModel): enabled: bool = Field(title="ValidateLoadedPlugin") optional: bool = Field(title="Optional") - plugins_for_check: list[str] = Field( - default_factory=list, title="Plugins Needed For Check" + family_plugins_mapping: list[FamilyPluginsMappingModel] = Field( + default_factory=list, title="Family Plugins Mapping" ) @@ -37,6 +43,6 @@ DEFAULT_PUBLISH_SETTINGS = { "ValidateLoadedPlugin": { "enabled": False, "optional": True, - "plugins_for_check": [] + "family_plugins_mapping": {} } } From 8d727a9b80922bb07b468f694ab4f57f69945926 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:30:38 +0800 Subject: [PATCH 087/298] hound --- .../max/plugins/publish/validate_loaded_plugin.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index e8284aeedd..dc82c7ed65 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -32,7 +32,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, required_plugins = ( instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["family_plugins_mapping"] + ["ValidateLoadedPlugin"] + ["family_plugins_mapping"] ) if not required_plugins: @@ -49,8 +50,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, for families, plugin in required_plugins.items(): families_list = families.split(",") excluded_families = [family for family in families_list - if instance.data["family"]!=family - and family!="_"] + if instance.data["family"] != family + and family != "_"] if excluded_families: self.log.debug("The {} instance is not part of {}.".format( instance.data["family"], excluded_families @@ -97,13 +98,14 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, } required_plugins = ( instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["family_plugins_mapping"] + ["ValidateLoadedPlugin"] + ["family_plugins_mapping"] ) for families, plugin in required_plugins.items(): families_list = families.split(",") excluded_families = [family for family in families_list - if instance.data["family"]!=family - and family!="_"] + if instance.data["family"] != family + and family != "_"] if excluded_families: cls.log.debug("The {} instance is not part of {}.".format( instance.data["family"], excluded_families From 1aef9dc449b525313760db6e3d7e86378e6f1ab9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:45:48 +0800 Subject: [PATCH 088/298] make sure the validator can be loaded in AYON --- openpype/settings/ayon_settings.py | 2 +- server_addon/max/server/settings/publishers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0cc2abdda4..4fe19c95a2 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -649,7 +649,7 @@ def _convert_3dsmax_project_settings(ayon_settings, output): item["families"]: item["plugins"] for item in family_plugin_mapping } - ayon_max["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( new_family_plugin_mapping ) diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index 3cf3ecf2a5..d0fbb3d552 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -43,6 +43,6 @@ DEFAULT_PUBLISH_SETTINGS = { "ValidateLoadedPlugin": { "enabled": False, "optional": True, - "family_plugins_mapping": {} + "family_plugins_mapping": [] } } From 58c9664f7e8ac2082a44279e652a1fb82674769d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Nov 2023 10:39:28 +0000 Subject: [PATCH 089/298] Use sets and don't check container children when getting unique number --- openpype/hosts/blender/api/plugin.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 45b0c60b3b..2f940011ba 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -44,15 +44,15 @@ def get_unique_number( if not avalon_container: return "01" # Check the names of both object and collection containers - obj_asset_groups = avalon_container.all_objects - obj_group_names = [ + obj_asset_groups = avalon_container.objects + obj_group_names = { c.name for c in obj_asset_groups - if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)] - coll_asset_groups = avalon_container.children_recursive - coll_group_names = [ + if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)} + coll_asset_groups = avalon_container.children + coll_group_names = { c.name for c in coll_asset_groups - if c.get(AVALON_PROPERTY)] - container_names = obj_group_names + coll_group_names + if c.get(AVALON_PROPERTY)} + container_names = obj_group_names.union(coll_group_names) count = 1 name = f"{asset}_{count:0>2}_{subset}" while name in container_names: From 59bb86dc3337cb340d3ed9bf1c842167f98fa6b0 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 2 Nov 2023 11:42:39 +0100 Subject: [PATCH 090/298] Rename BlenderCreator into BaseCreator --- openpype/hosts/blender/api/plugin.py | 2 +- openpype/hosts/blender/plugins/create/create_action.py | 2 +- openpype/hosts/blender/plugins/create/create_animation.py | 2 +- openpype/hosts/blender/plugins/create/create_camera.py | 2 +- openpype/hosts/blender/plugins/create/create_layout.py | 2 +- openpype/hosts/blender/plugins/create/create_model.py | 2 +- openpype/hosts/blender/plugins/create/create_pointcache.py | 2 +- openpype/hosts/blender/plugins/create/create_render.py | 2 +- openpype/hosts/blender/plugins/create/create_review.py | 2 +- openpype/hosts/blender/plugins/create/create_rig.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index dbd9f25d68..3ddc375670 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -140,7 +140,7 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class BlenderCreator(Creator): +class BaseCreator(Creator): """Base class for Blender Creator plug-ins.""" defaults = ['Main'] diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index e7b689c54e..7d00aa1dcb 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -8,7 +8,7 @@ from openpype.hosts.blender.api import lib from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): +class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): """Action output for character rigs""" identifier = "io.openpype.creators.blender.action" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 8b4214ceda..6cfd054e74 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateAnimation(plugin.BlenderCreator): +class CreateAnimation(plugin.BaseCreator): """Animation output for character rigs""" identifier = "io.openpype.creators.blender.animation" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 4747e50b2e..5d9682e575 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateCamera(plugin.BlenderCreator): +class CreateCamera(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.camera" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 0c97d57af3..ed47b0632f 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateLayout(plugin.BlenderCreator): +class CreateLayout(plugin.BaseCreator): """Layout output for character rigs""" identifier = "io.openpype.creators.blender.layout" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 761d9fca9f..949fae0f76 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateModel(plugin.BlenderCreator): +class CreateModel(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.model" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index a40bd5af61..2ad12caa9c 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -8,7 +8,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class CreatePointcache(plugin.BlenderCreator): +class CreatePointcache(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.pointcache" diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index ab3119b32e..45570f3491 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateRenderlayer(plugin.BlenderCreator): +class CreateRenderlayer(plugin.BaseCreator): """Single baked camera""" name = "renderingMain" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 8c9a8d5927..e8b893b4c0 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateReview(plugin.BlenderCreator): +class CreateReview(plugin.BaseCreator): """Single baked camera""" identifier = "io.openpype.creators.blender.review" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 110a9f5c8e..6223e64174 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateRig(plugin.BlenderCreator): +class CreateRig(plugin.BaseCreator): """Artist-friendly rig with controls to direct motion""" identifier = "io.openpype.creators.blender.rig" From 88116be4c63079598ec8a38b45ad3cc329155a87 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 20:44:21 +0800 Subject: [PATCH 091/298] allows users to preset the settings before the creator setting --- .../hosts/max/plugins/create/create_review.py | 58 ++++++++--- openpype/plugins/publish/extract_review.py | 2 +- .../defaults/project_settings/max.json | 10 ++ .../projects_schema/schema_project_max.json | 98 +++++++++++++++++++ 4 files changed, 154 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 8052b74f06..67dc158001 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -12,6 +12,32 @@ class CreateReview(plugin.MaxCreator): family = "review" icon = "video-camera" + review_width = 1920 + review_height = 1080 + percentSize = 100 + keep_images = False + image_format = "png" + visual_style = "Realistic" + viewport_preset = "Quality" + vp_texture = True + anti_aliasing = None + + + @classmethod + def apply_settings(cls, project_settings): + settings = project_settings["max"]["PreviewAnimation"] # noqa + + # Take some defaults from settings + cls.review_width = settings.get("review_width", cls.review_width) + cls.review_height = settings.get("review_height", cls.review_height) + cls.percentSize = settings.get("percentSize", cls.percentSize) + cls.keep_images = settings.get("keep_images", cls.keep_images) + cls.image_format = settings.get("image_format", cls.image_format) + cls.visual_style = settings.get("visual_style", cls.visual_style) + cls.viewport_preset = settings.get("viewport_preset", cls.viewport_preset) + cls.vp_texture = settings.get("vp_texture", cls.vp_texture) + cls.anti_aliasing = settings.get("anti_aliasing", cls.anti_aliasing) + def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance creator_attributes = instance_data.setdefault( @@ -23,6 +49,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", + "anti_aliasing", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -33,7 +60,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = ["exr", "jpg", "png"] + image_format_enum = ["exr", "jpg", "png", "tga"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", @@ -45,41 +72,46 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum = [ "Quality", "Standard", "Performance", "DXMode", "Customize"] + anti_aliasing_enum = ["None", "2X", "4X", "8X"] return [ NumberDef("review_width", label="Review width", decimals=0, minimum=0, - default=1920), + default=self.review_width), NumberDef("review_height", label="Review height", decimals=0, minimum=0, - default=1080), - BoolDef("keepImages", - label="Keep Image Sequences", - default=False), - EnumDef("imageFormat", - image_format_enum, - default="png", - label="Image Format Options"), + default=self.review_height), NumberDef("percentSize", label="Percent of Output", default=100, minimum=1, decimals=0), + BoolDef("keepImages", + label="Keep Image Sequences", + default=self.keep_images), + EnumDef("imageFormat", + image_format_enum, + default=self.image_format, + label="Image Format Options"), EnumDef("visualStyleMode", visual_style_preset_enum, - default="Realistic", + default=self.visual_style, label="Preference"), EnumDef("viewportPreset", preview_preset_enum, - default="Quality", + default=self.viewport_preset, label="Pre-View Preset"), + EnumDef("anti_aliasing", + anti_aliasing_enum, + default=self.anti_aliasing, + label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", - default=False) + default=self.vp_texture) ] def get_pre_create_attr_defs(self): diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0ae941511c..db8a030dfa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -68,7 +68,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..c610a963d4 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -16,6 +16,16 @@ "image_format": "exr", "multipass": true }, + "PreviewAnimation": { + "review_width": 1920, + "review_height": 1080, + "percentSize": 100, + "keep_images": false, + "image_format": "png", + "visual_style": "Realistic", + "viewport_preset": "Quality", + "vp_texture": true + }, "PointCloud": { "attribute": { "Age": "age", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index e314174dff..b012e73fc4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -65,6 +65,104 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "PreviewAnimation", + "label": "Preview Animation", + "children": [ + { + "type": "number", + "key": "review_width", + "label": "Review Width" + }, + { + "type": "number", + "key": "review_height", + "label": "Review Height" + }, + { + "type": "number", + "key": "percentSize", + "label": "Percent of Output" + }, + { + "type": "boolean", + "key": "keep_images", + "label": "Keep Image Sequences" + }, + { + "key": "image_format", + "label": "Image Format Options", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "exr"}, + {"jpg": "jpg"}, + {"png": "png"}, + {"tga": "tga"} + ] + }, + { + "key": "visual_style", + "label": "Preference", + "type": "enum", + "multiselection": false, + "defaults": "Realistic", + "enum_items": [ + {"Realistic": "Realistic"}, + {"Shaded": "Shaded"}, + {"Facets": "Facets"}, + {"ConsistentColors": "ConsistentColors"}, + {"HiddenLine": "HiddenLine"}, + {"Wireframe": "Wireframe"}, + {"BoundingBox": "BoundingBox"}, + {"Ink": "Ink"}, + {"ColorInk": "ColorInk"}, + {"Acrylic": "Acrylic"}, + {"Tech": "Tech"}, + {"Graphite": "Graphite"}, + {"ColorPencil": "ColorPencil"}, + {"Pastel": "Pastel"}, + {"Clay": "Clay"}, + {"ModelAssist": "ModelAssist"} + ] + }, + { + "key": "viewport_preset", + "label": "Pre-View Preset", + "type": "enum", + "multiselection": false, + "defaults": "Quality", + "enum_items": [ + {"Quality": "Quality"}, + {"Standard": "Standard"}, + {"Performance": "Performance"}, + {"DXMode": "DXMode"}, + {"Customize": "Customize"} + ] + }, + { + "key": "anti_aliasing", + "label": "Anti-aliasing Quality", + "type": "enum", + "multiselection": false, + "defaults": "None", + "enum_items": [ + {"None": "None"}, + {"2X": "2X"}, + {"4X": "4X"}, + {"8X": "8X"} + ] + }, + { + "type": "boolean", + "key": "vp_texture", + "label": "Viewport Texture" + } + ] + }, { "type": "dict", "collapsible": true, From 597260ad520393f2942b5685a817210d18544f45 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 20:49:40 +0800 Subject: [PATCH 092/298] cosmetic fix --- openpype/settings/defaults/project_settings/max.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index c610a963d4..ac04c60b54 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -24,6 +24,7 @@ "image_format": "png", "visual_style": "Realistic", "viewport_preset": "Quality", + "anti_aliasing": "None", "vp_texture": true }, "PointCloud": { From 026aae1d0d9ebc6409acedc545c9d20950b5365a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 21:35:18 +0800 Subject: [PATCH 093/298] add AA quality setting options & add the correct aspect ratio --- openpype/hosts/max/api/preview_animation.py | 4 +++- openpype/hosts/max/plugins/create/create_review.py | 8 +++++++- openpype/hosts/max/plugins/publish/collect_review.py | 1 + openpype/plugins/publish/extract_review.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index dcf243d31e..5f36b12edb 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -189,7 +189,8 @@ def _render_preview_animation_max_pre_2024( dib = rt.gw.getViewportDib() dib_width = float(dib.width) dib_height = float(dib.height) - renderRatio = float(dib_width / dib_height) + # aspect ratio + renderRatio = rt.getRendImageAspect() if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) @@ -311,6 +312,7 @@ def viewport_options_for_preview_animation(): viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", + "AntialiasingQuality": "None", "UseTextureEnabled": False } viewport_options["vp_btn_mgr"] = { diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 8052b74f06..331d2f30ea 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -23,6 +23,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", + "antialiasingQuality", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -33,7 +34,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = ["exr", "jpg", "png"] + image_format_enum = ["exr", "jpg", "png", "tga"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", @@ -45,6 +46,7 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum = [ "Quality", "Standard", "Performance", "DXMode", "Customize"] + anti_aliasing_enum = ["None", "2X", "4X", "8X"] return [ NumberDef("review_width", @@ -77,6 +79,10 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum, default="Quality", label="Pre-View Preset"), + EnumDef("antialiasingQuality", + anti_aliasing_enum, + default="None", + label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", default=False) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index b1d9c2d25e..a579b3f4b0 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -93,6 +93,7 @@ class CollectReview(pyblish.api.InstancePlugin, nitrous_viewport = { "VisualStyleMode": creator_attrs["visualStyleMode"], "ViewportPreset": creator_attrs["viewportPreset"], + "AntialiasingQuality": creator_attrs["antialiasingQuality"], "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0ae941511c..db8a030dfa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -68,7 +68,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts From db26cdd6e35a08d0025dcf8a54024b17af484099 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 21:55:22 +0800 Subject: [PATCH 094/298] hound --- openpype/hosts/max/plugins/create/create_review.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 67dc158001..ed0359ebd7 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -22,7 +22,6 @@ class CreateReview(plugin.MaxCreator): vp_texture = True anti_aliasing = None - @classmethod def apply_settings(cls, project_settings): settings = project_settings["max"]["PreviewAnimation"] # noqa @@ -34,9 +33,11 @@ class CreateReview(plugin.MaxCreator): cls.keep_images = settings.get("keep_images", cls.keep_images) cls.image_format = settings.get("image_format", cls.image_format) cls.visual_style = settings.get("visual_style", cls.visual_style) - cls.viewport_preset = settings.get("viewport_preset", cls.viewport_preset) + cls.viewport_preset = settings.get( + "viewport_preset", cls.viewport_preset) cls.vp_texture = settings.get("vp_texture", cls.vp_texture) - cls.anti_aliasing = settings.get("anti_aliasing", cls.anti_aliasing) + cls.anti_aliasing = settings.get( + "anti_aliasing", cls.anti_aliasing) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance @@ -49,7 +50,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", - "anti_aliasing", + "antialiasingQuality", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -105,7 +106,7 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum, default=self.viewport_preset, label="Pre-View Preset"), - EnumDef("anti_aliasing", + EnumDef("antialiasingQuality", anti_aliasing_enum, default=self.anti_aliasing, label="Anti-aliasing Quality"), From 487d8dfde72a9af0b2964a15d4b57a07954ce767 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 22:34:20 +0800 Subject: [PATCH 095/298] make sure the code doesn't break the extractor --- openpype/hosts/max/api/preview_animation.py | 14 +++++++++++++- .../hosts/max/plugins/publish/collect_review.py | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 5f36b12edb..0754fa61c4 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -52,6 +52,7 @@ def viewport_layout_and_camera(camera, layout="layout_1"): @contextlib.contextmanager def viewport_preference_setting(general_viewport, + nitrous_manager, nitrous_viewport, vp_button_mgr): """Function to set viewport setting during context @@ -59,6 +60,7 @@ def viewport_preference_setting(general_viewport, Args: camera (str): Viewport camera for review render general_viewport (dict): General viewport setting + nitrous_manager (dict): Nitrous graphic manager nitrous_viewport (dict): Nitrous setting for preview animation vp_button_mgr (dict): Viewport button manager Setting @@ -72,6 +74,9 @@ def viewport_preference_setting(general_viewport, vp_button_mgr_original = { key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr } + nitrous_manager_original = { + key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager + } nitrous_viewport_original = { key: getattr(viewport_setting, key) for key in nitrous_viewport } @@ -81,6 +86,8 @@ def viewport_preference_setting(general_viewport, rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) for key, value in vp_button_mgr.items(): setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_manager.items(): + setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport.items(): if nitrous_viewport[key] != nitrous_viewport_original[key]: setattr(viewport_setting, key, value) @@ -91,6 +98,8 @@ def viewport_preference_setting(general_viewport, rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) for key, value in vp_button_mgr_original.items(): setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_manager_original.items(): + setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport_original.items(): setattr(viewport_setting, key, value) @@ -258,6 +267,7 @@ def render_preview_animation( if int(get_max_version()) < 2024: with viewport_preference_setting( viewport_options["general_viewport"], + viewport_options["nitrous_manager"], viewport_options["nitrous_viewport"], viewport_options["vp_btn_mgr"] ): @@ -309,10 +319,12 @@ def viewport_options_for_preview_animation(): "dspBkg": True, "dspGrid": False } + viewport_options["nitrous_manager"] = { + "AntialiasingQuality": "None" + } viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", - "AntialiasingQuality": "None", "UseTextureEnabled": False } viewport_options["vp_btn_mgr"] = { diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index a579b3f4b0..e7e957e6f1 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -90,14 +90,17 @@ class CollectReview(pyblish.api.InstancePlugin, "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") } + nitrous_manager = { + "AntialiasingQuality": creator_attrs["antialiasingQuality"], + } nitrous_viewport = { "VisualStyleMode": creator_attrs["visualStyleMode"], "ViewportPreset": creator_attrs["viewportPreset"], - "AntialiasingQuality": creator_attrs["antialiasingQuality"], "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { "general_viewport": general_viewport, + "nitrous_manager": nitrous_manager, "nitrous_viewport": nitrous_viewport, "vp_btn_mgr": {"EnableButtons": False} } From ae536f2409cf9b622b617150dbd9778b6c180e91 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Nov 2023 16:15:03 +0100 Subject: [PATCH 096/298] traypublisher: folder path implementation --- openpype/hosts/traypublisher/api/editorial.py | 6 +++++- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 +++- .../traypublisher/plugins/publish/collect_shot_instances.py | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index e8f76bd314..2f5e709ffc 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -319,8 +319,12 @@ class ShotMetadataSolver: tasks = self._generate_tasks_from_settings( project_doc) + # generate hierarchy path from parents + hierarchy_path = self._create_hierarchy_path(parents) + return shot_name, { - "hierarchy": self._create_hierarchy_path(parents), + "hierarchy": hierarchy_path, + "folderPath": f"{hierarchy_path}/{shot_name}", "parents": parents, "tasks": tasks } diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 23cf066362..5dc3893697 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -217,9 +217,10 @@ or updating already created. Publishing will create OTIO file. } # Create otio editorial instance if AYON_SERVER_ENABLED: - asset_name = instance_data["folderPath"] + asset_name = instance_data.pop("folderPath") else: asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(self.project_name, asset_name) if pre_create_data["fps"] == "from_selection": @@ -682,6 +683,7 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { "asset_name": shot_name, + "Folder path": shot_metadata["folderPath"], "Parent hierarchy path": shot_metadata["hierarchy"], "workfile_start_frame": workfile_start_frame, "fps": fps, diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 78c1f14e4e..b08397caf7 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -120,7 +120,6 @@ class CollectShotInstance(pyblish.api.InstancePlugin): frame_dur = frame_end - frame_start return { - "asset": _cr_attrs["asset_name"], "fps": float(_cr_attrs["fps"]), "handleStart": _cr_attrs["handle_start"], "handleEnd": _cr_attrs["handle_end"], From 976ff308e97abed1efd6b87f09c477ad16c8f329 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 2 Nov 2023 16:18:51 +0100 Subject: [PATCH 097/298] Add Create workfile plugin --- .../blender/plugins/create/create_workfile.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 openpype/hosts/blender/plugins/create/create_workfile.py diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py new file mode 100644 index 0000000000..d1529f75f6 --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -0,0 +1,100 @@ +import bpy + +from openpype.pipeline import CreatedInstance, AutoCreator +from openpype.client import get_asset_by_name +from openpype.hosts.blender.api.plugin import BaseCreator +from openpype.hosts.blender.api.lib import imprint +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY + + +class CreateWorkfile(BaseCreator, AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.blender.workfile" + label = "Workfile" + family = "workfile" + icon = "fa5.file" + + def create(self): + """Create workfile instances.""" + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), + None, + ) + + project_name = self.project_name + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + + if not current_instance: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + task_name, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": task_name, + } + data.update( + self.get_dynamic_data( + task_name, + task_name, + asset_doc, + project_name, + host_name, + current_instance, + ) + ) + self.log.info("Auto-creating workfile instance...") + current_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(current_instance) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if it's different + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + task_name, task_name, asset_doc, project_name, host_name + ) + + current_instance.update( + { + "asset": asset_name, + "task": task_name, + "subset": subset_name, + } + ) + + def collect_instances(self): + """Collect workfile instances.""" + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["blender_cached_subsets"] + for node in cached_subsets.get(self.identifier, []): + created_instance = CreatedInstance.from_existing( + self.read_instance_node(node), self + ) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + """Update workfile instances.""" + for created_inst, _changes in update_list: + data = created_inst.data_to_store() + node = data.get("instance_node") + if not node: + task_name = self.create_context.get_current_task_name() + + bpy.context.scene[AVALON_PROPERTY] = node = { + "name": f"workfile{task_name}" + } + + created_inst["instance_node"] = node + data = created_inst.data_to_store() + + imprint(node, data) From 1e4005f4454efa4ea78f3bb14c0f96de400b8734 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 16:01:44 +0800 Subject: [PATCH 098/298] add AYON settings support and finalize the settings --- .../hosts/max/plugins/create/create_review.py | 2 +- server_addon/max/server/settings/main.py | 8 ++ .../max/server/settings/preview_animation.py | 92 +++++++++++++++++++ server_addon/max/server/version.py | 2 +- 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 server_addon/max/server/settings/preview_animation.py diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index ed0359ebd7..7aeea39b64 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -35,9 +35,9 @@ class CreateReview(plugin.MaxCreator): cls.visual_style = settings.get("visual_style", cls.visual_style) cls.viewport_preset = settings.get( "viewport_preset", cls.viewport_preset) - cls.vp_texture = settings.get("vp_texture", cls.vp_texture) cls.anti_aliasing = settings.get( "anti_aliasing", cls.anti_aliasing) + cls.vp_texture = settings.get("vp_texture", cls.vp_texture) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index 7f4561cbb1..0280fcebb9 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -4,6 +4,9 @@ from .imageio import ImageIOSettings from .render_settings import ( RenderSettingsModel, DEFAULT_RENDER_SETTINGS ) +from .preview_animation import ( + PreviewAnimationModel, DEFAULT_PREVIEW_ANIMATION_SETTINGS +) from .publishers import ( PublishersModel, DEFAULT_PUBLISH_SETTINGS ) @@ -29,6 +32,10 @@ class MaxSettings(BaseSettingsModel): default_factory=RenderSettingsModel, title="Render Settings" ) + PreviewAnimation: PreviewAnimationModel = Field( + default_factory=PreviewAnimationModel, + title="Preview Animation" + ) PointCloud: PointCloudSettings = Field( default_factory=PointCloudSettings, title="Point Cloud" @@ -40,6 +47,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "RenderSettings": DEFAULT_RENDER_SETTINGS, + "PreviewAnimation": DEFAULT_PREVIEW_ANIMATION_SETTINGS, "PointCloud": { "attribute": [ {"name": "Age", "value": "age"}, diff --git a/server_addon/max/server/settings/preview_animation.py b/server_addon/max/server/settings/preview_animation.py new file mode 100644 index 0000000000..2496e8e548 --- /dev/null +++ b/server_addon/max/server/settings/preview_animation.py @@ -0,0 +1,92 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +def image_format_enum(): + """Return enumerator for image output formats.""" + return [ + {"label": "exr", "value": "exr"}, + {"label": "jpg", "value": "jpg"}, + {"label": "png", "value": "png"}, + {"label": "tga", "value": "tga"} + ] + + +def visual_style_enum(): + """Return enumerator for viewport visual style.""" + return [ + {"label": "Realistic", "value": "Realistic"}, + {"label": "Shaded", "value": "Shaded"}, + {"label": "Facets", "value": "Facets"}, + {"label": "ConsistentColors", + "value": "ConsistentColors"}, + {"label": "Wireframe", "value": "Wireframe"}, + {"label": "BoundingBox", "value": "BoundingBox"}, + {"label": "Ink", "value": "Ink"}, + {"label": "ColorInk", "value": "ColorInk"}, + {"label": "Acrylic", "value": "Acrylic"}, + {"label": "Tech", "value": "Tech"}, + {"label": "Graphite", "value": "Graphite"}, + {"label": "ColorPencil", "value": "ColorPencil"}, + {"label": "Pastel", "value": "Pastel"}, + {"label": "Clay", "value": "Clay"}, + {"label": "ModelAssist", "value": "ModelAssist"} + ] + +def visual_preset_enum(): + """Return enumerator for viewport visual preset.""" + return [ + {"label": "Quality", "value": "Quality"}, + {"label": "Standard", "value": "Standard"}, + {"label": "Performance", "value": "Performance"}, + {"label": "DXMode", "value": "DXMode"}, + {"label": "Customize", "value": "Customize"}, + ] + + +def anti_aliasing_enum(): + """Return enumerator for viewport anti-aliasing.""" + return [ + {"label": "None", "value": "None"}, + {"label": "2X", "value": "2X"}, + {"label": "4X", "value": "4X"}, + {"label": "8X", "value": "8X"} + ] + + +class PreviewAnimationModel(BaseSettingsModel): + review_width: int = Field(1920, title="Review Width") + review_height: int = Field(1080, title="Review Height") + percentSize: float = Field(100.0, title="Percent of Output") + keep_images: bool = Field(False, title="Keep Image Sequences") + image_format: str = Field( + enum_resolver=image_format_enum, + title="Image Format Options" + ) + visual_style: str = Field( + enum_resolver=visual_style_enum, + title="Preference" + ) + viewport_preset: str = Field( + enum_resolver=visual_preset_enum, + title="Pre-View Preset" + ) + anti_aliasing: str = Field( + enum_resolver=anti_aliasing_enum, + title="Anti-aliasing Quality" + ) + vp_texture: bool = Field(True, title="Viewport Texture") + + +DEFAULT_PREVIEW_ANIMATION_SETTINGS = { + "review_width": 1920, + "review_height": 1080, + "percentSize": 100.0, + "keep_images": False, + "image_format": "png", + "visual_style": "Realistic", + "viewport_preset": "Quality", + "anti_aliasing": "None", + "vp_texture": True +} diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 2728379bbc0810155447cae35c01c65341ec12bd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 16:04:39 +0800 Subject: [PATCH 099/298] hound --- server_addon/max/server/settings/preview_animation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/max/server/settings/preview_animation.py b/server_addon/max/server/settings/preview_animation.py index 2496e8e548..759ce78291 100644 --- a/server_addon/max/server/settings/preview_animation.py +++ b/server_addon/max/server/settings/preview_animation.py @@ -34,6 +34,7 @@ def visual_style_enum(): {"label": "ModelAssist", "value": "ModelAssist"} ] + def visual_preset_enum(): """Return enumerator for viewport visual preset.""" return [ From 2d3ae5a0d346a95586abde0a11632291e8c83467 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:02:13 +0800 Subject: [PATCH 100/298] supports checking the required plugins with families type * --- .../plugins/publish/validate_loaded_plugin.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index dc82c7ed65..d6f849a57e 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -16,7 +16,11 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation - via studio defined project settings""" + via studio defined project settings + If families = ["*"], all the required plugins would be validated + If families + + """ order = ValidatorOrder hosts = ["max"] @@ -48,15 +52,17 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, } for families, plugin in required_plugins.items(): - families_list = families.split(",") - excluded_families = [family for family in families_list - if instance.data["family"] != family - and family != "_"] - if excluded_families: - self.log.debug("The {} instance is not part of {}.".format( - instance.data["family"], excluded_families - )) - return + # Out of for loop build the instance family lookup + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + self.log.debug(f"{instance_families}") + # In the for loop check whether any family matches + match_families = {fam.strip() for fam in families.split(",") if fam.strip()} + self.log.debug(f"match_families: {match_families}") + has_match = "*" in match_families or match_families.intersection( + instance_families) or families == "_" + if not has_match: + continue if not plugin: return @@ -66,7 +72,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if plugin_index is None: invalid.append( - f"Plugin {plugin} not exists in 3dsMax Plugin List." + f"Plugin {plugin} does not exist in 3dsMax Plugin List." ) continue @@ -82,12 +88,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, "- {}".format(invalid) for invalid in invalid_plugins ) report = ( - "Required plugins fails to load.\n\n" + "Required plugins are not loaded.\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to load the plugin." ) raise PublishValidationError( - report, title="Required Plugins unloaded") + report, title="Missing Required Plugins") @classmethod def repair(cls, instance): From e61d03556a1a7d915a6cd421c6a0ec45d40c8481 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:03:00 +0800 Subject: [PATCH 101/298] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index d6f849a57e..e58685cc4d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -57,7 +57,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families.update(instance.data.get("families", [])) self.log.debug(f"{instance_families}") # In the for loop check whether any family matches - match_families = {fam.strip() for fam in families.split(",") if fam.strip()} + match_families = {fam.strip() for fam in + families.split(",") if fam.strip()} self.log.debug(f"match_families: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) or families == "_" From 44a0b0e6284a73180dccdef244fe92c5761caaf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:42:44 +0800 Subject: [PATCH 102/298] make sure the aspect ratio correct --- openpype/hosts/max/api/preview_animation.py | 51 +++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 0754fa61c4..d6becd499e 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -166,13 +166,16 @@ def _render_preview_animation_max_2024( def _render_preview_animation_max_pre_2024( - filepath, startFrame, endFrame, percentSize, ext): + filepath, startFrame, endFrame, + width, height, percentSize, ext): """Render viewport animation by creating bitmaps ***For 3dsMax Version <2024 Args: filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame + width (int): render resolution width + height (int): render resolution height percentSize (float): render resolution multiplier by 100 e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x ext (str): image extension @@ -182,9 +185,8 @@ def _render_preview_animation_max_pre_2024( # get the screenshot percent = percentSize / 100.0 - res_width = int(round(rt.renderWidth * percent)) - res_height = int(round(rt.renderHeight * percent)) - viewportRatio = float(res_width / res_height) + res_width = int(round(width * percent)) + res_height = int(round(height * percent)) frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") files = [] @@ -196,10 +198,11 @@ def _render_preview_animation_max_pre_2024( res_width, res_height, filename=filepath ) dib = rt.gw.getViewportDib() - dib_width = float(dib.width) - dib_height = float(dib.height) + dib_width = rt.renderWidth + dib_height = rt.renderHeight # aspect ratio - renderRatio = rt.getRendImageAspect() + viewportRatio = dib_width /dib_height + renderRatio = float(res_width / res_height) if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) @@ -263,22 +266,24 @@ def render_preview_animation( viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): with viewport_layout_and_camera(camera): - with render_resolution(width, height): - if int(get_max_version()) < 2024: - with viewport_preference_setting( - viewport_options["general_viewport"], - viewport_options["nitrous_manager"], - viewport_options["nitrous_viewport"], - viewport_options["vp_btn_mgr"] - ): - return _render_preview_animation_max_pre_2024( - filepath, - start_frame, - end_frame, - percentSize, - ext - ) - else: + if int(get_max_version()) < 2024: + with viewport_preference_setting( + viewport_options["general_viewport"], + viewport_options["nitrous_manager"], + viewport_options["nitrous_viewport"], + viewport_options["vp_btn_mgr"] + ): + return _render_preview_animation_max_pre_2024( + filepath, + start_frame, + end_frame, + width, + height, + percentSize, + ext + ) + else: + with render_resolution(width, height): return _render_preview_animation_max_2024( filepath, start_frame, From 1132d1c9f3eb7d512b548988e717b16563dde119 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 15:43:33 +0100 Subject: [PATCH 103/298] avoid double slashes in context title path --- openpype/host/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 630fb873a8..afe06d1f55 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -170,7 +170,7 @@ class HostBase(object): if project_name: items.append(project_name) if asset_name: - items.append(asset_name) + items.append(asset_name.lstrip("/")) if task_name: items.append(task_name) if items: From 01c965eb9cb701eee833cb735dcec0daf4f6f61a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:43:34 +0800 Subject: [PATCH 104/298] hound --- openpype/hosts/max/api/preview_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index d6becd499e..bd0fee3658 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -201,7 +201,7 @@ def _render_preview_animation_max_pre_2024( dib_width = rt.renderWidth dib_height = rt.renderHeight # aspect ratio - viewportRatio = dib_width /dib_height + viewportRatio = dib_width / dib_height renderRatio = float(res_width / res_height) if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) From 49154c0750ab45ffeb7bd413ab99b79a6f6aadef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 16:45:12 +0100 Subject: [PATCH 105/298] fix create multishot layout --- .../maya/plugins/create/create_multishot_layout.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 0b027c02ea..7cd3fdbd17 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -45,10 +45,14 @@ class CreateMultishotLayout(plugin.MayaCreator): above is done. """ - current_folder = get_folder_by_name( - project_name=get_current_project_name(), - folder_name=get_current_asset_name(), - ) + project_name = get_current_project_name() + folder_path = get_current_asset_name() + if "/" in folder_path: + current_folder = get_folder_by_path(project_name, folder_path) + else: + current_folder = get_folder_by_name( + project_name, folder_name=folder_path + ) current_path_parts = current_folder["path"].split("/") From 10aea1088a68f49218255e4988febe4bc1f0f462 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 3 Nov 2023 17:06:44 +0100 Subject: [PATCH 106/298] Add collect workfile to blender host --- .../plugins/publish/collect_workfile.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/collect_workfile.py diff --git a/openpype/hosts/blender/plugins/publish/collect_workfile.py b/openpype/hosts/blender/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..e431405e80 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_workfile.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import bpy +from pyblish.api import InstancePlugin, CollectorOrder + + +class CollectWorkfile(InstancePlugin): + """Inject workfile data into its instance.""" + + order = CollectorOrder + label = "Collect Workfile" + hosts = ["blender"] + families = ["workfile"] + + def process(self, instance): + """Process collector.""" + + context = instance.context + filepath = Path(context.data["currentFile"]) + ext = filepath.suffix + + instance.data.update( + { + "setMembers": [filepath.as_posix()], + "frameStart": context.data.get("frameStart", 1), + "frameEnd": context.data.get("frameEnd", 1), + "handleStart": context.data.get("handleStart", 1), + "handledEnd": context.data.get("handleEnd", 1), + "representations": [ + { + "name": ext.lstrip("."), + "ext": ext, + "files": filepath.name, # TODO resources + "stagingDir": filepath.parent, + } + ], + } + ) From 821b478830f2f75f733d82374bd06195db01e9d8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 16:50:11 +0000 Subject: [PATCH 107/298] Get the selection when creating the instance --- .../plugins/create/create_blendScene.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 96e63924d3..970be157b9 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -15,6 +15,8 @@ class CreateBlendScene(plugin.Creator): family = "blendScene" icon = "cubes" + maintain_selection = False + def process(self): """ Run the creator on Blender main thread""" mti = ops.MainThreadItem(self._process) @@ -38,4 +40,29 @@ class CreateBlendScene(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) + try: + area = next( + area for area in bpy.context.window.screen.areas + if area.type == 'OUTLINER') + region = next( + region for region in area.regions + if region.type == 'WINDOW') + except StopIteration as e: + raise RuntimeError("Could not find outliner. An outliner space " + "must be in the main Blender window.") from e + + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + screen=bpy.context.window.screen + ): + ids = bpy.context.selected_ids + + for id in ids: + if isinstance(id, bpy.types.Collection): + asset_group.children.link(id) + elif isinstance(id, bpy.types.Object): + asset_group.objects.link(id) + return asset_group From b5ebe86b1482d5790296283323e96aab060dbad6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 16:51:21 +0000 Subject: [PATCH 108/298] Hound fixes --- openpype/hosts/blender/plugins/create/create_blendScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 970be157b9..791e741ca7 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -41,7 +41,7 @@ class CreateBlendScene(plugin.Creator): lib.imprint(asset_group, self.data) try: - area = next( + area = next( area for area in bpy.context.window.screen.areas if area.type == 'OUTLINER') region = next( From e6d13db010609fabe648e43a9745f94e2c355a14 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:27:25 +0000 Subject: [PATCH 109/298] Keeps the transform when updating --- openpype/hosts/blender/plugins/load/load_blendscene.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index fe7afb3119..34030d9d84 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -136,13 +136,18 @@ class BlendSceneLoader(plugin.AssetLoader): f"The asset is not loaded: {container['objectName']}" ) + # Get the parents of the members of the asset group, so we can + # re-link them after the update. + # Also gets the transform for each object to reapply after the update. collection_parents = {} + member_transforms = {} members = asset_group.get(AVALON_PROPERTY).get("members", []) loaded_collections = {c for c in bpy.data.collections if c in members} loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS)) for member in members: if isinstance(member, bpy.types.Object): member_parents = set(member.users_collection) + member_transforms[member.name] = member.matrix_basis.copy() elif isinstance(member, bpy.types.Collection): member_parents = { c for c in bpy.data.collections if c.user_of_id(member)} @@ -167,6 +172,9 @@ class BlendSceneLoader(plugin.AssetLoader): parent.objects.link(member) elif isinstance(member, bpy.types.Collection): parent.children.link(member) + if (member.name in member_transforms and + isinstance(member, bpy.types.Object)): + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From 4a9f1e7d785298b4df94a1a61bc983cdc73c37b9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:29:12 +0000 Subject: [PATCH 110/298] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 34030d9d84..28dcf4fc70 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -174,7 +174,7 @@ class BlendSceneLoader(plugin.AssetLoader): parent.children.link(member) if (member.name in member_transforms and isinstance(member, bpy.types.Object)): - member.matrix_basis = member_transforms[member.name] + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From 8f4d490af25ad8765c6f7ae056a57e00b97a228f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:30:25 +0000 Subject: [PATCH 111/298] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 28dcf4fc70..b1b2c3ba79 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -172,9 +172,10 @@ class BlendSceneLoader(plugin.AssetLoader): parent.objects.link(member) elif isinstance(member, bpy.types.Collection): parent.children.link(member) - if (member.name in member_transforms and - isinstance(member, bpy.types.Object)): - member.matrix_basis = member_transforms[member.name] + if member.name in member_transforms and isinstance( + member, bpy.types.Object + ): + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From b079ca8d0f76ede60866dcb26f506189cbfcea9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 15:41:20 +0800 Subject: [PATCH 112/298] clean up the duplicated variable --- openpype/settings/ayon_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 88fbbd5124..fa73199269 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -649,7 +649,6 @@ def _convert_3dsmax_project_settings(ayon_settings, output): attributes = {} ayon_publish["ValidateAttributes"]["attributes"] = attributes - ayon_publish = ayon_max["publish"] if "ValidateLoadedPlugin" in ayon_publish: family_plugin_mapping = ( ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] From 7b9c6c3b99900dde8ea841a2bfe25ea02f4cbeff Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Nov 2023 10:30:13 +0000 Subject: [PATCH 113/298] Added validator to verify that the instance is not empty --- .../publish/validate_instance_empty.py | 18 ++++++++++ .../defaults/project_settings/blender.json | 5 +++ .../schemas/schema_blender_publish.json | 16 +++++++++ .../server/settings/publish_plugins.py | 35 ++++++++++++------- server_addon/blender/server/version.py | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/validate_instance_empty.py diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py new file mode 100644 index 0000000000..66d8b45e1e --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -0,0 +1,18 @@ +import bpy + +import pyblish.api + + +class ValidateInstanceEmpty(pyblish.api.InstancePlugin): + """Validator to verify that the instance is not empty""" + + order = pyblish.api.ValidatorOrder - 0.01 + hosts = ["blender"] + families = ["blendScene"] + label = "Validate Instance is not Empty" + optional = False + + def process(self, instance): + collection = bpy.data.collections[instance.name] + if not (collection.objects or collection.children): + raise RuntimeError(f"Instance {instance.name} is empty.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 7fb8c333a6..385e97ef91 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -71,6 +71,11 @@ "optional": false, "active": true }, + "ValidateInstanceEmpty": { + "enabled": true, + "optional": false, + "active": true + }, "ExtractBlend": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index b84c663e6c..e4f1096223 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -79,6 +79,22 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "BlendScene", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateInstanceEmpty", + "label": "Validate Instance is not Empty" + } + ] + } + ] + }, { "type": "collapsible-wrap", "label": "Render", diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 27dc0b232f..bb68b40cbb 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -61,26 +61,16 @@ class PublishPuginsModel(BaseSettingsModel): ValidateCameraZeroKeyframe: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Camera Zero Keyframe", - section="Validators" + section="General Validators" ) ValidateFileSaved: ValidateFileSavedModel = Field( default_factory=ValidateFileSavedModel, title="Validate File Saved", - section="Validators" - ) - ValidateRenderCameraIsSet: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Render Camera Is Set", - section="Validators" - ) - ValidateDeadlinePublish: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Render Output for Deadline", - section="Validators" ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, - title="Validate Mesh Has Uvs" + title="Validate Mesh Has Uvs", + section="Model Validators" ) ValidateMeshNoNegativeScale: ValidatePluginModel = Field( default_factory=ValidatePluginModel, @@ -94,6 +84,20 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate No Colons In Name" ) + ValidateInstanceEmpty: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Instance is not Empty", + section="BlendScene Validators" + ) + ValidateRenderCameraIsSet: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Camera Is Set", + section="Render Validators" + ) + ValidateDeadlinePublish: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Output for Deadline", + ) ExtractBlend: ExtractBlendModel = Field( default_factory=ExtractBlendModel, title="Extract Blend", @@ -179,6 +183,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateInstanceEmpty": { + "enabled": True, + "optional": False, + "active": True + }, "ExtractBlend": { "enabled": True, "optional": True, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index ae7362549b..1276d0254f 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.5" From 026fa6567680e4079c20158f16edad466b777999 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 21:19:30 +0800 Subject: [PATCH 114/298] value tweaks on aspect ratio --- openpype/hosts/max/api/preview_animation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bd0fee3658..bbf05f4ca9 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -185,8 +185,8 @@ def _render_preview_animation_max_pre_2024( # get the screenshot percent = percentSize / 100.0 - res_width = int(round(width * percent)) - res_height = int(round(height * percent)) + res_width = width * percent + res_height = height * percent frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") files = [] @@ -212,7 +212,7 @@ def _render_preview_animation_max_pre_2024( widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) - src_box_value = rt.Box2(0, leftEdge, widthCrop, dib_height) + src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) # copy the bitmap and close it rt.copy(tempImage_bmp, preview_res) From 1d34d5b1ae48ef69e12d30585b03a9352203f498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:04:08 +0100 Subject: [PATCH 115/298] use "asset" value instead of name from assetEntity --- .../hosts/maya/plugins/publish/validate_instance_in_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py index 4ded57137c..edfb002278 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -74,4 +74,4 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, @staticmethod def get_context_asset(instance): - return instance.context.data["assetEntity"]["name"] + return instance.context.data["asset"] From f2e14e7f4aa4c9d1557e3fdacc05cc564c21d7bb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:05:37 +0100 Subject: [PATCH 116/298] use asset and project from instance and context data --- openpype/hosts/maya/plugins/publish/collect_review.py | 8 ++++---- .../hosts/maya/plugins/publish/validate_model_name.py | 6 ++++-- .../hosts/maya/plugins/publish/validate_shader_name.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 586939a3b8..0930da8f27 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -3,7 +3,7 @@ from maya import cmds, mel import pyblish.api from openpype.client import get_subset_by_name -from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline import KnownPublishError from openpype.hosts.maya.api import lib @@ -116,10 +116,10 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True else: - task = legacy_io.Session["AVALON_TASK"] - legacy_subset_name = task + 'Review' + project_name = instance.context.data["projectName"] asset_doc = instance.context.data['assetEntity'] - project_name = legacy_io.active_project() + task = instance.context.data["task"] + legacy_subset_name = task + 'Review' subset_doc = get_subset_by_name( project_name, legacy_subset_name, diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index f4c1aa39c7..11f59bb439 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -67,13 +67,15 @@ class ValidateModelName(pyblish.api.InstancePlugin, regex = cls.top_level_regex r = re.compile(regex) m = r.match(top_group) + project_name = instance.context.data["projectName"] + current_asset_name = instance.context.data["asset"] if m is None: cls.log.error("invalid name on: {}".format(top_group)) cls.log.error("name doesn't match regex {}".format(regex)) invalid.append(top_group) else: if "asset" in r.groupindex: - if m.group("asset") != legacy_io.Session["AVALON_ASSET"]: + if m.group("asset") != current_asset_name: cls.log.error("Invalid asset name in top level group.") return top_group if "subset" in r.groupindex: @@ -81,7 +83,7 @@ class ValidateModelName(pyblish.api.InstancePlugin, cls.log.error("Invalid subset name in top level group.") return top_group if "project" in r.groupindex: - if m.group("project") != legacy_io.Session["AVALON_PROJECT"]: + if m.group("project") != project_name: cls.log.error("Invalid project name in top level group.") return top_group diff --git a/openpype/hosts/maya/plugins/publish/validate_shader_name.py b/openpype/hosts/maya/plugins/publish/validate_shader_name.py index 36bb2c1fee..d6486dea7f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shader_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_shader_name.py @@ -51,7 +51,7 @@ class ValidateShaderName(pyblish.api.InstancePlugin, descendants = cmds.ls(descendants, noIntermediate=True, long=True) shapes = cmds.ls(descendants, type=["nurbsSurface", "mesh"], long=True) - asset_name = instance.data.get("asset", None) + asset_name = instance.data.get("asset") # Check the number of connected shadingEngines per shape regex_compile = re.compile(cls.regex) From 3fdccc886f4d8f785c0d9297264b15a6904d496d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Nov 2023 15:46:12 +0800 Subject: [PATCH 117/298] make sure the extraction would be skipped if there is empty list of animated_skeleton --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 8288bc9329..ad9ca385dc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -42,6 +42,11 @@ class ExtractFBXAnimation(publish.Extractor): # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile + if not out_members: + self.log.debug( + "Top group of animated skeleton not found...skipping extraction") + return + namespace = get_namespace(out_members[0]) relative_out_members = [ strip_namespace(node, namespace) for node in out_members From 69c33f9c8f560c3c92407ae13f471cba36b9790e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Nov 2023 15:52:19 +0800 Subject: [PATCH 118/298] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index ad9ca385dc..c6f8029e7d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,7 +44,8 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile if not out_members: self.log.debug( - "Top group of animated skeleton not found...skipping extraction") + "Top group of animated skeleton not found.." + "skipping extraction") return namespace = get_namespace(out_members[0]) From 93101debd87d1d72cbeec69f2131f093f06c27a7 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 7 Nov 2023 10:23:55 +0100 Subject: [PATCH 119/298] collect workfile remove unused import and comment --- openpype/hosts/blender/plugins/publish/collect_workfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_workfile.py b/openpype/hosts/blender/plugins/publish/collect_workfile.py index e431405e80..01c033084b 100644 --- a/openpype/hosts/blender/plugins/publish/collect_workfile.py +++ b/openpype/hosts/blender/plugins/publish/collect_workfile.py @@ -1,6 +1,5 @@ from pathlib import Path -import bpy from pyblish.api import InstancePlugin, CollectorOrder @@ -30,7 +29,7 @@ class CollectWorkfile(InstancePlugin): { "name": ext.lstrip("."), "ext": ext, - "files": filepath.name, # TODO resources + "files": filepath.name, "stagingDir": filepath.parent, } ], From 7f5986a683299a10d65789cc9ac86362d97262d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 11:43:08 +0100 Subject: [PATCH 120/298] fix maya workfile creator --- openpype/hosts/maya/plugins/create/create_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index 7282fc6b8b..198f9c4a36 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -72,7 +72,10 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ) asset_name = get_asset_name_identifier(asset_doc) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name From 1e819a26838570aaf93cf89e23adf5f673413a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 7 Nov 2023 13:28:26 +0100 Subject: [PATCH 121/298] Update openpype/hosts/traypublisher/api/editorial.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/traypublisher/api/editorial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 2f5e709ffc..ddb369468f 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -321,7 +321,8 @@ class ShotMetadataSolver: # generate hierarchy path from parents hierarchy_path = self._create_hierarchy_path(parents) - + if hierarchy_path: + hierarchy_path = f"/{hierarchy_path}" return shot_name, { "hierarchy": hierarchy_path, "folderPath": f"{hierarchy_path}/{shot_name}", From 46761a028f6a6c91815f6b8c7705abf706f48380 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 13:31:04 +0100 Subject: [PATCH 122/298] traypublisher: editorial better handling hierarchy for folder path --- openpype/hosts/traypublisher/api/editorial.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index ddb369468f..1a83fcecbd 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -322,10 +322,13 @@ class ShotMetadataSolver: # generate hierarchy path from parents hierarchy_path = self._create_hierarchy_path(parents) if hierarchy_path: - hierarchy_path = f"/{hierarchy_path}" + folder_path = f"/{hierarchy_path}/{shot_name}" + else: + folder_path = f"/{shot_name}" + return shot_name, { "hierarchy": hierarchy_path, - "folderPath": f"{hierarchy_path}/{shot_name}", + "folderPath": folder_path, "parents": parents, "tasks": tasks } From 62af7138e11cc11509b91ec2f1ee7cc1995fb10b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 13:38:43 +0100 Subject: [PATCH 123/298] traypublisher: editorial ayon attribute only https://github.com/ynput/OpenPype/pull/5873#discussion_r1384655823 --- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 5dc3893697..59f24a2a2b 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -683,13 +683,15 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { "asset_name": shot_name, - "Folder path": shot_metadata["folderPath"], "Parent hierarchy path": shot_metadata["hierarchy"], "workfile_start_frame": workfile_start_frame, "fps": fps, "handle_start": int(handle_start), "handle_end": int(handle_end) } + if AYON_SERVER_ENABLED: + creator_attributes["folderPath"] = shot_metadata["folderPath"] + creator_attributes.update(timing_data) # create shared new instance data From 7aa5b09ef26dd2da96cccb0eb28d79a6e3d20844 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 14:56:46 +0100 Subject: [PATCH 124/298] improving code --- openpype/hosts/traypublisher/api/editorial.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 1a83fcecbd..613f1de768 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -53,11 +53,11 @@ class ShotMetadataSolver: try: # format to new shot name return shot_rename_template.format(**data) - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct:: \n\n" f"From template string {shot_rename_template} > " - f"`{_E}` has no equivalent in \n" + f"`{_error}` has no equivalent in \n" f"{list(data.keys())} input formatting keys!" )) @@ -100,7 +100,7 @@ class ShotMetadataSolver: "at your project settings..." )) - # QUESTION:how to refactory `match[-1]` to some better way? + # QUESTION:how to refactor `match[-1]` to some better way? output_data[token_key] = match[-1] return output_data @@ -130,10 +130,10 @@ class ShotMetadataSolver: parent_token["name"]: parent_token["value"].format(**data) for parent_token in hierarchy_parents } - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct : \n" - f"`{_E}` has no equivalent in \n{list(data.keys())}" + f"`{_error}` has no equivalent in \n{list(data.keys())}" )) _parent_tokens_type = { @@ -147,10 +147,10 @@ class ShotMetadataSolver: try: parent_name = _parent.format( **_parent_tokens_formatting_data) - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct : \n\n" - f"`{_E}` from template string " + f"`{_error}` from template string " f"{shot_hierarchy['parents_path']}, " f" has no equivalent in \n" f"{list(_parent_tokens_formatting_data.keys())} parents" From 638c1c65e78cdf4fc9327820a265878f0a5e13a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 14:57:44 +0100 Subject: [PATCH 125/298] ayon distributed folder path from create to publish --- .../plugins/create/create_editorial.py | 69 +++++++++++-------- .../plugins/publish/collect_shot_instances.py | 8 ++- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 59f24a2a2b..8c5083bcb2 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -102,14 +102,23 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): label = "Editorial Shot" def get_instance_attr_defs(self): - attr_defs = [ - TextDef( - "asset_name", - label="Asset name", + instance_attributes = [] + if AYON_SERVER_ENABLED: + instance_attributes.append( + TextDef( + "folderPath", + label="Folder path" + ) ) - ] - attr_defs.extend(CLIP_ATTR_DEFS) - return attr_defs + else: + instance_attributes.append( + TextDef( + "shotName", + label="Shot name" + ) + ) + instance_attributes.extend(CLIP_ATTR_DEFS) + return instance_attributes class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): @@ -216,10 +225,7 @@ or updating already created. Publishing will create OTIO file. ] } # Create otio editorial instance - if AYON_SERVER_ENABLED: - asset_name = instance_data.pop("folderPath") - else: - asset_name = instance_data["asset"] + asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) @@ -671,7 +677,10 @@ or updating already created. Publishing will create OTIO file. } ) - self._validate_name_uniqueness(shot_name) + # It should be validated only in openpype since we are supporting + # publishing to AYON with folder path and uniqueness is not an issue + if not AYON_SERVER_ENABLED: + self._validate_name_uniqueness(shot_name) timing_data = self._get_timing_data( otio_clip, @@ -682,36 +691,42 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { - "asset_name": shot_name, - "Parent hierarchy path": shot_metadata["hierarchy"], + "workfile_start_frame": workfile_start_frame, "fps": fps, "handle_start": int(handle_start), "handle_end": int(handle_end) } - if AYON_SERVER_ENABLED: - creator_attributes["folderPath"] = shot_metadata["folderPath"] - + # add timing data creator_attributes.update(timing_data) - # create shared new instance data + # create base instance data base_instance_data = { "shotName": shot_name, "variant": variant_name, - - # HACK: just for temporal bug workaround - # TODO: should loockup shot name for update - "asset": parent_asset_name, "task": "", - "newAssetPublishing": True, - - # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, - # creator_attributes - "creator_attributes": creator_attributes } + # update base instance data with context data + # and also update creator attributes with context data + if AYON_SERVER_ENABLED: + # TODO: this is here just to be able to publish + # to AYON with folder path + creator_attributes["folderPath"] = shot_metadata.pop("folderPath") + base_instance_data["folderPath"] = parent_asset_name + else: + creator_attributes.update({ + "shotName": shot_name, + "Parent hierarchy path": shot_metadata["hierarchy"] + }) + + base_instance_data["asset"] = parent_asset_name + + + # add creator attributes to shared instance data + base_instance_data["creator_attributes"] = creator_attributes # add hierarchy shot metadata base_instance_data.update(shot_metadata) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index b08397caf7..65dd35782f 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -2,6 +2,8 @@ from pprint import pformat import pyblish.api import opentimelineio as otio +from openpype import AYON_SERVER_ENABLED + class CollectShotInstance(pyblish.api.InstancePlugin): """ Collect shot instances @@ -119,7 +121,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): frame_end = _cr_attrs["frameEnd"] frame_dur = frame_end - frame_start - return { + data = { "fps": float(_cr_attrs["fps"]), "handleStart": _cr_attrs["handle_start"], "handleEnd": _cr_attrs["handle_end"], @@ -132,6 +134,10 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "sourceOut": _cr_attrs["sourceOut"], "workfileFrameStart": workfile_start_frame } + if AYON_SERVER_ENABLED: + data["asset"] = _cr_attrs["asset"] + + return data def _solve_hierarchy_context(self, instance): """ Adding hierarchy data to context shared data. From cf356e7ecd66521f91996024bac4e911a4ce09ac Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 7 Nov 2023 16:00:50 +0100 Subject: [PATCH 126/298] add create render identifier --- openpype/hosts/blender/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 45570f3491..e036ae7df3 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -13,6 +13,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateRenderlayer(plugin.BaseCreator): """Single baked camera""" + identifier = "io.openpype.creators.blender.render" name = "renderingMain" label = "Render" family = "render" @@ -28,8 +29,7 @@ class CreateRenderlayer(plugin.BaseCreator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = instance_data.get("asset") - name = plugin.asset_name(asset, subset_name) + name = plugin.asset_name(instance_data.get("asset"), subset_name) asset_group = bpy.data.collections.new(name=name) try: From 44578b2121f1eee9fa6daca3e72dac047f41ebf2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 16:15:16 +0100 Subject: [PATCH 127/298] traypublisher: editorial collector asset to instance with ayon exception for folderPath --- .../traypublisher/plugins/publish/collect_shot_instances.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 65dd35782f..0b7e022658 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -135,7 +135,9 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "workfileFrameStart": workfile_start_frame } if AYON_SERVER_ENABLED: - data["asset"] = _cr_attrs["asset"] + data["asset"] = _cr_attrs["folderPath"] + else: + data["asset"] = _cr_attrs["shotName"] return data From f3370c0229da5ff9c323a7277f8711122b25a4b7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 7 Nov 2023 15:21:10 +0000 Subject: [PATCH 128/298] Changed how extractors name the output files --- openpype/hosts/blender/plugins/publish/extract_abc.py | 9 ++++++--- .../blender/plugins/publish/extract_abc_animation.py | 10 +++++++--- .../hosts/blender/plugins/publish/extract_blend.py | 9 ++++++--- .../plugins/publish/extract_blend_animation.py | 9 ++++++--- .../blender/plugins/publish/extract_camera_abc.py | 9 ++++++--- .../blender/plugins/publish/extract_camera_fbx.py | 9 ++++++--- openpype/hosts/blender/plugins/publish/extract_fbx.py | 9 ++++++--- .../blender/plugins/publish/extract_fbx_animation.py | 11 +++++++---- .../hosts/blender/plugins/publish/extract_layout.py | 10 +++++++--- .../blender/plugins/publish/extract_playblast.py | 5 ++++- .../blender/plugins/publish/extract_thumbnail.py | 5 ++++- 11 files changed, 65 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index b17d7cc6e4..59035d8f61 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -17,7 +17,10 @@ class ExtractABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -59,8 +62,8 @@ class ExtractABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") class ExtractModelABC(ExtractABC): diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 6866b05fea..0ac6f12de5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -17,7 +17,11 @@ class ExtractAnimationABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" + filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -66,5 +70,5 @@ class ExtractAnimationABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index c8eeef7fd7..0a9fb74f7b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -17,7 +17,10 @@ class ExtractBlend(publish.Extractor): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.blend" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -52,5 +55,5 @@ class ExtractBlend(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 661cecce81..3d36ee7ec3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -17,7 +17,10 @@ class ExtractBlendAnimation(publish.Extractor): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.blend" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -50,5 +53,5 @@ class ExtractBlendAnimation(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 5916564ac0..b6b38b41ff 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -18,7 +18,10 @@ class ExtractCameraABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -64,5 +67,5 @@ class ExtractCameraABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index a541f5b375..be9f178d1b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -17,7 +17,10 @@ class ExtractCamera(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.fbx" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -73,5 +76,5 @@ class ExtractCamera(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index f2ce117dcd..c21dc35ff6 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -18,7 +18,10 @@ class ExtractFBX(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.fbx" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -84,5 +87,5 @@ class ExtractFBX(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 5fe5931e65..ed4e7ecc6a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -86,7 +86,10 @@ class ExtractAnimationFBX(publish.Extractor): asset_group.select_set(True) armature.select_set(True) - fbx_filename = f"{instance.name}_{armature.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + fbx_filename = f"{instance_name}_{armature.name}.fbx" filepath = os.path.join(stagingdir, fbx_filename) override = plugin.create_blender_context( @@ -119,7 +122,7 @@ class ExtractAnimationFBX(publish.Extractor): pair[1].user_clear() bpy.data.actions.remove(pair[1]) - json_filename = f"{instance.name}.json" + json_filename = f"{instance_name}.json" json_path = os.path.join(stagingdir, json_filename) json_dict = { @@ -158,5 +161,5 @@ class ExtractAnimationFBX(publish.Extractor): instance.data["representations"].append(fbx_representation) instance.data["representations"].append(json_representation) - self.log.info("Extracted instance '{}' to: {}".format( - instance.name, fbx_representation)) + self.log.info( + f"Extracted instance '{instance_name}' to: {fbx_representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 05f86b8370..8e820ee84e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -212,7 +212,11 @@ class ExtractLayout(publish.Extractor): json_data.append(json_element) - json_filename = "{}.json".format(instance.name) + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + json_filename = f"{instance_name}.json" + json_path = os.path.join(stagingdir, json_filename) with open(json_path, "w+") as file: @@ -245,5 +249,5 @@ class ExtractLayout(publish.Extractor): } instance.data["representations"].append(fbx_representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, json_representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {json_representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index b0099cce85..805aacc5f4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -50,7 +50,10 @@ class ExtractPlayblast(publish.Extractor): # get output path stagingdir = self.staging_dir(instance) - filename = instance.name + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + filename = f"{asset_name}_{subset}" + path = os.path.join(stagingdir, filename) self.log.debug(f"Outputting images to {path}") diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 52e5d98fc4..e8a9c68dd1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -27,7 +27,10 @@ class ExtractThumbnail(publish.Extractor): self.log.debug("Extracting capture..") stagingdir = self.staging_dir(instance) - filename = instance.name + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + filename = f"{asset_name}_{subset}" + path = os.path.join(stagingdir, filename) self.log.debug(f"Outputting images to {path}") From e32263916c4856d3d65c4b1c31aeeeaf3047f018 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 17:26:29 +0100 Subject: [PATCH 129/298] traypublisher: label of instances with folder path --- .../traypublisher/plugins/create/create_editorial.py | 12 ++++++++---- .../plugins/publish/collect_shot_instances.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8c5083bcb2..128010cef9 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -606,19 +606,23 @@ or updating already created. Publishing will create OTIO file. Returns: str: label string """ - shot_name = instance_data["shotName"] + if AYON_SERVER_ENABLED: + asset_name = instance_data["creator_attributes"]["folderPath"] + else: + asset_name = instance_data["creator_attributes"]["shotName"] + variant_name = instance_data["variant"] family = preset["family"] - # get variant name from preset or from inharitance + # get variant name from preset or from inheritance _variant_name = preset.get("variant") or variant_name # subset name subset_name = "{}{}".format( family, _variant_name.capitalize() ) - label = "{}_{}".format( - shot_name, + label = "{} {}".format( + asset_name, subset_name ) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 0b7e022658..e00ac64244 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -155,7 +155,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): else {} ) - name = instance.data["asset"] + asset_name = instance.data["asset"] # get handles handle_start = int(instance.data["handleStart"]) @@ -177,7 +177,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): parents = instance.data.get('parents', []) - actual = {name: in_info} + actual = {asset_name: in_info} for parent in reversed(parents): parent_name = parent["entity_name"] From cfc54439095140f2b6e2d84adc9ebfa09e96a4d3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 16:35:28 +0800 Subject: [PATCH 130/298] clean up code tweaks for OP settings(not suitable for ayon yet) --- .../plugins/publish/validate_loaded_plugin.py | 82 ++++++++++--------- openpype/settings/ayon_settings.py | 3 + .../schemas/schema_max_publish.json | 10 ++- .../max/server/settings/publishers.py | 5 +- 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index e58685cc4d..8d59bbc120 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -51,34 +51,36 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, get_plugins()) } - for families, plugin in required_plugins.items(): - # Out of for loop build the instance family lookup - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - self.log.debug(f"{instance_families}") - # In the for loop check whether any family matches + # Build instance families lookup + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + self.log.debug(f"Checking plug-in validation for instance families: {instance_families}") + for families in required_plugins.keys(): + # Check for matching families match_families = {fam.strip() for fam in families.split(",") if fam.strip()} - self.log.debug(f"match_families: {match_families}") + self.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( - instance_families) or families == "_" + instance_families) + if not has_match: continue - if not plugin: - return + plugins = [plugin for plugin in required_plugins[families]["plugins"]] + for plugin in plugins: + if not plugin: + return + plugin_name = plugin.format(**os.environ).lower() + plugin_index = available_plugins.get(plugin_name) - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) + if plugin_index is None: + invalid.append( + f"Plugin {plugin} does not exist in 3dsMax Plugin List." + ) + continue - if plugin_index is None: - invalid.append( - f"Plugin {plugin} does not exist in 3dsMax Plugin List." - ) - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append(f"Plugin {plugin} not loaded.") + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + invalid.append(f"Plugin {plugin} not loaded.") return invalid @@ -108,25 +110,29 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, ["ValidateLoadedPlugin"] ["family_plugins_mapping"] ) - for families, plugin in required_plugins.items(): - families_list = families.split(",") - excluded_families = [family for family in families_list - if instance.data["family"] != family - and family != "_"] - if excluded_families: - cls.log.debug("The {} instance is not part of {}.".format( - instance.data["family"], excluded_families - )) - continue - if not plugin: + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + cls.log.debug(f"Checking plug-in validation for instance families: {instance_families}") + for families in required_plugins.keys(): + match_families = {fam.strip() for fam in + families.split(",") if fam.strip()} + cls.log.debug(f"Plug-in family requirements: {match_families}") + has_match = "*" in match_families or match_families.intersection( + instance_families) + + if not has_match: continue - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) + plugins = [plugin for plugin in required_plugins[families]["plugins"]] + for plugin in plugins: + if not plugin: + return + plugin_name = plugin.format(**os.environ).lower() + plugin_index = available_plugins.get(plugin_name) - if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {plugin}") - continue + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {plugin}") + continue - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - rt.pluginManager.loadPluginDll(plugin_index) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index fa73199269..0cefd047b1 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -653,6 +653,9 @@ def _convert_3dsmax_project_settings(ayon_settings, output): family_plugin_mapping = ( ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] ) + for item in family_plugin_mapping: + if "product_types" in item: + item["families"] = item.pop("product_types") new_family_plugin_mapping = { item["families"]: item["plugins"] for item in family_plugin_mapping diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index b48ce20f5d..c44c7525da 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -72,7 +72,15 @@ "label": "Family Plugins Mapping", "use_label_wrap": true, "object_type": { - "type": "text" + "type": "dict", + "children": [ + { + "key": "plugins", + "label": "plugins", + "type": "list", + "object_type": "text" + } + ] } } ] diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index d23acc6dd7..d7169f8b96 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -29,8 +29,9 @@ class ValidateAttributesModel(BaseSettingsModel): class FamilyPluginsMappingModel(BaseSettingsModel): _layout = "compact" - families: str = Field(title="Families") - plugins: str = Field(title="Plugins") + product_types: str = Field(title="Product Types") + plugins: list[str] = Field( + default_factory=list,title="Plugins") class ValidateLoadedPluginModel(BaseSettingsModel): From 105cf5c116ab755ddc315956b63145d6b511e7db Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:31:31 +0200 Subject: [PATCH 131/298] fix a typo in maya ayon settings --- server_addon/maya/server/settings/creators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 84e873589d..34a54832af 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -27,7 +27,7 @@ class CreateUnrealStaticMeshModel(BaseSettingsModel): default_factory=list, title="Default Products" ) - static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + static_mesh_prefix: str = Field("S", title="Static Mesh Prefix") collision_prefixes: list[str] = Field( default_factory=list, title="Collision Prefixes" From 60b775a4f410aec128627a8697004be5a45527cb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:31:49 +0200 Subject: [PATCH 132/298] fix some typos in Houdini ayon settings --- server_addon/houdini/server/settings/create.py | 2 +- server_addon/houdini/server/settings/publish.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server_addon/houdini/server/settings/create.py b/server_addon/houdini/server/settings/create.py index 81b871e83f..e8db917849 100644 --- a/server_addon/houdini/server/settings/create.py +++ b/server_addon/houdini/server/settings/create.py @@ -26,7 +26,7 @@ class CreateStaticMeshModel(BaseSettingsModel): default_factory=list, title="Default Products" ) - static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + static_mesh_prefix: str = Field("S", title="Static Mesh Prefix") collision_prefixes: list[str] = Field( default_factory=list, title="Collision Prefixes" diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 342bf957c1..92a676b0d0 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -33,9 +33,9 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): - CollectRopFrameRange: CollectRopFrameRangeModel = Field( - default_factory=CollectRopFrameRangeModel, - title="Collect Rop Frame Range.", + CollectAssetHandles: CollectAssetHandlesModel = Field( + default_factory=CollectAssetHandlesModel, + title="Collect Asset Handles.", section="Collectors" ) ValidateContainers: BasicValidateModel = Field( @@ -60,7 +60,7 @@ class PublishPluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_PUBLISH_SETTINGS = { - "CollectRopFrameRange": { + "CollectAssetHandles": { "use_asset_handles": True }, "ValidateContainers": { From 4311f4840dd3b39115e5ddd244c025d86df1f1c8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:38:21 +0200 Subject: [PATCH 133/298] bump addon versions --- server_addon/houdini/server/version.py | 2 +- server_addon/maya/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index c49a95c357..75cf7831c4 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.8" +__version__ = "0.2.9" diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 90ce344d3e..805897cda3 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.5" +__version__ = "0.1.6" From d15dfaaf849d51c49d9a0e14d5be8f1c6f85ea2d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 18:47:42 +0800 Subject: [PATCH 134/298] hound & code tweak regarding to OP setting --- .../plugins/publish/validate_loaded_plugin.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 8d59bbc120..d348e37abc 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Validator for Loaded Plugin.""" import os -from pyblish.api import InstancePlugin, ValidatorOrder +import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import ( @@ -13,7 +13,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - InstancePlugin): + pyblish.api.InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings @@ -22,24 +22,21 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, """ - order = ValidatorOrder + order = pyblish.api.ValidatorOrder hosts = ["max"] label = "Validate Loaded Plugins" optional = True actions = [RepairAction] + family_plugins_mapping = {} + def get_invalid(self, instance): """Plugin entry point.""" if not self.is_active(instance.data): self.log.debug("Skipping Validate Loaded Plugin...") return - required_plugins = ( - instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"] - ["family_plugins_mapping"] - ) - + required_plugins = self.family_plugins_mapping if not required_plugins: return @@ -54,11 +51,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, # Build instance families lookup instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) - self.log.debug(f"Checking plug-in validation for instance families: {instance_families}") - for families in required_plugins.keys(): + self.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + for family in required_plugins: # Check for matching families match_families = {fam.strip() for fam in - families.split(",") if fam.strip()} + family.split(",") if fam.strip()} self.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) @@ -66,7 +64,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[families]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return @@ -75,7 +74,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if plugin_index is None: invalid.append( - f"Plugin {plugin} does not exist in 3dsMax Plugin List." + f"Plugin {plugin} does not exist" + " in 3dsMax Plugin List." ) continue @@ -105,17 +105,14 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = ( - instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"] - ["family_plugins_mapping"] - ) + required_plugins = cls.family_plugins_mapping instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) - cls.log.debug(f"Checking plug-in validation for instance families: {instance_families}") - for families in required_plugins.keys(): + cls.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + for family in required_plugins.keys(): match_families = {fam.strip() for fam in - families.split(",") if fam.strip()} + family.split(",") if fam.strip()} cls.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) @@ -123,7 +120,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[families]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return From 1207ef3bbbcfe1b5db9bf0b5107d6e67eb5f6c53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 12:00:59 +0100 Subject: [PATCH 135/298] autofix folder path on older instances --- openpype/pipeline/create/context.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 333ab25f54..e4dcedda2c 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2255,11 +2255,11 @@ class CreateContext: if task_name: task_names_by_asset_name[asset_name].add(task_name) - asset_names = [ + asset_names = { asset_name for asset_name in task_names_by_asset_name.keys() if asset_name is not None - ] + } fields = {"name", "data.tasks"} if AYON_SERVER_ENABLED: fields |= {"data.parents"} @@ -2270,10 +2270,12 @@ class CreateContext: )) task_names_by_asset_name = {} + asset_docs_by_name = collections.defaultdict(list) for asset_doc in asset_docs: asset_name = get_asset_name_identifier(asset_doc) tasks = asset_doc.get("data", {}).get("tasks") or {} task_names_by_asset_name[asset_name] = set(tasks.keys()) + asset_docs_by_name[asset_doc["name"]].append(asset_doc) for instance in instances: if not instance.has_valid_asset or not instance.has_valid_task: @@ -2281,6 +2283,11 @@ class CreateContext: if AYON_SERVER_ENABLED: asset_name = instance["folderPath"] + if "/" not in asset_name: + asset_docs = asset_docs_by_name.get(asset_name) + if len(asset_docs) == 1: + asset_name = get_asset_name_identifier(asset_docs[0]) + instance["folderPath"] = asset_name else: asset_name = instance["asset"] From d3804564f26b3ba1a3d304195ec42b408c2b3dff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 12:01:20 +0100 Subject: [PATCH 136/298] do not yield same asset multiple times --- openpype/client/server/entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 9e86dfdd63..becf4abda3 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -232,10 +232,12 @@ def get_assets( else: new_asset_names.add(name) + yielded_ids = set() if folder_paths: for folder in _folders_query( project_name, con, fields, folder_paths=folder_paths, **kwargs ): + yielded_ids.add(folder["id"]) yield convert_v4_folder_to_v3(folder, project_name) if not new_asset_names: @@ -244,7 +246,9 @@ def get_assets( for folder in _folders_query( project_name, con, fields, folder_names=new_asset_names, **kwargs ): - yield convert_v4_folder_to_v3(folder, project_name) + if folder["id"] not in yielded_ids: + yielded_ids.add(folder["id"]) + yield convert_v4_folder_to_v3(folder, project_name) def get_archived_assets( From 2f23b83481de1d7cad5a5139ff0955091e980da0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Nov 2023 13:38:45 +0100 Subject: [PATCH 137/298] traypublisher: failed validator in editorial not necessary --- .../traypublisher/plugins/publish/validate_frame_ranges.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index 09de2d8db2..7a5a3c7fc1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -30,12 +30,17 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return + # Skip the instance if does not have asset entity in database + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + self.log.warning("No asset data found, skipping.") + return + if (self.skip_timelines_check and any(re.search(pattern, instance.data["task"]) for pattern in self.skip_timelines_check)): self.log.info("Skipping for {} task".format(instance.data["task"])) - asset_doc = instance.data["assetEntity"] asset_data = asset_doc["data"] frame_start = asset_data["frameStart"] frame_end = asset_data["frameEnd"] From 8f1648f87a917538b8ab139f0c1af5286aa19988 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:04:56 +0100 Subject: [PATCH 138/298] duplicated session 3 > session 4 --- openpype/pipeline/schema/session-4.0.json | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 openpype/pipeline/schema/session-4.0.json diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json new file mode 100644 index 0000000000..9f785939e4 --- /dev/null +++ b/openpype/pipeline/schema/session-4.0.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-3.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECT", + "AVALON_ASSET" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_APP": { + "description": "Name of host", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + } + } +} From e85504b80c5665d6ddf73fa3b98855db7504b18c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:08:03 +0100 Subject: [PATCH 139/298] use new schema in 'legacy_io' --- openpype/pipeline/legacy_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index 60fa035c22..864102dff9 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -30,7 +30,7 @@ def install(): session = session_data_from_environment(context_keys=True) - session["schema"] = "openpype:session-3.0" + session["schema"] = "openpype:session-4.0" try: schema.validate(session) except schema.ValidationError as e: From 2bc41f53bfa696ff6fe04bd1e840fd85f5fa48ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:17 +0100 Subject: [PATCH 140/298] chnaged title of schema --- openpype/pipeline/schema/session-4.0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 9f785939e4..dc4791994e 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "openpype:session-3.0", + "title": "openpype:session-4.0", "description": "The Avalon environment", "type": "object", From d3fc80f9055b4c6b5d2e4d95ef6a489db465169c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:35 +0100 Subject: [PATCH 141/298] allow forward slash in AVALON_ASSET --- openpype/pipeline/schema/session-4.0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index dc4791994e..9610d8ec64 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -28,7 +28,7 @@ "AVALON_ASSET": { "description": "Name of asset", "type": "string", - "pattern": "^\\w*$", + "pattern": "^[\\/\\w]*$", "example": "Bruce" }, "AVALON_TASK": { From 3cade9a288db81c4eb6b8684641eafb947b5a183 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:50 +0100 Subject: [PATCH 142/298] removed unused keys --- openpype/pipeline/schema/session-4.0.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 9610d8ec64..54a6323e8f 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -14,11 +14,6 @@ ], "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, "AVALON_PROJECT": { "description": "Name of project", "type": "string", @@ -62,20 +57,6 @@ "pattern": "^[0-9]*$", "default": "1000", "example": "1000" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" } } } From e5d3a1aeb0d9d9320eb1b201f73aab3fa383cc65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:07:06 +0100 Subject: [PATCH 143/298] 'AVALON_ASSET' is not required --- openpype/pipeline/schema/session-4.0.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 54a6323e8f..088156af85 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -9,8 +9,7 @@ "additionalProperties": true, "required": [ - "AVALON_PROJECT", - "AVALON_ASSET" + "AVALON_PROJECT" ], "properties": { From 389b568f6b8f356a9be4ab89496b37fa26706337 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:10:06 +0100 Subject: [PATCH 144/298] removed AVALON_PROJECTS --- openpype/pipeline/context_tools.py | 6 +----- openpype/pipeline/mongodb.py | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 38c80c87bb..fe46bd1558 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -88,11 +88,7 @@ def registered_root(): root = _registered_root["_"] if root: return root - - root = legacy_io.Session.get("AVALON_PROJECTS") - if root: - return os.path.normpath(root) - return "" + return {} def install_host(host): diff --git a/openpype/pipeline/mongodb.py b/openpype/pipeline/mongodb.py index 41a44c7373..c948983c3d 100644 --- a/openpype/pipeline/mongodb.py +++ b/openpype/pipeline/mongodb.py @@ -62,8 +62,6 @@ def auto_reconnect(func): SESSION_CONTEXT_KEYS = ( - # Root directory of projects on disk - "AVALON_PROJECTS", # Name of current Project "AVALON_PROJECT", # Name of current Asset From eda0afc26052f1ac1d5a6088aed71db94e8c3010 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Nov 2023 14:27:47 +0100 Subject: [PATCH 145/298] traypublisher: adding exceptions for editorial instances --- .../traypublisher/plugins/create/create_editorial.py | 2 ++ .../plugins/publish/collect_sequence_frame_data.py | 6 ++++++ .../plugins/publish/validate_frame_ranges.py | 8 ++++---- openpype/plugins/publish/collect_resources_path.py | 6 ++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8640500b18..a2746f115f 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -701,6 +701,8 @@ or updating already created. Publishing will create OTIO file. # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, + "isEditorial": True, + # creator_attributes "creator_attributes": creator_attributes } diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index db70d4fe0a..92cedf6b5b 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -27,6 +27,12 @@ class CollectSequenceFrameData( if not self.is_active(instance.data): return + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") + return + frame_data = self.get_frame_data_from_repre_sequence(instance) if not frame_data: diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index 7a5a3c7fc1..4977a13374 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -30,10 +30,10 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return - # Skip the instance if does not have asset entity in database - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - self.log.warning("No asset data found, skipping.") + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") return if (self.skip_timelines_check and diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index cfb4d63c1b..14c13310df 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -68,6 +68,12 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): ] def process(self, instance): + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") + return + anatomy = instance.context.data["anatomy"] template_data = copy.deepcopy(instance.data["anatomyData"]) From ea69e9943ec21cbcb0c5094ed6f063dd393cccb8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 15:56:37 +0200 Subject: [PATCH 146/298] update houdini license validator --- .../validate_houdini_license_category.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index f1c52f22c1..e0e06e37c8 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -3,30 +3,29 @@ import pyblish.api from openpype.pipeline import PublishValidationError -class ValidateHoudiniCommercialLicense(pyblish.api.InstancePlugin): - """Validate the Houdini instance runs a Commercial license. +class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): + """Validate the Houdini instance runs a non Apprentice license. - When extracting USD files from a non-commercial Houdini license, even with - Houdini Indie license, the resulting files will get "scrambled" with - a license protection and get a special .usdnc or .usdlc suffix. + When extracting USD files from an apprentice Houdini license, + the resulting files will get "scrambled" with a license protection + and get a special .usdnc or .usdlc suffix. This currently breaks the Subset/representation pipeline so we disallow - any publish with those licenses. Only the commercial license is valid. + any publish with apprentice license. """ order = pyblish.api.ValidatorOrder families = ["usd"] hosts = ["houdini"] - label = "Houdini Commercial License" + label = "Houdini Apprentice License" def process(self, instance): import hou - license = hou.licenseCategory() - if license != hou.licenseCategoryType.Commercial: + if hou.isApprentice(): raise PublishValidationError( - ("USD Publishing requires a full Commercial " - "license. You are on: {}").format(license), + ("USD Publishing requires a non apprentice " + "license."), title=self.label) From d410899714cffcf75ab4be154b78af6acd99a601 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 22:05:33 +0800 Subject: [PATCH 147/298] add ayon settings support for validate loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 11 ++++---- openpype/settings/ayon_settings.py | 19 ++++++-------- .../max/server/settings/publishers.py | 25 +++++++++++++------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index d348e37abc..a681dc507f 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -63,12 +63,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] + plugins = [plugin for plugin in required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return + # make sure the validation applied for + # plugins with different Max version plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) @@ -110,7 +110,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families.update(instance.data.get("families", [])) cls.log.debug("Checking plug-in validation " f"for instance families: {instance_families}") - for family in required_plugins.keys(): + for family in required_plugins: match_families = {fam.strip() for fam in family.split(",") if fam.strip()} cls.log.debug(f"Plug-in family requirements: {match_families}") @@ -120,8 +120,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] + plugins = [plugin for plugin in family["plugins"]] for plugin in plugins: if not plugin: return diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0cefd047b1..8fd7f990c4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -650,19 +650,14 @@ def _convert_3dsmax_project_settings(ayon_settings, output): ayon_publish["ValidateAttributes"]["attributes"] = attributes if "ValidateLoadedPlugin" in ayon_publish: - family_plugin_mapping = ( - ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] - ) - for item in family_plugin_mapping: - if "product_types" in item: - item["families"] = item.pop("product_types") - new_family_plugin_mapping = { - item["families"]: item["plugins"] - for item in family_plugin_mapping - } - ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( - new_family_plugin_mapping + new_plugin_mapping = {} + loaded_plugin = ( + ayon_publish["ValidateLoadedPlugin"] ) + for item in loaded_plugin["family_plugins_mapping"]: + name = item.pop("name") + new_plugin_mapping[name] = item + loaded_plugin["family_plugins_mapping"] = new_plugin_mapping output["max"] = ayon_max diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index d7169f8b96..cf482d59d8 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,7 +1,7 @@ import json from pydantic import Field, validator -from ayon_server.settings import BaseSettingsModel +from ayon_server.settings import BaseSettingsModel, ensure_unique_names from ayon_server.exceptions import BadRequestException @@ -27,20 +27,31 @@ class ValidateAttributesModel(BaseSettingsModel): return value -class FamilyPluginsMappingModel(BaseSettingsModel): +class FamilyMappingItemModel(BaseSettingsModel): _layout = "compact" - product_types: str = Field(title="Product Types") + name: str = Field("", title="Product type") plugins: list[str] = Field( - default_factory=list,title="Plugins") + default_factory=list, + title="Plugins" + ) class ValidateLoadedPluginModel(BaseSettingsModel): - enabled: bool = Field(title="ValidateLoadedPlugin") + enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") - family_plugins_mapping: list[FamilyPluginsMappingModel] = Field( - default_factory=list, title="Family Plugins Mapping" + family_plugins_mapping: list[FamilyMappingItemModel] = ( + Field( + default_factory=list, + title="Family Plugins Mapping" + ) ) + # This is to validate unique names (like in dict) + @validator("family_plugins_mapping") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") From 57131a8b40bb6633aaeda75528a58bb3967298b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 22:07:36 +0800 Subject: [PATCH 148/298] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- server_addon/max/server/settings/publishers.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index a681dc507f..06486e94a6 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -63,7 +63,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[family]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index cf482d59d8..a752d8cb74 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -39,11 +39,9 @@ class FamilyMappingItemModel(BaseSettingsModel): class ValidateLoadedPluginModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") - family_plugins_mapping: list[FamilyMappingItemModel] = ( - Field( + family_plugins_mapping: list[FamilyMappingItemModel] = Field( default_factory=list, title="Family Plugins Mapping" - ) ) # This is to validate unique names (like in dict) From af9718f753b836542aa55a837123caa655beb369 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Wed, 8 Nov 2023 16:25:17 +0200 Subject: [PATCH 149/298] BigRoy's comment - update doc string Co-authored-by: Roy Nieterau --- .../plugins/publish/validate_houdini_license_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index e0e06e37c8..fd6ad9e3be 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -8,7 +8,7 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): When extracting USD files from an apprentice Houdini license, the resulting files will get "scrambled" with a license protection - and get a special .usdnc or .usdlc suffix. + and get a special .usdnc suffix. This currently breaks the Subset/representation pipeline so we disallow any publish with apprentice license. From 81d054799c5761180baca44dba95a4f00a0f32de Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:26:21 +0100 Subject: [PATCH 150/298] Apply suggestions from code review Co-authored-by: Roy Nieterau --- openpype/pipeline/schema/session-4.0.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 088156af85..0dab48aa46 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -35,7 +35,7 @@ "description": "Name of host", "type": "string", "pattern": "^\\w*$", - "example": "maya2016" + "example": "maya" }, "AVALON_DB": { "description": "Name of database", @@ -47,7 +47,7 @@ "AVALON_LABEL": { "description": "Nice name of Avalon, used in e.g. graphical user interfaces", "type": "string", - "example": "Mindbender", + "example": "MyLabel", "default": "Avalon" }, "AVALON_TIMEOUT": { From 1676f37d3aa98633ff9ac8531285b850d7c5ba28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 16:32:33 +0100 Subject: [PATCH 151/298] change default value of registered root --- openpype/pipeline/context_tools.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index fe46bd1558..33eb335ab9 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -43,7 +43,7 @@ from . import ( _is_installed = False _process_id = None -_registered_root = {"_": ""} +_registered_root = {"_": {}} _registered_host = {"_": None} # Keep modules manager (and it's modules) in memory # - that gives option to register modules' callbacks @@ -85,10 +85,7 @@ def register_root(path): def registered_root(): """Return currently registered root""" - root = _registered_root["_"] - if root: - return root - return {} + return _registered_root["_"] def install_host(host): From 06856644d160155dee3b378ad2326cd44babf5b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 17:07:48 +0100 Subject: [PATCH 152/298] modify 'registered_root' function docstring --- openpype/pipeline/context_tools.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 33eb335ab9..71f41fd234 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -84,7 +84,21 @@ def register_root(path): def registered_root(): - """Return currently registered root""" + """Return registered roots from current project anatomy. + + Consider this does return roots only for current project and current + platforms, only if host was installer using 'install_host'. + + Deprecated: + Please use project 'Anatomy' to get roots. This function is still used + at current core functions of load logic, but that will change + in future and this function will be removed eventually. Using this + function at new places can cause problems in the future. + + Returns: + dict[str, str]: Root paths. + """ + return _registered_root["_"] From b3649b5c5cd1167acb6de9d63ec3c50263be2408 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 13:29:48 +0800 Subject: [PATCH 153/298] improve debug message & use debug instead of info for artist-facing report --- openpype/hosts/maya/api/fbx.py | 4 ++-- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index dbb3578f08..c8f4050bc1 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -156,7 +156,7 @@ class FBXExtractor: # Parse export options options = self.default_options options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Collect the start and end including handles start = instance.data.get("frameStartHandle") or \ @@ -186,7 +186,7 @@ class FBXExtractor: template = "FBXExport{0} {1}" if key == "UpAxis" else \ "FBXExport{0} -v {1}" # noqa cmd = template.format(key, value) - self.log.info(cmd) + self.log.debug(cmd) mel.eval(cmd) # Never show the UI or generate a log diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index c6f8029e7d..c13d349394 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -43,9 +43,13 @@ class ExtractFBXAnimation(publish.Extractor): # FBX does not include the namespace but preserves the node # names as existing in the rig workfile if not out_members: + skeleton_set = [ + i for i in instance + if i.endswith("skeletonAnim_SET") + ] self.log.debug( - "Top group of animated skeleton not found.." - "skipping extraction") + "Top group of animated skeleton not found in " + "{}.\nSkipping fbx animation extraction".format(skeleton_set)) return namespace = get_namespace(out_members[0]) From ed60c361bab707bdd06d6de8fc09e828ff57c639 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 13:30:31 +0800 Subject: [PATCH 154/298] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index c13d349394..e88b8b1e16 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,8 +44,8 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile if not out_members: skeleton_set = [ - i for i in instance - if i.endswith("skeletonAnim_SET") + i for i in instance + if i.endswith("skeletonAnim_SET") ] self.log.debug( "Top group of animated skeleton not found in " From 0403af298e795cdc7a206b88485f40bd6e07072b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 17:21:37 +0800 Subject: [PATCH 155/298] tweaks the codes to use list instead of dict for OP settings --- .../plugins/publish/validate_loaded_plugin.py | 141 +++++++++--------- .../defaults/project_settings/max.json | 2 +- .../schemas/schema_max_publish.json | 10 +- 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 06486e94a6..7450c8f971 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -17,9 +17,6 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings - If families = ["*"], all the required plugins would be validated - If families - """ order = pyblish.api.ValidatorOrder @@ -30,66 +27,77 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, family_plugins_mapping = {} - def get_invalid(self, instance): + @classmethod + def get_invalid(cls, instance): """Plugin entry point.""" - if not self.is_active(instance.data): - self.log.debug("Skipping Validate Loaded Plugin...") - return - - required_plugins = self.family_plugins_mapping - if not required_plugins: + family_plugins_mapping = cls.family_plugins_mapping + if not family_plugins_mapping: return invalid = [] + # Find all plug-in requirements for current instance + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + cls.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + all_required_plugins = set() + + for mapping in family_plugins_mapping: + # Check for matching families + if not mapping: + return + + match_families = {fam for fam in mapping["families"] if fam.strip()} + has_match = "*" in match_families or match_families.intersection( + instance_families) + + if not has_match: + continue + + cls.log.debug(f"Found plug-in family requirements: {match_families}") + required_plugins = [ + # match lowercase and format with os.environ to allow + # plugin names defined by max version, e.g. {3DSMAX_VERSION} + plugin.format(**os.environ).lower() + for plugin in mapping["plugins"] + # ignore empty fields in settings + if plugin.strip() + ] + + all_required_plugins.update(required_plugins) + + if not all_required_plugins: + # Instance has no plug-in requirements + return # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - - # Build instance families lookup - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - self.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") - for family in required_plugins: - # Check for matching families - match_families = {fam.strip() for fam in - family.split(",") if fam.strip()} - self.log.debug(f"Plug-in family requirements: {match_families}") - has_match = "*" in match_families or match_families.intersection( - instance_families) - - if not has_match: + # validate the required plug-ins + for plugin in sorted(all_required_plugins): + plugin_index = available_plugins.get(plugin) + if plugin_index is None: + debug_msg = ( + f"Plugin {plugin} does not exist" + " in 3dsMax Plugin List." + ) + invalid.append((plugin, debug_msg)) continue - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] - for plugin in plugins: - if not plugin: - return - # make sure the validation applied for - # plugins with different Max version - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) - - if plugin_index is None: - invalid.append( - f"Plugin {plugin} does not exist" - " in 3dsMax Plugin List." - ) - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append(f"Plugin {plugin} not loaded.") - + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + debug_msg = f"Plugin {plugin} not loaded." + invalid.append((plugin, debug_msg)) return invalid def process(self, instance): - invalid_plugins = self.get_invalid(instance) - if invalid_plugins: + if not self.is_active(instance.data): + self.log.debug("Skipping Validate Loaded Plugin...") + return + invalid = self.get_invalid(instance) + if invalid: bullet_point_invalid_statement = "\n".join( - "- {}".format(invalid) for invalid in invalid_plugins + "- {}".format(message) for _, message in invalid ) report = ( "Required plugins are not loaded.\n\n" @@ -101,36 +109,23 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, @classmethod def repair(cls, instance): + # get all DLL loaded plugins in Max and their plugin index + invalid = cls.get_invalid(instance) + if not invalid: + return + # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = cls.family_plugins_mapping - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - cls.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") - for family in required_plugins: - match_families = {fam.strip() for fam in - family.split(",") if fam.strip()} - cls.log.debug(f"Plug-in family requirements: {match_families}") - has_match = "*" in match_families or match_families.intersection( - instance_families) - if not has_match: + for invalid_plugin, _ in invalid: + plugin_index = available_plugins.get(invalid_plugin) + + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {invalid_plugin}") continue - plugins = [plugin for plugin in family["plugins"]] - for plugin in plugins: - if not plugin: - return - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) - - if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {plugin}") - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - rt.pluginManager.loadPluginDll(plugin_index) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 57927b48c7..92049cdbe9 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -44,7 +44,7 @@ "ValidateLoadedPlugin": { "enabled": false, "optional": true, - "family_plugins_mapping": {} + "family_plugins_mapping": [] } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index c44c7525da..c6d37ae993 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -66,7 +66,7 @@ "label": "Optional" }, { - "type": "dict-modifiable", + "type": "list", "collapsible": true, "key": "family_plugins_mapping", "label": "Family Plugins Mapping", @@ -74,9 +74,15 @@ "object_type": { "type": "dict", "children": [ + { + "key": "families", + "label": "Famiies", + "type": "list", + "object_type": "text" + }, { "key": "plugins", - "label": "plugins", + "label": "Plugins", "type": "list", "object_type": "text" } From f52af5bcc464621066d1db19a29c0e31bb6f7773 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 17:23:11 +0800 Subject: [PATCH 156/298] add the full stop --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index e88b8b1e16..756158d4f0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -49,7 +49,7 @@ class ExtractFBXAnimation(publish.Extractor): ] self.log.debug( "Top group of animated skeleton not found in " - "{}.\nSkipping fbx animation extraction".format(skeleton_set)) + "{}.\nSkipping fbx animation extraction.".format(skeleton_set)) return namespace = get_namespace(out_members[0]) From 0d6e728552d0a42c6338b07dd761e981111d6b6e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 10:42:38 +0000 Subject: [PATCH 157/298] Added function to get collections Also added a flag in old function to get selection to include collections. --- openpype/hosts/blender/api/lib.py | 54 +++++++++++++++++-- .../plugins/create/create_blendScene.py | 30 +++-------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 9bb560c364..2f33fd25ad 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -266,9 +266,57 @@ def read(node: bpy.types.bpy_struct_meta_idprop): return data -def get_selection() -> List[bpy.types.Object]: - """Return the selected objects from the current scene.""" - return [obj for obj in bpy.context.scene.objects if obj.select_get()] +def get_selected_collections(): + """ + Returns a list of the currently selected collections in the outliner. + + Raises: + RuntimeError: If the outliner cannot be found in the main Blender + window. + + Returns: + list: A list of `bpy.types.Collection` objects that are currently + selected in the outliner. + """ + try: + area = next( + area for area in bpy.context.window.screen.areas + if area.type == 'OUTLINER') + region = next( + region for region in area.regions + if region.type == 'WINDOW') + except StopIteration as e: + raise RuntimeError("Could not find outliner. An outliner space " + "must be in the main Blender window.") from e + + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + screen=bpy.context.window.screen + ): + ids = bpy.context.selected_ids + + return [id for id in ids if isinstance(id, bpy.types.Collection)] + + +def get_selection(include_collections: bool = False) -> List[bpy.types.Object]: + """ + Returns a list of selected objects in the current Blender scene. + + Args: + include_collections (bool, optional): Whether to include selected + collections in the result. Defaults to False. + + Returns: + List[bpy.types.Object]: A list of selected objects. + """ + selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + + if include_collections: + selection.extend(get_selected_collections()) + + return selection @contextlib.contextmanager diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 791e741ca7..bb57a16888 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -40,29 +40,13 @@ class CreateBlendScene(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - try: - area = next( - area for area in bpy.context.window.screen.areas - if area.type == 'OUTLINER') - region = next( - region for region in area.regions - if region.type == 'WINDOW') - except StopIteration as e: - raise RuntimeError("Could not find outliner. An outliner space " - "must be in the main Blender window.") from e + if (self.options or {}).get("useSelection"): + selection = lib.get_selection(include_collections=True) - with bpy.context.temp_override( - window=bpy.context.window, - area=area, - region=region, - screen=bpy.context.window.screen - ): - ids = bpy.context.selected_ids - - for id in ids: - if isinstance(id, bpy.types.Collection): - asset_group.children.link(id) - elif isinstance(id, bpy.types.Object): - asset_group.objects.link(id) + for data in selection: + if isinstance(data, bpy.types.Collection): + asset_group.children.link(data) + elif isinstance(data, bpy.types.Object): + asset_group.objects.link(data) return asset_group From f3de6175bcb1bbbd5105602023be05fac3f717dc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 10:44:37 +0000 Subject: [PATCH 158/298] Hound fixes --- openpype/hosts/blender/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 2f33fd25ad..1f68dd0839 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -287,7 +287,7 @@ def get_selected_collections(): if region.type == 'WINDOW') except StopIteration as e: raise RuntimeError("Could not find outliner. An outliner space " - "must be in the main Blender window.") from e + "must be in the main Blender window.") from e with bpy.context.temp_override( window=bpy.context.window, @@ -311,7 +311,7 @@ def get_selection(include_collections: bool = False) -> List[bpy.types.Object]: Returns: List[bpy.types.Object]: A list of selected objects. """ - selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] if include_collections: selection.extend(get_selected_collections()) From a023183ca2f24a634b3377fde17ea394ec46a38f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 11:08:36 +0000 Subject: [PATCH 159/298] Fix potential problem when removing data --- .../blender/plugins/load/load_blendscene.py | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index b1b2c3ba79..2c955af9e8 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -180,7 +180,7 @@ class BlendSceneLoader(plugin.AssetLoader): avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) - # Restore the old data, but reset memebers, as they don't exist anymore + # Restore the old data, but reset members, as they don't exist anymore # This avoids a crash, because the memory addresses of those members # are not valid anymore old_data["members"] = [] @@ -202,22 +202,20 @@ class BlendSceneLoader(plugin.AssetLoader): group_name = container["objectName"] asset_group = bpy.data.collections.get(group_name) - attrs = [ - attr for attr in dir(bpy.data) - if isinstance( - getattr(bpy.data, attr), - bpy.types.bpy_prop_collection - ) - ] + members = set(asset_group.get(AVALON_PROPERTY).get("members", [])) - members = asset_group.get(AVALON_PROPERTY).get("members", []) + if members: + for attr_name in dir(bpy.data): + attr = getattr(bpy.data, attr_name) + if not isinstance(attr, bpy.types.bpy_prop_collection): + continue - for attr in attrs: - for data in getattr(bpy.data, attr): - if data in members: - # Skip the asset group - if data == asset_group: + # ensure to make a list copy because we + # we remove members as we iterate + for data in list(attr): + if data not in members or data == asset_group: continue - getattr(bpy.data, attr).remove(data) + + attr.remove(data) bpy.data.collections.remove(asset_group) From 8a040ad0a4d88b99eca7423e6bea2ea04e183a29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 12:23:46 +0100 Subject: [PATCH 160/298] pass in variant to 'get_addons_settings' --- openpype/settings/ayon_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 5eb68e3972..67ef109d8b 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1497,7 +1497,8 @@ class _AyonSettingsCache: if cls._use_bundles(): value = ayon_api.get_addons_settings( bundle_name=cls._get_bundle_name(), - project_name=project_name + project_name=project_name, + variant=cls._get_variant() ) else: value = ayon_api.get_addons_settings(project_name) From a4dbc1958011cb0fbdeef68499797606c5fabc51 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 12:56:34 +0000 Subject: [PATCH 161/298] Changed validator to work with other families as well --- .../plugins/publish/validate_instance_empty.py | 17 +++++++++++++---- .../blender/server/settings/publish_plugins.py | 9 ++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 66d8b45e1e..5abfd6dee8 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -8,11 +8,20 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.01 hosts = ["blender"] - families = ["blendScene"] + families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"] label = "Validate Instance is not Empty" optional = False def process(self, instance): - collection = bpy.data.collections[instance.name] - if not (collection.objects or collection.children): - raise RuntimeError(f"Instance {instance.name} is empty.") + self.log.debug(instance) + self.log.debug(instance.data) + if instance.data["family"] == "blendScene": + # blendScene instances are collections + collection = bpy.data.collections[instance.name] + if not (collection.objects or collection.children): + raise RuntimeError(f"Instance {instance.name} is empty.") + else: + # All other instances are objects + asset_group = bpy.data.objects[instance.name] + if not asset_group.children: + raise RuntimeError(f"Instance {instance.name} is empty.") diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index bb68b40cbb..1c4ad0c6fd 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -67,6 +67,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidateFileSavedModel, title="Validate File Saved", ) + ValidateInstanceEmpty: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Instance is not Empty" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs", @@ -84,11 +88,6 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate No Colons In Name" ) - ValidateInstanceEmpty: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Instance is not Empty", - section="BlendScene Validators" - ) ValidateRenderCameraIsSet: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Render Camera Is Set", From cab0a6a3ee661828d694297d2229721dc0a2b969 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 13:03:46 +0000 Subject: [PATCH 162/298] Improved formatting --- openpype/hosts/blender/plugins/publish/extract_blend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index f29dae7f69..17e574c1be 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -28,11 +28,13 @@ class ExtractBlend(publish.Extractor): for data in instance: data_blocks.add(data) # Pack used images in the blend files. - if not(isinstance(data, bpy.types.Object) and data.type == 'MESH'): + if not ( + isinstance(data, bpy.types.Object) and data.type == 'MESH' + ): continue for material_slot in data.material_slots: mat = material_slot.material - if not(mat and mat.use_nodes): + if not (mat and mat.use_nodes): continue tree = mat.node_tree if tree.type != 'SHADER': From 12c8d3d2f8d79dbadca22efe8cc691c998def4a1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:17:29 +0800 Subject: [PATCH 163/298] add supports for ayon settings --- openpype/settings/ayon_settings.py | 9 +++------ server_addon/max/server/settings/publishers.py | 11 ++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8fd7f990c4..eb7e3a2d0f 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -650,14 +650,11 @@ def _convert_3dsmax_project_settings(ayon_settings, output): ayon_publish["ValidateAttributes"]["attributes"] = attributes if "ValidateLoadedPlugin" in ayon_publish: - new_plugin_mapping = {} loaded_plugin = ( - ayon_publish["ValidateLoadedPlugin"] + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] ) - for item in loaded_plugin["family_plugins_mapping"]: - name = item.pop("name") - new_plugin_mapping[name] = item - loaded_plugin["family_plugins_mapping"] = new_plugin_mapping + for item in loaded_plugin: + item["families"] = item.pop("product_types") output["max"] = ayon_max diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a752d8cb74..eeb6478216 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -29,7 +29,10 @@ class ValidateAttributesModel(BaseSettingsModel): class FamilyMappingItemModel(BaseSettingsModel): _layout = "compact" - name: str = Field("", title="Product type") + product_types: list[str] = Field( + default_factory=list, + title="Product Types" + ) plugins: list[str] = Field( default_factory=list, title="Plugins" @@ -44,12 +47,6 @@ class ValidateLoadedPluginModel(BaseSettingsModel): title="Family Plugins Mapping" ) - # This is to validate unique names (like in dict) - @validator("family_plugins_mapping") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") From e0fba84c9293fa6fe7d9ddb5aa5e1783faaa56d7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:17:42 +0800 Subject: [PATCH 164/298] add supports for ayon settings --- server_addon/max/server/settings/publishers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index eeb6478216..4b6429250f 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,7 +1,7 @@ import json from pydantic import Field, validator -from ayon_server.settings import BaseSettingsModel, ensure_unique_names +from ayon_server.settings import BaseSettingsModel from ayon_server.exceptions import BadRequestException From 25be7762f18518ffacc3f0ac511f647025b6fbb0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:19:14 +0800 Subject: [PATCH 165/298] hound --- .../max/plugins/publish/validate_loaded_plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 7450c8f971..ea2fee353d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -39,7 +39,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) cls.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") + f"for instance families: {instance_families}") all_required_plugins = set() for mapping in family_plugins_mapping: @@ -47,14 +47,16 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not mapping: return - match_families = {fam for fam in mapping["families"] if fam.strip()} + match_families = {fam for fam in mapping["families"] + if fam.strip()} has_match = "*" in match_families or match_families.intersection( instance_families) if not has_match: continue - cls.log.debug(f"Found plug-in family requirements: {match_families}") + cls.log.debug( + f"Found plug-in family requirements: {match_families}") required_plugins = [ # match lowercase and format with os.environ to allow # plugin names defined by max version, e.g. {3DSMAX_VERSION} @@ -124,7 +126,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, plugin_index = available_plugins.get(invalid_plugin) if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {invalid_plugin}") + cls.log.warning( + f"Can't enable missing plugin: {invalid_plugin}") continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): From 1e0f44923909de38a76b58e59588d3c417240a7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:32:57 +0100 Subject: [PATCH 166/298] implemented 'get_ayon_server_api_connection' to help create connection --- openpype/client/__init__.py | 3 +++ openpype/client/server/utils.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 7831afd8ad..fe6dc97877 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -1,6 +1,7 @@ from .mongo import ( OpenPypeMongoConnection, ) +from .server.utils import get_ayon_server_api_connection from .entities import ( get_projects, @@ -59,6 +60,8 @@ from .operations import ( __all__ = ( "OpenPypeMongoConnection", + "get_ayon_server_api_connection", + "get_projects", "get_project", "get_whole_project", diff --git a/openpype/client/server/utils.py b/openpype/client/server/utils.py index ed128cfad9..a9dcf539bd 100644 --- a/openpype/client/server/utils.py +++ b/openpype/client/server/utils.py @@ -1,8 +1,33 @@ +import os import uuid +import ayon_api + from openpype.client.operations_base import REMOVED_VALUE +class _GlobalCache: + initialized = False + + +def get_ayon_server_api_connection(): + if _GlobalCache.initialized: + con = ayon_api.get_server_api_connection() + else: + from openpype.lib.local_settings import get_local_site_id + + _GlobalCache.initialized = True + site_id = get_local_site_id() + version = os.getenv("AYON_VERSION") + if ayon_api.is_connection_created(): + con = ayon_api.get_server_api_connection() + con.set_site_id(site_id) + con.set_client_version(version) + else: + con = ayon_api.create_connection(site_id, version) + return con + + def create_entity_id(): return uuid.uuid1().hex From 8e0513fe548b15aed1edcda6f83a38c919758241 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:33:19 +0100 Subject: [PATCH 167/298] use 'get_ayon_server_api_connection' in server functions --- openpype/client/server/entities.py | 27 +++++++++++++------------- openpype/client/server/entity_links.py | 14 ++++++------- openpype/client/server/operations.py | 11 +++++------ 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 16223d3d91..b41727a797 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -1,9 +1,8 @@ import collections -from ayon_api import get_server_api_connection - from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA +from .utils import get_ayon_server_api_connection from .openpype_comp import get_folders_with_tasks from .conversion_utils import ( project_fields_v3_to_v4, @@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None): elif inactive: active = False - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = project_fields_v3_to_v4(fields, con) for project in con.get_projects(active, library, fields=fields): yield convert_v4_project_to_v3(project) @@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None): def get_project(project_name, active=True, inactive=False, fields=None): # Skip if both are disabled - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = project_fields_v3_to_v4(fields, con) return convert_v4_project_to_v3( con.get_project(project_name, fields=fields) @@ -66,7 +65,7 @@ def _get_subsets( fields=None ): # Convert fields and add minimum required fields - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = subset_fields_v3_to_v4(fields, con) if fields is not None: for key in ( @@ -102,7 +101,7 @@ def _get_versions( active=None, fields=None ): - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = version_fields_v3_to_v4(fields, con) @@ -198,7 +197,7 @@ def get_assets( if archived: active = None - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = folder_fields_v3_to_v4(fields, con) kwargs = dict( folder_ids=asset_ids, @@ -236,7 +235,7 @@ def get_archived_assets( def get_asset_ids_with_subsets(project_name, asset_ids=None): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.get_folder_ids_with_products(project_name, asset_ids) @@ -282,7 +281,7 @@ def get_subsets( def get_subset_families(project_name, subset_ids=None): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.get_product_type_names(project_name, subset_ids) @@ -430,7 +429,7 @@ def get_output_link_versions(project_name, version_id, fields=None): if not version_id: return [] - con = get_server_api_connection() + con = get_ayon_server_api_connection() version_links = con.get_version_links( project_name, version_id, link_direction="out") @@ -446,7 +445,7 @@ def get_output_link_versions(project_name, version_id, fields=None): def version_is_latest(project_name, version_id): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.version_is_latest(project_name, version_id) @@ -501,7 +500,7 @@ def get_representations( else: active = None - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = representation_fields_v3_to_v4(fields, con) if fields and active is not None: fields.add("active") @@ -535,7 +534,7 @@ def get_representations_parents(project_name, representations): repre["_id"] for repre in representations } - con = get_server_api_connection() + con = get_ayon_server_api_connection() parents_by_repre_id = con.get_representations_parents(project_name, repre_ids) folder_ids = set() @@ -677,7 +676,7 @@ def get_workfile_info( if not asset_id or not task_name or not filename: return None - con = get_server_api_connection() + con = get_ayon_server_api_connection() task = con.get_task_by_name( project_name, asset_id, task_name, fields=["id", "name", "folderId"] ) diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py index d8395aabe7..368dcdcb9d 100644 --- a/openpype/client/server/entity_links.py +++ b/openpype/client/server/entity_links.py @@ -1,6 +1,4 @@ -import ayon_api -from ayon_api import get_folder_links, get_versions_links - +from .utils import get_ayon_server_api_connection from .entities import get_assets, get_representation_by_id @@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): if not asset_id: asset_id = asset_doc["_id"] - links = get_folder_links(project_name, asset_id, link_direction="in") + con = get_ayon_server_api_connection() + links = con.get_folder_links(project_name, asset_id, link_direction="in") return [ link["entityId"] for link in links @@ -115,6 +114,7 @@ def get_linked_representation_id( if link_type: link_types = [link_type] + con = get_ayon_server_api_connection() # Store already found version ids to avoid recursion, and also to store # output -> Don't forget to remove 'version_id' at the end!!! linked_version_ids = {version_id} @@ -124,7 +124,7 @@ def get_linked_representation_id( if not versions_to_check: break - links = get_versions_links( + links = con.get_versions_links( project_name, versions_to_check, link_types=link_types, @@ -145,8 +145,8 @@ def get_linked_representation_id( linked_version_ids.remove(version_id) if not linked_version_ids: return [] - - representations = ayon_api.get_representations( + con = get_ayon_server_api_connection() + representations = con.get_representations( project_name, version_ids=linked_version_ids, fields=["id"]) diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index 5b38405c34..eddc1eaf60 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -5,7 +5,6 @@ import uuid import datetime from bson.objectid import ObjectId -from ayon_api import get_server_api_connection from openpype.client.operations_base import ( REMOVED_VALUE, @@ -41,7 +40,7 @@ from .conversion_utils import ( convert_update_representation_to_v4, convert_update_workfile_info_to_v4, ) -from .utils import create_entity_id +from .utils import create_entity_id, get_ayon_server_api_connection def _create_or_convert_to_id(entity_id=None): @@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession): def __init__(self, con=None, *args, **kwargs): super(OperationsSession, self).__init__(*args, **kwargs) if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() self._con = con self._project_cache = {} self._nested_operations = collections.defaultdict(list) @@ -858,7 +857,7 @@ def create_project( """ if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.create_project( project_name, @@ -870,12 +869,12 @@ def create_project( def delete_project(project_name, con=None): if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.delete_project(project_name) def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None): if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.create_thumbnail(project_name, src_filepath, thumbnail_id) From 14ddd5cb51e8785f27b5be5406b358792bbe6328 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:36:42 +0100 Subject: [PATCH 168/298] use 'get_ayon_server_api_connection' in core functions --- openpype/lib/local_settings.py | 6 +++--- openpype/modules/base.py | 10 +++++++--- openpype/pipeline/anatomy.py | 6 +++--- openpype/pipeline/context_tools.py | 6 ++++++ openpype/pipeline/thumbnail.py | 6 ++---- openpype/settings/ayon_settings.py | 28 +++++++++++++++++++--------- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 9b780fd88a..ea42d2f0b5 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -36,6 +36,7 @@ from openpype.settings import ( ) from openpype.client.mongo import validate_mongo_connection +from openpype.client import get_ayon_server_api_connection _PLACEHOLDER = object() @@ -613,9 +614,8 @@ def get_openpype_username(): """ if AYON_SERVER_ENABLED: - import ayon_api - - return ayon_api.get_user()["name"] + con = get_ayon_server_api_connection() + return con.get_user()["name"] username = os.environ.get("OPENPYPE_USERNAME") if not username: diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 457e29905d..4636906cec 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -16,9 +16,9 @@ from abc import ABCMeta, abstractmethod import six import appdirs -import ayon_api from openpype import AYON_SERVER_ENABLED +from openpype.client import get_ayon_server_api_connection from openpype.settings import ( get_system_settings, SYSTEM_SETTINGS_KEY, @@ -319,8 +319,11 @@ def load_modules(force=False): def _get_ayon_bundle_data(): + con = get_ayon_server_api_connection() + bundles = con.get_bundles()["bundles"] + bundle_name = os.getenv("AYON_BUNDLE_NAME") - bundles = ayon_api.get_bundles()["bundles"] + return next( ( bundle @@ -345,7 +348,8 @@ def _get_ayon_addons_information(bundle_info): output = [] bundle_addons = bundle_info["addons"] - addons = ayon_api.get_addons_info()["addons"] + con = get_ayon_server_api_connection() + addons = con.get_addons_info()["addons"] for addon in addons: name = addon["name"] versions = addon.get("versions") diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 029b5cc1ff..0e5ab1d42e 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -5,7 +5,6 @@ import platform import collections import numbers -import ayon_api import six import time @@ -16,7 +15,7 @@ from openpype.settings.lib import ( from openpype.settings.constants import ( DEFAULT_PROJECT_KEY ) -from openpype.client import get_project +from openpype.client import get_project, get_ayon_server_api_connection from openpype.lib import Logger, get_local_site_id from openpype.lib.path_templates import ( TemplateUnsolved, @@ -479,7 +478,8 @@ class Anatomy(BaseAnatomy): if AYON_SERVER_ENABLED: if not project_name: return - return ayon_api.get_project_roots_for_site( + con = get_ayon_server_api_connection() + return con.get_project_roots_for_site( project_name, get_local_site_id() ) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 5afdb30f7b..034bbc0070 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -11,12 +11,14 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype import AYON_SERVER_ENABLED from openpype.host import HostBase from openpype.client import ( get_project, get_asset_by_id, get_asset_by_name, version_is_latest, + get_ayon_server_api_connection, ) from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager @@ -105,6 +107,10 @@ def install_host(host): _is_installed = True + # Make sure global AYON connection has set site id and version + if AYON_SERVER_ENABLED: + get_ayon_server_api_connection() + legacy_io.install() modules_manager = _get_modules_manager() diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 63c55d0c19..14fb8b06fc 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -4,7 +4,7 @@ import logging from openpype import AYON_SERVER_ENABLED from openpype.lib import Logger -from openpype.client import get_project +from openpype.client import get_project, get_ayon_server_api_connection from . import legacy_io from .anatomy import Anatomy from .plugin_discover import ( @@ -153,8 +153,6 @@ class ServerThumbnailResolver(ThumbnailResolver): if not entity_type or not entity_id: return None - import ayon_api - project_name = self.dbcon.active_project() thumbnail_id = thumbnail_entity["_id"] @@ -169,7 +167,7 @@ class ServerThumbnailResolver(ThumbnailResolver): # NOTE Use 'get_server_api_connection' because public function # 'get_thumbnail_by_id' does not return output of 'ServerAPI' # method. - con = ayon_api.get_server_api_connection() + con = get_ayon_server_api_connection() if hasattr(con, "get_thumbnail_by_id"): result = con.get_thumbnail_by_id(thumbnail_id) if result.is_valid: diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 67ef109d8b..745cadfc6e 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -20,7 +20,8 @@ import copy import time import six -import ayon_api + +from openpype.client import get_ayon_server_api_connection def _convert_color(color_value): @@ -1445,7 +1446,8 @@ class _AyonSettingsCache: @classmethod def _use_bundles(cls): if _AyonSettingsCache.use_bundles is None: - major, minor, _, _, _ = ayon_api.get_server_version_tuple() + con = get_ayon_server_api_connection() + major, minor, _, _, _ = con.get_server_version_tuple() use_bundles = True if (major, minor) < (0, 3): use_bundles = False @@ -1462,6 +1464,8 @@ class _AyonSettingsCache: variant = cls._get_dev_mode_settings_variant() elif is_staging_enabled(): variant = "staging" + + # Cache variant _AyonSettingsCache.variant = variant return _AyonSettingsCache.variant @@ -1477,8 +1481,9 @@ class _AyonSettingsCache: str: Name of settings variant. """ - bundles = ayon_api.get_bundles() - user = ayon_api.get_user() + con = get_ayon_server_api_connection() + bundles = con.get_bundles() + user = con.get_user() username = user["name"] for bundle in bundles["bundles"]: if ( @@ -1494,21 +1499,23 @@ class _AyonSettingsCache: def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] if cache_item.is_outdated: + con = get_ayon_server_api_connection() if cls._use_bundles(): - value = ayon_api.get_addons_settings( + value = con.get_addons_settings( bundle_name=cls._get_bundle_name(), project_name=project_name, variant=cls._get_variant() ) else: - value = ayon_api.get_addons_settings(project_name) + value = con.get_addons_settings(project_name) cache_item.update_value(value) return cache_item.get_value() @classmethod def _get_addon_versions_from_bundle(cls): + con = get_ayon_server_api_connection() expected_bundle = cls._get_bundle_name() - bundles = ayon_api.get_bundles()["bundles"] + bundles = con.get_bundles()["bundles"] bundle = next( ( bundle @@ -1528,8 +1535,11 @@ class _AyonSettingsCache: if cls._use_bundles(): addons = cls._get_addon_versions_from_bundle() else: - settings_data = ayon_api.get_addons_settings( - only_values=False, variant=cls._get_variant()) + con = get_ayon_server_api_connection() + settings_data = con.get_addons_settings( + only_values=False, + variant=cls._get_variant() + ) addons = settings_data["versions"] cache_item.update_value(addons) From e23a2b5e09dfb13c5ed1587a450bd5de89b7ef35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:36:55 +0100 Subject: [PATCH 169/298] set default variant in ayon_settings --- openpype/settings/ayon_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 745cadfc6e..f0b4528802 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1467,6 +1467,10 @@ class _AyonSettingsCache: # Cache variant _AyonSettingsCache.variant = variant + + # Set the variant to global ayon api connection + con = get_ayon_server_api_connection() + con.set_default_settings_variant(variant) return _AyonSettingsCache.variant @classmethod From 0b8dde268708594e23d1f305a7bfbd3da99a55d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 15:47:14 +0100 Subject: [PATCH 170/298] resolve: creating clips with folder path also converting dict() to {} --- openpype/hosts/resolve/api/plugin.py | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 49c3b484d2..197f288150 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -19,7 +19,7 @@ from .menu import load_stylesheet class CreatorWidget(QtWidgets.QDialog): # output items - items = dict() + items = {} def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) @@ -101,7 +101,7 @@ class CreatorWidget(QtWidgets.QDialog): self.close() def value(self, data, new_data=None): - new_data = new_data or dict() + new_data = new_data or {} for k, v in data.items(): new_data[k] = { "target": None, @@ -290,7 +290,7 @@ class Spacer(QtWidgets.QWidget): class ClipLoader: active_bin = None - data = dict() + data = {} def __init__(self, loader_obj, context, **options): """ Initialize object @@ -588,8 +588,8 @@ class PublishClip: Returns: hiero.core.TrackItem: hiero track item object with openpype tag """ - vertical_clip_match = dict() - tag_data = dict() + vertical_clip_match = {} + tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -665,15 +665,23 @@ class PublishClip: new_name = self.tag_data.pop("newClipName") if self.rename: - self.tag_data["asset"] = new_name + self.tag_data["asset_name"] = new_name else: - self.tag_data["asset"] = self.ti_name + self.tag_data["asset_name"] = self.ti_name + # AYON unique identifier + folder_path = "/{}/{}".format( + self.tag_data["hierarchy"], + self.tag_data["asset_name"] + ) + self.tag_data["folder_path"] = folder_path + + # create new name for track item if not lib.pype_marker_workflow: # create compound clip workflow lib.create_compound_clip( self.timeline_item_data, - self.tag_data["asset"], + self.tag_data["asset_name"], self.mp_folder ) @@ -765,7 +773,7 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formatting_data = dict() + hierarchy_formatting_data = {} _data = self.timeline_item_default_data.copy() if self.ui_inputs: # adding tag metadata from ui @@ -854,8 +862,7 @@ class PublishClip: "parents": self.parents, "hierarchyData": hierarchy_formatting_data, "subset": self.subset, - "family": self.subset_family, - "families": ["clip"] + "family": self.subset_family } def _convert_to_entity(self, key): From d07898a3286b139c0d48bc14fb16b1417adcd8e5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 15:47:50 +0100 Subject: [PATCH 171/298] resolve: collect instances with folder path --- .../plugins/publish/precollect_instances.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 8ec169ad65..58c1c85276 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -9,6 +9,7 @@ from openpype.hosts.resolve.api.lib import ( get_publish_attribute, get_otio_clip_instance_data, ) +from openpype import AYON_SERVER_ENABLED class PrecollectInstances(pyblish.api.ContextPlugin): @@ -29,7 +30,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for timeline_item_data in selected_timeline_items: - data = dict() + data = {} timeline_item = timeline_item_data["clip"]["item"] # get pype tag data @@ -60,24 +61,25 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["asset"] + if AYON_SERVER_ENABLED: + asset = tag_data["folder_path"] + else: + asset = tag_data["asset_name"] + subset = tag_data["subset"] - # insert family into families - family = tag_data["family"] - families = [str(f) for f in tag_data["families"]] - families.insert(0, str(family)) - data.update({ - "name": "{} {} {}".format(asset, subset, families), + "name": "{}_{}".format(asset, subset), + "label": "{} {}".format(asset, subset), "asset": asset, "item": timeline_item, - "families": families, "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, "handleEnd": handle_end, - "newAssetPublishing": True + "newAssetPublishing": True, + "families": ["clip"], + "isEditorial": True }) # otio clip data @@ -135,7 +137,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): family = "shot" data.update({ - "name": "{} {} {}".format(asset, subset, family), + "name": "{}_{}".format(asset, subset), + "label": "{} {}".format(asset, subset), "subset": subset, "asset": asset, "family": family, From 7cd98fe9033c17b4897208ab7c54bda5e7e53c45 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 9 Nov 2023 15:52:36 +0100 Subject: [PATCH 172/298] Remove deprecated code, use avalon instances in basecreator --- openpype/hosts/blender/api/plugin.py | 25 +++++----- .../blender/plugins/create/create_action.py | 25 ---------- .../plugins/create/create_animation.py | 37 -------------- .../blender/plugins/create/create_camera.py | 49 ------------------- .../blender/plugins/create/create_layout.py | 37 -------------- .../blender/plugins/create/create_model.py | 37 -------------- .../plugins/create/create_pointcache.py | 36 -------------- .../blender/plugins/create/create_review.py | 33 ------------- .../blender/plugins/create/create_rig.py | 37 -------------- 9 files changed, 11 insertions(+), 305 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 3ddc375670..629cb4dac9 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -144,18 +144,6 @@ class BaseCreator(Creator): """Base class for Blender Creator plug-ins.""" defaults = ['Main'] - # Deprecated? - def process(self): - collection = bpy.data.collections.new(name=self.data["subset"]) - bpy.context.scene.collection.children.link(collection) - imprint(collection, self.data) - - if (self.options or {}).get("useSelection"): - for obj in get_selection(): - collection.objects.link(obj) - - return collection - @staticmethod def cache_subsets(shared_data): """Cache instances for Creators shared data. @@ -233,8 +221,17 @@ class BaseCreator(Creator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ - collection = bpy.data.collections.new(name=subset_name) - bpy.context.scene.collection.children.link(collection) + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance collection + collection = bpy.data.collections.new( + name=asset_name(instance_data["asset"], subset_name) + ) + instances.children.link(collection) collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 7d00aa1dcb..9267fc0765 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -59,28 +59,3 @@ class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): collection.objects.link(empty_obj) return collection - - # Deprecated - def process(self): - - asset = self.data["asset"] - subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) - self.data['task'] = get_current_task_name() - lib.imprint(collection, self.data) - - if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - if (obj.animation_data is not None - and obj.animation_data.action is not None): - - empty_obj = bpy.data.objects.new(name=name, - object_data=None) - empty_obj.animation_data_create() - empty_obj.animation_data.action = obj.animation_data.action - empty_obj.animation_data.action.name = name - collection.objects.link(empty_obj) - - return collection diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 6cfd054e74..89567061b6 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -75,40 +75,3 @@ class CreateAnimation(plugin.BaseCreator): asset_group.objects.link(obj) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - # name = self.name - # if not name: - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - # asset_group = bpy.data.objects.new(name=name, object_data=None) - # asset_group.empty_display_type = 'SINGLE_ARROW' - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 5d9682e575..125514ae01 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -84,52 +84,3 @@ class CreateCamera(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - print(f"self.data: {self.data}") - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - else: - plugin.deselect_all() - camera = bpy.data.cameras.new(subset) - camera_obj = bpy.data.objects.new(subset, camera) - - instances.objects.link(camera_obj) - - camera_obj.select_set(True) - asset_group.select_set(True) - bpy.context.view_layer.objects.active = asset_group - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ed47b0632f..a6c7053cb2 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -73,40 +73,3 @@ class CreateLayout(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 949fae0f76..8beb8025c4 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -73,40 +73,3 @@ class CreateModel(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 2ad12caa9c..aa8b297d16 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -54,39 +54,3 @@ class CreatePointcache(plugin.BaseCreator): objects.extend(obj.children) return collection - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) - ops.execute_in_main_thread(mti) - - def _process(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index e8b893b4c0..13fa3b621f 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -71,36 +71,3 @@ class CreateReview(plugin.BaseCreator): asset_group.objects.link(obj) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 6223e64174..6682162f4b 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -73,40 +73,3 @@ class CreateRig(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group From 42c61fa348ca6036cef594f0f6fe6932b3c41b5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 16:22:17 +0100 Subject: [PATCH 173/298] do not check for "/" if asset name is empty --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e4dcedda2c..f32477dfb7 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2283,7 +2283,7 @@ class CreateContext: if AYON_SERVER_ENABLED: asset_name = instance["folderPath"] - if "/" not in asset_name: + if asset_name and "/" not in asset_name: asset_docs = asset_docs_by_name.get(asset_name) if len(asset_docs) == 1: asset_name = get_asset_name_identifier(asset_docs[0]) From 573da36d4b12d58ab84110bbc46105be3f4e9e90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 16:32:20 +0100 Subject: [PATCH 174/298] workfile instance with support of folder path --- .../plugins/publish/extract_workfile.py | 1 + .../plugins/publish/precollect_workfile.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/extract_workfile.py b/openpype/hosts/resolve/plugins/publish/extract_workfile.py index 535f879b58..db63487405 100644 --- a/openpype/hosts/resolve/plugins/publish/extract_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/extract_workfile.py @@ -26,6 +26,7 @@ class ExtractWorkfile(publish.Extractor): resolve_workfile_ext = ".drp" drp_file_name = name + resolve_workfile_ext + drp_file_path = os.path.normpath( os.path.join(staging_dir, drp_file_name)) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 39c28e29f5..ccc5fd86ff 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -3,6 +3,7 @@ from pprint import pformat from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name + from openpype.hosts.resolve import api as rapi from openpype.hosts.resolve.otio import davinci_export @@ -14,14 +15,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): - current_asset = get_current_asset_name() - if AYON_SERVER_ENABLED: - # AYON compatibility split name and use last piece - asset_name = current_asset.split("/")[-1] - else: - asset_name = current_asset + current_asset_name = asset_name = get_current_asset_name() - subset = "workfile" + if AYON_SERVER_ENABLED: + asset_name = current_asset_name.split("/")[-1] + + subset = "workfileMain" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") video_tracks = rapi.get_video_track_names() @@ -31,8 +30,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): instance_data = { "name": "{}_{}".format(asset_name, subset), - "asset": current_asset, - "subset": "{}{}".format(asset_name, subset.capitalize()), + "label": "{} {}".format(current_asset_name, subset), + "asset": current_asset_name, + "subset": subset, "item": project, "family": "workfile", "families": [] From a0da4cd17f2a0809eb2d4904a48ec8a5f5c04ab1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 15:35:58 +0000 Subject: [PATCH 175/298] Changed how we get instance group in the validator --- .../blender/plugins/publish/collect_instances.py | 4 ++-- .../plugins/publish/validate_instance_empty.py | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index ad2ce54147..2d56e5fd7b 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -1,4 +1,3 @@ -import json from typing import Generator import bpy @@ -50,6 +49,7 @@ class CollectInstances(pyblish.api.ContextPlugin): for group in asset_groups: instance = self.create_instance(context, group) + instance.data["instance_group"] = group members = [] if isinstance(group, bpy.types.Collection): members = list(group.objects) @@ -65,6 +65,6 @@ class CollectInstances(pyblish.api.ContextPlugin): members.append(group) instance[:] = members - self.log.debug(json.dumps(instance.data, indent=4)) + self.log.debug(instance.data) for obj in instance: self.log.debug(obj) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 5abfd6dee8..3ebc6515d3 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -13,15 +13,11 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): optional = False def process(self, instance): - self.log.debug(instance) - self.log.debug(instance.data) - if instance.data["family"] == "blendScene": - # blendScene instances are collections - collection = bpy.data.collections[instance.name] - if not (collection.objects or collection.children): + asset_group = instance.data["instance_group"] + + if isinstance(asset_group, bpy.types.Collection): + if not (asset_group.objects or asset_group.children): raise RuntimeError(f"Instance {instance.name} is empty.") - else: - # All other instances are objects - asset_group = bpy.data.objects[instance.name] + elif isinstance(asset_group, bpy.types.Object): if not asset_group.children: raise RuntimeError(f"Instance {instance.name} is empty.") From ae4bb7dacfba6d0326aea7cc904128e8440da4d6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 10 Nov 2023 16:38:21 +0800 Subject: [PATCH 176/298] make sure if the aspect ratio is 16:9 and follows the render aspect ratio, the render sequences wont be cropped --- openpype/hosts/max/api/preview_animation.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bbf05f4ca9..6c7b8eaa80 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -203,20 +203,24 @@ def _render_preview_animation_max_pre_2024( # aspect ratio viewportRatio = dib_width / dib_height renderRatio = float(res_width / res_height) - if viewportRatio <= renderRatio: + if viewportRatio < renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) tempImage_bmp = rt.bitmap(dib_width, heightCrop) src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop) - else: + rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) + rt.copy(tempImage_bmp, preview_res) + rt.close(tempImage_bmp) + elif viewportRatio > renderRatio: widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height) - rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) - # copy the bitmap and close it - rt.copy(tempImage_bmp, preview_res) - rt.close(tempImage_bmp) + rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) + rt.copy(tempImage_bmp, preview_res) + rt.close(tempImage_bmp) + else: + rt.copy(dib, preview_res) rt.save(preview_res) rt.close(preview_res) rt.close(dib) From 958e3019faee4ec25e50674cfeb3988827de52d3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 10 Nov 2023 18:47:14 +0800 Subject: [PATCH 177/298] dont make the layout compact --- server_addon/max/server/settings/publishers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index 4b6429250f..b48f14a064 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -28,7 +28,6 @@ class ValidateAttributesModel(BaseSettingsModel): class FamilyMappingItemModel(BaseSettingsModel): - _layout = "compact" product_types: list[str] = Field( default_factory=list, title="Product Types" From 01be65d283a531ad8eae43beca9d3637bf7221f2 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 10 Nov 2023 16:18:28 +0100 Subject: [PATCH 178/298] Reduce redundancy in create action, animation, pointcache, render and review --- openpype/hosts/blender/api/plugin.py | 16 ++++--- .../blender/plugins/create/create_action.py | 34 +++----------- .../plugins/create/create_animation.py | 45 +++---------------- .../plugins/create/create_pointcache.py | 34 ++------------ .../blender/plugins/create/create_render.py | 43 +++--------------- .../blender/plugins/create/create_review.py | 42 +++-------------- 6 files changed, 41 insertions(+), 173 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 629cb4dac9..aef891bd83 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -237,6 +237,7 @@ class BaseCreator(Creator): "name": collection.name, } + # Set instance data instance_data.update( { "id": "pyblish.avalon.instance", @@ -248,16 +249,15 @@ class BaseCreator(Creator): } ) - instance = CreatedInstance( - self.family, subset_name, instance_data, self + self._add_instance_to_context( + CreatedInstance( + self.family, subset_name, instance_data, self + ) ) - self._add_instance_to_context(instance) imprint(collection, instance_data) - if pre_create_data.get("useSelection"): - for obj in get_selection(): - collection.objects.link(obj) + return collection def collect_instances(self): """Override abstract method from BaseCreator. @@ -267,7 +267,9 @@ class BaseCreator(Creator): self.cache_subsets(self.collection_shared_data) # Get cached subsets - cached_subsets = self.collection_shared_data.get('blender_cached_subsets') + cached_subsets = self.collection_shared_data.get( + "blender_cached_subsets" + ) if not cached_subsets: return diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 9267fc0765..95bd42682c 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -2,13 +2,11 @@ import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance -import openpype.hosts.blender.api.plugin +from openpype.hosts.blender.api.plugin import BaseCreator, asset_name from openpype.hosts.blender.api import lib -from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): +class CreateAction(BaseCreator): """Action output for character rigs""" identifier = "io.openpype.creators.blender.action" @@ -20,31 +18,13 @@ class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) + # Run parent create method + collection = super().create( + subset_name, instance_data, pre_create_data ) - name = openpype.hosts.blender.api.plugin.asset_name( - instance_data["asset"], subset_name - ) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) - - collection[AVALON_PROPERTY] = instance_node = { - "name": collection.name, - } - - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) - lib.imprint(collection, instance_data) + # Get instance name + name = asset_name(instance_data["asset"], subset_name) if pre_create_data.get("useSelection"): for obj in lib.get_selection(): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 89567061b6..3c70ae1bd0 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -1,13 +1,8 @@ """Create an animation asset.""" -import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance +from openpype.pipeline import CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) class CreateAnimation(plugin.BaseCreator): @@ -35,43 +30,17 @@ class CreateAnimation(plugin.BaseCreator): def _process( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - # name = self.name - # if not name: - name = plugin.asset_name(instance_data["asset"], subset_name) - # asset_group = bpy.data.objects.new(name=name, object_data=None) - # asset_group.empty_display_type = 'SINGLE_ARROW' - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } + # Run parent create method + collection = super().create( + subset_name, instance_data, pre_create_data ) - lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): selected = lib.get_selection() for obj in selected: - asset_group.objects.link(obj) + collection.objects.link(obj) elif pre_create_data.get("asset_group"): obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) + collection.objects.link(obj) - return asset_group + return collection diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index aa8b297d16..d823193249 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -1,11 +1,6 @@ """Create a pointcache asset.""" -import bpy - -from openpype.pipeline import get_current_task_name, CreatedInstance -from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY +from openpype.hosts.blender.api import plugin, lib class CreatePointcache(plugin.BaseCreator): @@ -20,32 +15,11 @@ class CreatePointcache(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) + # Run parent create method + collection = super().create( + subset_name, instance_data, pre_create_data ) - name = plugin.asset_name( - instance_data["asset"], subset_name - ) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) - - collection[AVALON_PROPERTY] = instance_node = { - "name": collection.name, - } - - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) - lib.imprint(collection, instance_data) - if pre_create_data.get("useSelection"): objects = lib.get_selection() for obj in objects: diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index e036ae7df3..0e5a284caf 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,13 +1,8 @@ """Create render.""" import bpy -from openpype.pipeline import get_current_task_name -from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.render_lib import prepare_rendering -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) class CreateRenderlayer(plugin.BaseCreator): @@ -22,40 +17,16 @@ class CreateRenderlayer(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data.get("asset"), subset_name) - asset_group = bpy.data.collections.new(name=name) - try: - instances.children.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name - } - - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } + # Run parent create method + collection = super().create( + subset_name, instance_data, pre_create_data ) - lib.imprint(asset_group, instance_data) - - prepare_rendering(asset_group) + prepare_rendering(collection) except Exception: # Remove the instance if there was an error - bpy.data.collections.remove(asset_group) + bpy.data.collections.remove(collection) raise # TODO: this is undesiderable, but it's the only way to be sure that @@ -69,4 +40,4 @@ class CreateRenderlayer(plugin.BaseCreator): # now it is to force the file to be saved. bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) - return asset_group + return collection diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 13fa3b621f..e35e405ee1 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -1,13 +1,7 @@ """Create review.""" -import bpy - -from openpype.pipeline import get_current_task_name, CreatedInstance +from openpype.pipeline import CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) class CreateReview(plugin.BaseCreator): @@ -35,39 +29,17 @@ class CreateReview(plugin.BaseCreator): def _process( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data["asset"], subset_name) - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } + # Run parent create method + collection = super().create( + subset_name, instance_data, pre_create_data ) - lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): selected = lib.get_selection() for obj in selected: - asset_group.objects.link(obj) + collection.objects.link(obj) elif pre_create_data.get("asset_group"): obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) + collection.objects.link(obj) - return asset_group + return collection From 0feca7015e977cec82812ac27c7e90bc45200373 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Nov 2023 16:40:40 +0100 Subject: [PATCH 179/298] skip 'get_site_icons' if site sync addon is disabled --- openpype/tools/ayon_sceneinventory/models/site_sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py index b8c9443230..1297137cb0 100644 --- a/openpype/tools/ayon_sceneinventory/models/site_sync.py +++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py @@ -40,9 +40,9 @@ class SiteSyncModel: dict[str, str]: Path by provider name. """ - site_sync = self._get_sync_server_module() - if site_sync is None: + if not self.is_sync_server_enabled(): return {} + site_sync = self._get_sync_server_module() return site_sync.get_site_icons() def get_sites_information(self): From c614522b4c3df1f80b1151e7e7a59dee4108c05f Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 10 Nov 2023 17:16:55 +0100 Subject: [PATCH 180/298] Fix animation and review duplication in publisher UI --- .../blender/plugins/create/create_action.py | 2 +- .../plugins/create/create_animation.py | 20 +++---------------- .../plugins/create/create_blendScene.py | 2 +- .../blender/plugins/create/create_camera.py | 4 ++-- .../blender/plugins/create/create_layout.py | 4 ++-- .../blender/plugins/create/create_model.py | 2 +- .../plugins/create/create_pointcache.py | 2 +- .../blender/plugins/create/create_render.py | 2 +- .../blender/plugins/create/create_review.py | 19 +++--------------- .../blender/plugins/create/create_rig.py | 4 ++-- 10 files changed, 17 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 95bd42682c..ac425dff74 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import lib class CreateAction(BaseCreator): - """Action output for character rigs""" + """Action output for character rigs.""" identifier = "io.openpype.creators.blender.action" name = "actionMain" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 3c70ae1bd0..f7cb9f88aa 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -1,12 +1,10 @@ """Create an animation asset.""" - -from openpype.pipeline import CreatedInstance -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib class CreateAnimation(plugin.BaseCreator): - """Animation output for character rigs""" + """Animation output for character rigs.""" identifier = "io.openpype.creators.blender.animation" name = "animationMain" @@ -17,19 +15,7 @@ class CreateAnimation(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): + """Run the creator on Blender main thread.""" # Run parent create method collection = super().create( subset_name, instance_data, pre_create_data diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 23ff991654..df3a70f199 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateBlendScene(plugin.Creator): - """Generic group of assets""" + """Generic group of assets.""" name = "blendScene" label = "Blender Scene" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 125514ae01..a42a9d6ef0 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateCamera(plugin.BaseCreator): - """Polygonal static geometry""" + """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.camera" name = "cameraMain" @@ -22,7 +22,7 @@ class CreateCamera(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" self._add_instance_to_context( CreatedInstance(self.family, subset_name, instance_data, self) ) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index a6c7053cb2..ef1822dedf 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateLayout(plugin.BaseCreator): - """Layout output for character rigs""" + """Layout output for character rigs.""" identifier = "io.openpype.creators.blender.layout" name = "layoutMain" @@ -22,7 +22,7 @@ class CreateLayout(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" self._add_instance_to_context( CreatedInstance(self.family, subset_name, instance_data, self) ) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 8beb8025c4..e4937f989e 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateModel(plugin.BaseCreator): - """Polygonal static geometry""" + """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.model" name = "modelMain" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index d823193249..d05b3adb04 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -4,7 +4,7 @@ from openpype.hosts.blender.api import plugin, lib class CreatePointcache(plugin.BaseCreator): - """Polygonal static geometry""" + """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.pointcache" name = "pointcacheMain" diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 0e5a284caf..bffa5696df 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -6,7 +6,7 @@ from openpype.hosts.blender.api.render_lib import prepare_rendering class CreateRenderlayer(plugin.BaseCreator): - """Single baked camera""" + """Single baked camera.""" identifier = "io.openpype.creators.blender.render" name = "renderingMain" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index e35e405ee1..91333b7741 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -1,11 +1,10 @@ """Create review.""" -from openpype.pipeline import CreatedInstance -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib class CreateReview(plugin.BaseCreator): - """Single baked camera""" + """Single baked camera.""" identifier = "io.openpype.creators.blender.review" name = "reviewDefault" @@ -16,19 +15,7 @@ class CreateReview(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): + """Run the creator on Blender main thread.""" # Run parent create method collection = super().create( subset_name, instance_data, pre_create_data diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 6682162f4b..b54cc73d4a 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateRig(plugin.BaseCreator): - """Artist-friendly rig with controls to direct motion""" + """Artist-friendly rig with controls to direct motion.""" identifier = "io.openpype.creators.blender.rig" name = "rigMain" @@ -22,7 +22,7 @@ class CreateRig(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" self._add_instance_to_context( CreatedInstance(self.family, subset_name, instance_data, self) ) From 7d20d332abc9e2353b16da83bf473694ac9830ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Nov 2023 18:15:23 +0100 Subject: [PATCH 181/298] don't use constants from objects --- openpype/tools/publisher/publish_report_viewer/window.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 127a65dd9b..dc4ad70934 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -329,7 +329,9 @@ class LoadedFilesView(QtWidgets.QTreeView): def __init__(self, *args, **kwargs): super(LoadedFilesView, self).__init__(*args, **kwargs) self.setEditTriggers( - self.EditKeyPressed | self.SelectedClicked | self.DoubleClicked + QtWidgets.QAbstractItemView.EditKeyPressed + | QtWidgets.QAbstractItemView.SelectedClicked + | QtWidgets.QAbstractItemView.DoubleClicked ) self.setIndentation(0) self.setAlternatingRowColors(True) @@ -366,7 +368,7 @@ class LoadedFilesView(QtWidgets.QTreeView): def _on_rows_inserted(self): header = self.header() - header.resizeSections(header.ResizeToContents) + header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) self._update_remove_btn() def resizeEvent(self, event): @@ -377,7 +379,7 @@ class LoadedFilesView(QtWidgets.QTreeView): super(LoadedFilesView, self).showEvent(event) self._model.refresh() header = self.header() - header.resizeSections(header.ResizeToContents) + header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) self._update_remove_btn() def _on_selection_change(self): From 1eeb70bf4ae7d6579eea76ab9eb857b1f0ace62a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 11 Nov 2023 03:25:56 +0000 Subject: [PATCH 182/298] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 8500b78966..611fdc82ce 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.6-nightly.1" +__version__ = "3.17.6-nightly.2" From 9bd74c74f16e11f751a106d84253f7aea605abc9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Nov 2023 03:26:30 +0000 Subject: [PATCH 183/298] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5d4db81a77..e377773007 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.6-nightly.2 - 3.17.6-nightly.1 - 3.17.5 - 3.17.5-nightly.3 @@ -134,7 +135,6 @@ body: - 3.15.2-nightly.1 - 3.15.1 - 3.15.1-nightly.6 - - 3.15.1-nightly.5 validations: required: true - type: dropdown From d3f4a397f4fa897ae66e44f04ddbd8fcdbaf22f5 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 09:47:52 +0200 Subject: [PATCH 184/298] add pointcache --- .../publish/validate_houdini_license_category.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index e0e06e37c8..1a21cd4746 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError +import hou class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): @@ -16,16 +17,18 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["usd"] + families = ["usd", "abc"] hosts = ["houdini"] label = "Houdini Apprentice License" def process(self, instance): - import hou + if hou.isApprentice() or 1: + families = [instance.data["family"]] + families += instance.data.get("families", []) + families = " ".join(families).title() - if hou.isApprentice(): raise PublishValidationError( - ("USD Publishing requires a non apprentice " - "license."), + "{} Publishing requires a non apprentice license." + .format(families), title=self.label) From 43a2955f865db051832363b5421ea624c2a1a607 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 09:50:47 +0200 Subject: [PATCH 185/298] remove debugging code --- .../plugins/publish/validate_houdini_license_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index 3894a8d57b..feb28aeaa6 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -23,7 +23,7 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): def process(self, instance): - if hou.isApprentice() or 1: + if hou.isApprentice(): families = [instance.data["family"]] families += instance.data.get("families", []) families = " ".join(families).title() From f84cf14316c36bb37d2f499a5d3f63866ece0010 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 09:59:10 +0200 Subject: [PATCH 186/298] update doc string --- .../publish/validate_houdini_license_category.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index feb28aeaa6..3d9e854dcd 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -7,13 +7,16 @@ import hou class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): """Validate the Houdini instance runs a non Apprentice license. - When extracting USD files from an apprentice Houdini license, - the resulting files will get "scrambled" with a license protection - and get a special .usdnc suffix. + USD ROPs: + When extracting USD files from an apprentice Houdini license, + the resulting files will get "scrambled" with a license protection + and get a special .usdnc suffix. - This currently breaks the Subset/representation pipeline so we disallow - any publish with apprentice license. + This currently breaks the Subset/representation pipeline so we disallow + any publish with apprentice license. + Alembic ROPs: + Houdini Apprentice does not export Alembic. """ order = pyblish.api.ValidatorOrder From 73a570d0f45342d2a1a09226de1f12d01db62024 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 12:40:15 +0200 Subject: [PATCH 187/298] better error reporting --- .../plugins/publish/validate_houdini_license_category.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index 3d9e854dcd..4124d0c489 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -27,11 +27,12 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): def process(self, instance): if hou.isApprentice(): - families = [instance.data["family"]] - families += instance.data.get("families", []) + # Find which family was matched with the plug-in + families = {instance.data["family"]} + families.update(instance.data.get("families", [])) families = " ".join(families).title() raise PublishValidationError( - "{} Publishing requires a non apprentice license." + "{} publishing requires a non apprentice license." .format(families), title=self.label) From c89ccfc3eaead3db4daddce53e19c042f728b7ce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 18:42:20 +0800 Subject: [PATCH 188/298] adding default value back to Anti-aliasing Quality and fix tab spaces issues in OP settings --- .../hosts/max/plugins/create/create_review.py | 2 +- .../projects_schema/schema_project_max.json | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 6651326a05..7aeea39b64 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -108,7 +108,7 @@ class CreateReview(plugin.MaxCreator): label="Pre-View Preset"), EnumDef("antialiasingQuality", anti_aliasing_enum, - default="None", + default=self.anti_aliasing, label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index b012e73fc4..82905480fa 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -98,10 +98,10 @@ "multiselection": false, "defaults": "exr", "enum_items": [ - {"exr": "exr"}, + {"exr": "exr"}, {"jpg": "jpg"}, - {"png": "png"}, - {"tga": "tga"} + {"png": "png"}, + {"tga": "tga"} ] }, { @@ -111,11 +111,11 @@ "multiselection": false, "defaults": "Realistic", "enum_items": [ - {"Realistic": "Realistic"}, + {"Realistic": "Realistic"}, {"Shaded": "Shaded"}, - {"Facets": "Facets"}, - {"ConsistentColors": "ConsistentColors"}, - {"HiddenLine": "HiddenLine"}, + {"Facets": "Facets"}, + {"ConsistentColors": "ConsistentColors"}, + {"HiddenLine": "HiddenLine"}, {"Wireframe": "Wireframe"}, {"BoundingBox": "BoundingBox"}, {"Ink": "Ink"}, @@ -136,11 +136,11 @@ "multiselection": false, "defaults": "Quality", "enum_items": [ - {"Quality": "Quality"}, + {"Quality": "Quality"}, {"Standard": "Standard"}, - {"Performance": "Performance"}, - {"DXMode": "DXMode"}, - {"Customize": "Customize"} + {"Performance": "Performance"}, + {"DXMode": "DXMode"}, + {"Customize": "Customize"} ] }, { @@ -150,10 +150,10 @@ "multiselection": false, "defaults": "None", "enum_items": [ - {"None": "None"}, + {"None": "None"}, {"2X": "2X"}, - {"4X": "4X"}, - {"8X": "8X"} + {"4X": "4X"}, + {"8X": "8X"} ] }, { From e209308ad71b49b7992aed1377d9c6719c848128 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 12:54:06 +0200 Subject: [PATCH 189/298] BigRoy's comment - only report the conflicting families --- .../plugins/publish/validate_houdini_license_category.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index 4124d0c489..5076acda60 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -30,7 +30,8 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): # Find which family was matched with the plug-in families = {instance.data["family"]} families.update(instance.data.get("families", [])) - families = " ".join(families).title() + disallowed_families = families.intersection(self.families) + families = " ".join(sorted(disallowed_families)).title() raise PublishValidationError( "{} publishing requires a non apprentice license." From 62fa3304178fc0d9d14bf4f7b5a58e2dee1c5680 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 13 Nov 2023 11:58:47 +0100 Subject: [PATCH 190/298] Add set_instance_data method to reduce redundancy --- openpype/hosts/blender/api/plugin.py | 41 +++++++++++++------ .../plugins/create/create_blendScene.py | 12 +----- .../blender/plugins/create/create_camera.py | 11 +---- .../blender/plugins/create/create_layout.py | 11 +---- .../blender/plugins/create/create_model.py | 11 +---- .../blender/plugins/create/create_rig.py | 11 +---- 6 files changed, 34 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index aef891bd83..e9683bee3a 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -217,7 +217,7 @@ class BaseCreator(Creator): Args: subset_name(str): Subset name of created instance. - instance_data(dict): Base data for instance. + instance_data(dict): Instance base data. pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ @@ -237,17 +237,7 @@ class BaseCreator(Creator): "name": collection.name, } - # Set instance data - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) + self.set_instance_data(subset_name, instance_data, instance_node) self._add_instance_to_context( CreatedInstance( @@ -326,6 +316,33 @@ class BaseCreator(Creator): self._remove_instance_from_context(instance) + def set_instance_data( + self, + subset_name: str, + instance_data: dict, + instance_node: bpy.types.ID, + ): + """Fill instance data with required items. + + Args: + subset_name(str): Subset name of created instance. + instance_data(dict): Instance base data. + instance_node(bpy.types.ID): Instance node in blender scene. + """ + if not instance_data: + instance_data = {} + + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index df3a70f199..0773c4dae3 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -47,17 +47,7 @@ class CreateBlendScene(plugin.Creator): "name": asset_group.name } - instance_data.update( - { - "id": "publish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) - + self.set_instance_data(subset_name, instance_data, instance_node) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index a42a9d6ef0..8dbba229a9 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -52,16 +52,7 @@ class CreateCamera(plugin.BaseCreator): "name": asset_group.name, } - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) + self.set_instance_data(subset_name, instance_data, instance_node) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ef1822dedf..a9bf115ea9 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -51,16 +51,7 @@ class CreateLayout(plugin.BaseCreator): "name": asset_group.name, } - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) + self.set_instance_data(subset_name, instance_data, instance_node) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index e4937f989e..3501071e8f 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -51,16 +51,7 @@ class CreateModel(plugin.BaseCreator): "name": asset_group.name, } - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) + self.set_instance_data(subset_name, instance_data, instance_node) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index b54cc73d4a..b5b61b3971 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -51,16 +51,7 @@ class CreateRig(plugin.BaseCreator): "name": asset_group.name, } - instance_data.update( - { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), - "subset": subset_name, - "instance_node": instance_node, - } - ) + self.set_instance_data(subset_name, instance_data, instance_node) lib.imprint(asset_group, instance_data) # Add selected objects to instance From 573d103889db8810c6780fb0ee0113ac6bf735df Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 19:10:34 +0800 Subject: [PATCH 191/298] use self.percentSize --- openpype/hosts/max/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 7aeea39b64..0284831533 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -88,7 +88,7 @@ class CreateReview(plugin.MaxCreator): default=self.review_height), NumberDef("percentSize", label="Percent of Output", - default=100, + default=self.percentSize, minimum=1, decimals=0), BoolDef("keepImages", From 7ac1d541321dd769c94598fc5c3f7c2c3a200051 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 19:48:42 +0800 Subject: [PATCH 192/298] Renamed preview animation to create review in OP/AYON settings --- openpype/hosts/max/plugins/create/create_review.py | 2 +- openpype/settings/defaults/project_settings/max.json | 2 +- .../entities/schemas/projects_schema/schema_project_max.json | 4 ++-- server_addon/max/server/settings/main.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 0284831533..538b445e66 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -24,7 +24,7 @@ class CreateReview(plugin.MaxCreator): @classmethod def apply_settings(cls, project_settings): - settings = project_settings["max"]["PreviewAnimation"] # noqa + settings = project_settings["max"]["CreateReview"] # noqa # Take some defaults from settings cls.review_width = settings.get("review_width", cls.review_width) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 0a1209668d..150cc9e684 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -16,7 +16,7 @@ "image_format": "exr", "multipass": true }, - "PreviewAnimation": { + "CreateReview": { "review_width": 1920, "review_height": 1080, "percentSize": 100, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 82905480fa..78cca357a3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -68,8 +68,8 @@ { "type": "dict", "collapsible": true, - "key": "PreviewAnimation", - "label": "Preview Animation", + "key": "CreateReview", + "label": "Create Review", "children": [ { "type": "number", diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index 0280fcebb9..ef02c6221e 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -32,7 +32,7 @@ class MaxSettings(BaseSettingsModel): default_factory=RenderSettingsModel, title="Render Settings" ) - PreviewAnimation: PreviewAnimationModel = Field( + CreateReview: PreviewAnimationModel = Field( default_factory=PreviewAnimationModel, title="Preview Animation" ) @@ -47,7 +47,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "RenderSettings": DEFAULT_RENDER_SETTINGS, - "PreviewAnimation": DEFAULT_PREVIEW_ANIMATION_SETTINGS, + "CreateReview": DEFAULT_PREVIEW_ANIMATION_SETTINGS, "PointCloud": { "attribute": [ {"name": "Age", "value": "age"}, From cf21c8e56f3bfba2f2c509aeb0b6686db0205876 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 20:47:14 +0800 Subject: [PATCH 193/298] reaame preview animation to create review --- ...review_animation.py => create_review_settings.py} | 4 ++-- server_addon/max/server/settings/main.py | 12 ++++++------ server_addon/max/server/version.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename server_addon/max/server/settings/{preview_animation.py => create_review_settings.py} (97%) diff --git a/server_addon/max/server/settings/preview_animation.py b/server_addon/max/server/settings/create_review_settings.py similarity index 97% rename from server_addon/max/server/settings/preview_animation.py rename to server_addon/max/server/settings/create_review_settings.py index 759ce78291..cc0f35ecb8 100644 --- a/server_addon/max/server/settings/preview_animation.py +++ b/server_addon/max/server/settings/create_review_settings.py @@ -56,7 +56,7 @@ def anti_aliasing_enum(): ] -class PreviewAnimationModel(BaseSettingsModel): +class CreateReviewModel(BaseSettingsModel): review_width: int = Field(1920, title="Review Width") review_height: int = Field(1080, title="Review Height") percentSize: float = Field(100.0, title="Percent of Output") @@ -80,7 +80,7 @@ class PreviewAnimationModel(BaseSettingsModel): vp_texture: bool = Field(True, title="Viewport Texture") -DEFAULT_PREVIEW_ANIMATION_SETTINGS = { +DEFAULT_CREATE_REVIEW_SETTINGS = { "review_width": 1920, "review_height": 1080, "percentSize": 100.0, diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index ef02c6221e..ea6a11915a 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -4,8 +4,8 @@ from .imageio import ImageIOSettings from .render_settings import ( RenderSettingsModel, DEFAULT_RENDER_SETTINGS ) -from .preview_animation import ( - PreviewAnimationModel, DEFAULT_PREVIEW_ANIMATION_SETTINGS +from .create_review_settings import ( + CreateReviewModel, DEFAULT_CREATE_REVIEW_SETTINGS ) from .publishers import ( PublishersModel, DEFAULT_PUBLISH_SETTINGS @@ -32,9 +32,9 @@ class MaxSettings(BaseSettingsModel): default_factory=RenderSettingsModel, title="Render Settings" ) - CreateReview: PreviewAnimationModel = Field( - default_factory=PreviewAnimationModel, - title="Preview Animation" + CreateReview: CreateReviewModel = Field( + default_factory=CreateReviewModel, + title="Create Review" ) PointCloud: PointCloudSettings = Field( default_factory=PointCloudSettings, @@ -47,7 +47,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "RenderSettings": DEFAULT_RENDER_SETTINGS, - "CreateReview": DEFAULT_PREVIEW_ANIMATION_SETTINGS, + "CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS, "PointCloud": { "attribute": [ {"name": "Age", "value": "age"}, diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From a4cbd80fa23e4398cd0dbb430c6c060c672b04f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 20:56:41 +0800 Subject: [PATCH 194/298] variable renaming & not using classmethod for apply_settings --- .../hosts/max/plugins/create/create_review.py | 25 +++++++++---------- .../server/settings/create_review_settings.py | 6 ++--- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 538b445e66..40358aefbf 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -22,22 +22,21 @@ class CreateReview(plugin.MaxCreator): vp_texture = True anti_aliasing = None - @classmethod - def apply_settings(cls, project_settings): + def apply_settings(self, project_settings): settings = project_settings["max"]["CreateReview"] # noqa # Take some defaults from settings - cls.review_width = settings.get("review_width", cls.review_width) - cls.review_height = settings.get("review_height", cls.review_height) - cls.percentSize = settings.get("percentSize", cls.percentSize) - cls.keep_images = settings.get("keep_images", cls.keep_images) - cls.image_format = settings.get("image_format", cls.image_format) - cls.visual_style = settings.get("visual_style", cls.visual_style) - cls.viewport_preset = settings.get( - "viewport_preset", cls.viewport_preset) - cls.anti_aliasing = settings.get( - "anti_aliasing", cls.anti_aliasing) - cls.vp_texture = settings.get("vp_texture", cls.vp_texture) + self.review_width = settings.get("review_width", self.review_width) + self.review_height = settings.get("review_height", self.review_height) + self.percentSize = settings.get("percentSize", self.percentSize) + self.keep_images = settings.get("keep_images", self.keep_images) + self.image_format = settings.get("image_format", self.image_format) + self.visual_style = settings.get("visual_style", self.visual_style) + self.viewport_preset = settings.get( + "viewport_preset", self.viewport_preset) + self.anti_aliasing = settings.get( + "anti_aliasing", self.anti_aliasing) + self.vp_texture = settings.get("vp_texture", self.vp_texture) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance diff --git a/server_addon/max/server/settings/create_review_settings.py b/server_addon/max/server/settings/create_review_settings.py index cc0f35ecb8..205ebbd09f 100644 --- a/server_addon/max/server/settings/create_review_settings.py +++ b/server_addon/max/server/settings/create_review_settings.py @@ -35,7 +35,7 @@ def visual_style_enum(): ] -def visual_preset_enum(): +def preview_preset_enum(): """Return enumerator for viewport visual preset.""" return [ {"label": "Quality", "value": "Quality"}, @@ -70,8 +70,8 @@ class CreateReviewModel(BaseSettingsModel): title="Preference" ) viewport_preset: str = Field( - enum_resolver=visual_preset_enum, - title="Pre-View Preset" + enum_resolver=preview_preset_enum , + title="Preview Preset" ) anti_aliasing: str = Field( enum_resolver=anti_aliasing_enum, From 7a778b3a83e74be670da7bd502137aefb386e3e1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 20:57:20 +0800 Subject: [PATCH 195/298] hound --- server_addon/max/server/settings/create_review_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/settings/create_review_settings.py b/server_addon/max/server/settings/create_review_settings.py index 205ebbd09f..43dac0730a 100644 --- a/server_addon/max/server/settings/create_review_settings.py +++ b/server_addon/max/server/settings/create_review_settings.py @@ -70,7 +70,7 @@ class CreateReviewModel(BaseSettingsModel): title="Preference" ) viewport_preset: str = Field( - enum_resolver=preview_preset_enum , + enum_resolver=preview_preset_enum, title="Preview Preset" ) anti_aliasing: str = Field( From aac075f93b2fa653ccd5dd4f1ca18339a78e42a9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 20:58:16 +0800 Subject: [PATCH 196/298] renamed the label for preview preset --- openpype/hosts/max/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 40358aefbf..e8c92fce79 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -104,7 +104,7 @@ class CreateReview(plugin.MaxCreator): EnumDef("viewportPreset", preview_preset_enum, default=self.viewport_preset, - label="Pre-View Preset"), + label="Preview Preset"), EnumDef("antialiasingQuality", anti_aliasing_enum, default=self.anti_aliasing, From b497876c51466103a60691f5106d2663063be3c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Nov 2023 14:33:03 +0100 Subject: [PATCH 197/298] Fix houdini workfile creator --- openpype/hosts/houdini/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index f8ee68ebc9..850f5c994e 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -48,7 +48,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): "variant": variant } if AYON_SERVER_ENABLED: - data["folderpath"] = asset_name + data["folderPath"] = asset_name else: data["asset"] = asset_name From cc77114db9e5e2ce81bdabdabf07d9ca4d86ef31 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Nov 2023 21:36:22 +0800 Subject: [PATCH 198/298] tweaks on the settings for variable --- openpype/hosts/max/plugins/create/create_review.py | 2 +- openpype/settings/defaults/project_settings/max.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index e8c92fce79..78d27a722b 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -20,7 +20,7 @@ class CreateReview(plugin.MaxCreator): visual_style = "Realistic" viewport_preset = "Quality" vp_texture = True - anti_aliasing = None + anti_aliasing = "None" def apply_settings(self, project_settings): settings = project_settings["max"]["CreateReview"] # noqa diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 150cc9e684..fdaa8d2b91 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -19,7 +19,7 @@ "CreateReview": { "review_width": 1920, "review_height": 1080, - "percentSize": 100, + "percentSize": 100.0, "keep_images": false, "image_format": "png", "visual_style": "Realistic", From f906d05c7305f123d8e037bd02e3d5742103883f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Nov 2023 16:56:47 +0100 Subject: [PATCH 199/298] fusion: removing hardcoded template name for saver --- openpype/hosts/fusion/plugins/create/create_saver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 2dc48f4b60..e8ba2880a4 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -149,9 +149,13 @@ class CreateSaver(NewCreator): # get frame padding from anatomy templates anatomy = Anatomy() - frame_padding = int( - anatomy.templates["render"].get("frame_padding", 4) - ) + render_anatomy_template = anatomy.templates.get("render") + if render_anatomy_template: + frame_padding = int( + render_anatomy_template.get("frame_padding", 4) + ) + else: + frame_padding = int(anatomy.templates.get("frame_padding", 4)) # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) From 8d58b284fc5a6fac5aeb535c3adcfabde4f7ff54 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 13 Nov 2023 17:21:28 +0100 Subject: [PATCH 200/298] Remove unused dependency, fix collect_render --- openpype/hosts/blender/api/plugin.py | 5 +---- openpype/hosts/blender/plugins/publish/collect_render.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index e9683bee3a..4bb489dca2 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -20,10 +20,7 @@ from .ops import ( MainThreadItem, execute_in_main_thread ) -from .lib import ( - imprint, - get_selection -) +from .lib import imprint VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 92e2473a95..578ac36ed3 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -73,7 +73,9 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): def process(self, instance): context = instance.context - render_data = bpy.data.collections[str(instance)].get("render_data") + render_data = bpy.data.collections[ + instance.data["instance_node"]["name"] + ].get("render_data") assert render_data, "No render data found." From 51f1b14fd679f7df16f9ec8c21dad24725f10e43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:20:09 +0100 Subject: [PATCH 201/298] Fix optional states --- .../blender/plugins/publish/extract_abc.py | 3 ++ .../plugins/publish/extract_abc_animation.py | 3 ++ .../blender/plugins/publish/extract_blend.py | 3 ++ .../publish/extract_blend_animation.py | 3 ++ .../plugins/publish/extract_camera_abc.py | 3 ++ .../plugins/publish/extract_camera_fbx.py | 3 ++ .../blender/plugins/publish/extract_fbx.py | 3 ++ .../plugins/publish/extract_fbx_animation.py | 3 ++ .../blender/plugins/publish/extract_layout.py | 3 ++ .../plugins/publish/extract_playblast.py | 3 +- .../publish/increment_workfile_version.py | 2 ++ .../publish/validate_camera_zero_keyframe.py | 3 ++ .../plugins/publish/validate_file_saved.py | 30 +++++++++++++++++-- .../plugins/publish/validate_mesh_has_uv.py | 3 ++ .../validate_mesh_no_negative_scale.py | 11 +++++-- .../publish/validate_no_colons_in_name.py | 11 +++++-- .../plugins/publish/validate_object_mode.py | 3 ++ .../publish/validate_render_camera_is_set.py | 8 ++++- .../publish/validate_transform_zero.py | 11 +++++-- 19 files changed, 102 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 5af8104344..61ff17e441 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -15,6 +15,9 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): families = ["pointcache"] def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) filename = f"{instance.name}.abc" diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 0b6b93b7a5..7ec10ed6c4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -18,6 +18,9 @@ class ExtractAnimationABC( optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) filename = f"{instance.name}.abc" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index fba9a861a0..37842ed90f 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -14,6 +14,9 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path if not self.is_active(instance.data): diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 3a5b788c9e..84e1e7d602 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -17,6 +17,9 @@ class ExtractBlendAnimation( optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 2a327f4d65..1c0c033364 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -16,6 +16,9 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) filename = f"{instance.name}.abc" diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 8e5e4d37d4..8e5cc6f651 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -15,6 +15,9 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) filename = f"{instance.name}.fbx" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 8ace6a43a7..07c35207d8 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -16,6 +16,9 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) filename = f"{instance.name}.fbx" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 04f50f8207..5a8381dbc3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -22,6 +22,9 @@ class ExtractAnimationFBX( optional = True def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 8445560bba..576c2c33fc 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -113,6 +113,9 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): return None, n def process(self, instance): + if not self.is_active(instance.data): + return + # Define extract output file path stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 82ddbc1fc2..07b51becce 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -24,7 +24,8 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): order = pyblish.api.ExtractorOrder + 0.01 def process(self, instance): - self.log.info("Extracting capture..") + if not self.is_active(instance.data): + return self.log.info(instance.data) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 176668f366..8e522977e4 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -17,6 +17,8 @@ class IncrementWorkfileVersion( "pointcache", "render"] def process(self, context): + if not self.is_active(context.data): + return assert all(result["success"] for result in context.data["results"]), ( "Publishing not successful so version is not increased.") diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index 48c267fd18..f109d9c3ce 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -40,6 +40,9 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( diff --git a/openpype/hosts/blender/plugins/publish/validate_file_saved.py b/openpype/hosts/blender/plugins/publish/validate_file_saved.py index e191585c55..44b33a573a 100644 --- a/openpype/hosts/blender/plugins/publish/validate_file_saved.py +++ b/openpype/hosts/blender/plugins/publish/validate_file_saved.py @@ -2,8 +2,24 @@ import bpy import pyblish.api +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateFileSaved(pyblish.api.InstancePlugin): + +class SaveWorkfileAction(pyblish.api.Action): + """Save Workfile.""" + label = "Save Workfile" + on = "failed" + icon = "save" + + def process(self, context, plugin): + bpy.ops.wm.avalon_workfiles() + + +class ValidateFileSaved(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate that the workfile has been saved.""" order = pyblish.api.ValidatorOrder - 0.01 @@ -11,10 +27,20 @@ class ValidateFileSaved(pyblish.api.InstancePlugin): label = "Validate File Saved" optional = False exclude_families = [] + actions = [SaveWorkfileAction] def process(self, instance): + if not self.is_active(instance.data): + return + + if not instance.context.data["currentFile"]: + # File has not been saved at all and has no filename + raise PublishValidationError( + "Current file is empty. Save the file before continuing." + ) + if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return if bpy.data.is_dirty: - raise RuntimeError("Workfile is not saved.") + raise PublishValidationError("Workfile has unsaved changes.") diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index 687371b362..c2d63685e2 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -55,6 +55,9 @@ class ValidateMeshHasUvs( return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 618feb95c1..737fd80798 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -4,11 +4,15 @@ import bpy import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, +) import openpype.hosts.blender.api.action -class ValidateMeshNoNegativeScale(pyblish.api.Validator): +class ValidateMeshNoNegativeScale(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale.""" order = ValidateContentsOrder @@ -27,6 +31,9 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index 1a98ec4c1d..fbaf40e91f 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -5,10 +5,14 @@ import bpy import pyblish.api import openpype.hosts.blender.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, +) -class ValidateNoColonsInName(pyblish.api.InstancePlugin): +class ValidateNoColonsInName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """There cannot be colons in names Object or bone names cannot include colons. Other software do not @@ -36,6 +40,9 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index d8d2e3c8bf..19e8d1bc8d 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -29,6 +29,9 @@ class ValidateObjectIsInObjectMode( return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py index ba3a796f35..0207b0fd09 100644 --- a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -2,8 +2,11 @@ import bpy import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin -class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): + +class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate that there is a camera set as active for rendering.""" order = pyblish.api.ValidatorOrder @@ -13,5 +16,8 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): optional = False def process(self, instance): + if not self.is_active(instance.data): + return + if not bpy.context.scene.camera: raise RuntimeError("No camera is active for rendering.") diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 66ef731e6e..3c68e61277 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -6,10 +6,14 @@ import bpy import pyblish.api import openpype.hosts.blender.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, +) -class ValidateTransformZero(pyblish.api.InstancePlugin): +class ValidateTransformZero(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Transforms can't have any values To solve this issue, try freezing the transforms. So long @@ -38,6 +42,9 @@ class ValidateTransformZero(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise RuntimeError( From 7a0fd92481dcbb36e75bb564a1d0f951804f34c0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:23:08 +0100 Subject: [PATCH 202/298] Remove legacy instance collecting (since it resulted in duplicate instances) + fix workfile publishing --- .../plugins/publish/collect_current_file.py | 57 --------------- .../plugins/publish/collect_instances.py | 70 ------------------- .../plugins/publish/collect_workfile.py | 2 +- 3 files changed, 1 insertion(+), 128 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/publish/collect_instances.py diff --git a/openpype/hosts/blender/plugins/publish/collect_current_file.py b/openpype/hosts/blender/plugins/publish/collect_current_file.py index c2d8a96a18..91c88f2e28 100644 --- a/openpype/hosts/blender/plugins/publish/collect_current_file.py +++ b/openpype/hosts/blender/plugins/publish/collect_current_file.py @@ -1,72 +1,15 @@ -import os -import bpy - import pyblish.api -from openpype.pipeline import get_current_task_name, get_current_asset_name from openpype.hosts.blender.api import workio -class SaveWorkfiledAction(pyblish.api.Action): - """Save Workfile.""" - label = "Save Workfile" - on = "failed" - icon = "save" - - def process(self, context, plugin): - bpy.ops.wm.avalon_workfiles() - - class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" order = pyblish.api.CollectorOrder - 0.5 label = "Blender Current File" hosts = ["blender"] - actions = [SaveWorkfiledAction] def process(self, context): """Inject the current working file""" current_file = workio.current_file() - context.data["currentFile"] = current_file - - assert current_file, ( - "Current file is empty. Save the file before continuing." - ) - - folder, file = os.path.split(current_file) - filename, ext = os.path.splitext(file) - - task = get_current_task_name() - - data = {} - - # create instance - instance = context.create_instance(name=filename) - subset = "workfile" + task.capitalize() - - data.update({ - "subset": subset, - "asset": get_current_asset_name(), - "label": subset, - "publish": True, - "family": "workfile", - "families": ["workfile"], - "setMembers": [current_file], - "frameStart": bpy.context.scene.frame_start, - "frameEnd": bpy.context.scene.frame_end, - }) - - data["representations"] = [{ - "name": ext.lstrip("."), - "ext": ext.lstrip("."), - "files": file, - "stagingDir": folder, - }] - - instance.data.update(data) - - self.log.info("Collected instance: {}".format(file)) - self.log.info("Scene path: {}".format(current_file)) - self.log.info("staging Dir: {}".format(folder)) - self.log.info("subset: {}".format(subset)) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py deleted file mode 100644 index ad2ce54147..0000000000 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -from typing import Generator - -import bpy - -import pyblish.api -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) - - -class CollectInstances(pyblish.api.ContextPlugin): - """Collect the data of a model.""" - - hosts = ["blender"] - label = "Collect Instances" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_asset_groups() -> Generator: - """Return all instances that are empty objects asset groups. - """ - instances = bpy.data.collections.get(AVALON_INSTANCES) - for obj in list(instances.objects) + list(instances.children): - avalon_prop = obj.get(AVALON_PROPERTY) or {} - if avalon_prop.get('id') == 'pyblish.avalon.instance': - yield obj - - @staticmethod - def create_instance(context, group): - avalon_prop = group[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - return context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - - def process(self, context): - """Collect the models from the current Blender scene.""" - asset_groups = self.get_asset_groups() - - for group in asset_groups: - instance = self.create_instance(context, group) - members = [] - if isinstance(group, bpy.types.Collection): - members = list(group.objects) - family = instance.data["family"] - if family == "animation": - for obj in group.objects: - if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): - members.extend( - child for child in obj.children - if child.type == 'ARMATURE') - else: - members = group.children_recursive - - members.append(group) - instance[:] = members - self.log.debug(json.dumps(instance.data, indent=4)) - for obj in instance: - self.log.debug(obj) diff --git a/openpype/hosts/blender/plugins/publish/collect_workfile.py b/openpype/hosts/blender/plugins/publish/collect_workfile.py index 01c033084b..6561c89605 100644 --- a/openpype/hosts/blender/plugins/publish/collect_workfile.py +++ b/openpype/hosts/blender/plugins/publish/collect_workfile.py @@ -28,7 +28,7 @@ class CollectWorkfile(InstancePlugin): "representations": [ { "name": ext.lstrip("."), - "ext": ext, + "ext": ext.lstrip("."), "files": filepath.name, "stagingDir": filepath.parent, } From 60486c2d9f07775d6dcd41f34fa7434d7120d84a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:36:26 +0100 Subject: [PATCH 203/298] Tweak logging levels for artist-facing reports + some log message cosmetics --- openpype/hosts/blender/plugins/publish/collect_render.py | 4 ++-- openpype/hosts/blender/plugins/publish/extract_abc.py | 6 +++--- .../blender/plugins/publish/extract_abc_animation.py | 6 +++--- openpype/hosts/blender/plugins/publish/extract_blend.py | 6 +++--- .../blender/plugins/publish/extract_blend_animation.py | 6 +++--- .../hosts/blender/plugins/publish/extract_camera_abc.py | 6 +++--- .../hosts/blender/plugins/publish/extract_camera_fbx.py | 6 +++--- openpype/hosts/blender/plugins/publish/extract_fbx.py | 6 +++--- .../blender/plugins/publish/extract_fbx_animation.py | 8 ++++---- openpype/hosts/blender/plugins/publish/extract_layout.py | 8 ++++---- .../hosts/blender/plugins/publish/extract_playblast.py | 6 ++---- .../hosts/blender/plugins/publish/extract_thumbnail.py | 6 +++--- .../blender/plugins/publish/increment_workfile_version.py | 2 +- .../hosts/blender/plugins/publish/integrate_animation.py | 2 +- 14 files changed, 38 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 578ac36ed3..48b03256ae 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -79,7 +79,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): assert render_data, "No render data found." - self.log.info(f"render_data: {dict(render_data)}") + self.log.debug(f"render_data: {dict(render_data)}") render_product = render_data.get("render_product") aov_file_product = render_data.get("aov_file_product") @@ -122,4 +122,4 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "renderProducts": colorspace.ARenderProduct(), }) - self.log.info(f"data: {instance.data}") + self.log.debug(f"data: {instance.data}") diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 61ff17e441..1602c4d266 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -24,7 +24,7 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() @@ -62,8 +62,8 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) class ExtractModelABC(ExtractABC): diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 7ec10ed6c4..0ed2819407 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -27,7 +27,7 @@ class ExtractAnimationABC( filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() @@ -72,5 +72,5 @@ class ExtractAnimationABC( } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index 37842ed90f..884169f7f1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -27,7 +27,7 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") data_blocks = set() @@ -58,5 +58,5 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 84e1e7d602..2e0de9317e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -27,7 +27,7 @@ class ExtractBlendAnimation( filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") data_blocks = set() @@ -56,5 +56,5 @@ class ExtractBlendAnimation( } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 1c0c033364..9db1505d37 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -25,7 +25,7 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() @@ -67,5 +67,5 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 8e5cc6f651..9bbcf047cc 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -24,7 +24,7 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() @@ -76,5 +76,5 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 07c35207d8..fae438158a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -25,7 +25,7 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() @@ -87,5 +87,5 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 5a8381dbc3..ba584ac99a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -29,7 +29,7 @@ class ExtractAnimationFBX( stagingdir = self.staging_dir(instance) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") # The first collection object in the instance is taken, as there # should be only one that contains the asset group. @@ -62,7 +62,7 @@ class ExtractAnimationFBX( starting_frames.append(curr_frame_range[0]) ending_frames.append(curr_frame_range[1]) else: - self.log.info("Object have no animation.") + self.log.info("Object has no animation.") return asset_group_name = asset_group.name @@ -164,5 +164,5 @@ class ExtractAnimationFBX( instance.data["representations"].append(fbx_representation) instance.data["representations"].append(json_representation) - self.log.info("Extracted instance '{}' to: {}".format( - instance.name, fbx_representation)) + self.log.debug("Extracted instance '{}' to: {}".format( + instance.name, fbx_representation)) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 576c2c33fc..d0adac6edb 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -45,7 +45,7 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): starting_frames.append(curr_frame_range[0]) ending_frames.append(curr_frame_range[1]) else: - self.log.info("Object have no animation.") + self.log.info("Object has no animation.") continue asset_group_name = asset.name @@ -120,7 +120,7 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): stagingdir = self.staging_dir(instance) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") if "representations" not in instance.data: instance.data["representations"] = [] @@ -248,5 +248,5 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): } instance.data["representations"].append(fbx_representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, json_representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, json_representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 07b51becce..696cf85089 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -27,8 +27,6 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - self.log.info(instance.data) - # get scene fps fps = instance.data.get("fps") if fps is None: @@ -56,7 +54,7 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): filename = instance.name path = os.path.join(stagingdir, filename) - self.log.info(f"Outputting images to {path}") + self.log.debug(f"Outputting images to {path}") project_settings = instance.context.data["project_settings"]["blender"] presets = project_settings["publish"]["ExtractPlayblast"]["presets"] @@ -101,7 +99,7 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): frame_collection = collections[0] - self.log.info(f"We found collection of interest {frame_collection}") + self.log.debug(f"We found collection of interest {frame_collection}") instance.data.setdefault("representations", []) diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 65c3627375..52e5d98fc4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -24,13 +24,13 @@ class ExtractThumbnail(publish.Extractor): presets = {} def process(self, instance): - self.log.info("Extracting capture..") + self.log.debug("Extracting capture..") stagingdir = self.staging_dir(instance) filename = instance.name path = os.path.join(stagingdir, filename) - self.log.info(f"Outputting images to {path}") + self.log.debug(f"Outputting images to {path}") camera = instance.data.get("review_camera", "AUTO") start = instance.data.get("frameStart", bpy.context.scene.frame_start) @@ -61,7 +61,7 @@ class ExtractThumbnail(publish.Extractor): thumbnail = os.path.basename(self._fix_output_path(path)) - self.log.info(f"thumbnail: {thumbnail}") + self.log.debug(f"thumbnail: {thumbnail}") instance.data.setdefault("representations", []) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 8e522977e4..7e33fd53fa 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -29,4 +29,4 @@ class IncrementWorkfileVersion( save_file(filepath, copy=False) - self.log.info('Incrementing script version') + self.log.debug('Incrementing blender workfile version') diff --git a/openpype/hosts/blender/plugins/publish/integrate_animation.py b/openpype/hosts/blender/plugins/publish/integrate_animation.py index b7e5423fa8..623da9c585 100644 --- a/openpype/hosts/blender/plugins/publish/integrate_animation.py +++ b/openpype/hosts/blender/plugins/publish/integrate_animation.py @@ -17,7 +17,7 @@ class IntegrateAnimation( families = ["setdress"] def process(self, instance): - self.log.info("Integrate Animation") + self.log.debug("Integrate Animation") representation = instance.data.get('representations')[0] json_path = representation.get('publishedFiles')[0] From ae6c810ba54091d5157dd6d656589377da7f2446 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:38:21 +0100 Subject: [PATCH 204/298] Raise PublishValidationError from validators --- .../plugins/publish/validate_camera_zero_keyframe.py | 7 +++++-- .../blender/plugins/publish/validate_deadline_publish.py | 2 +- .../hosts/blender/plugins/publish/validate_mesh_has_uv.py | 3 ++- .../plugins/publish/validate_mesh_no_negative_scale.py | 3 ++- .../blender/plugins/publish/validate_no_colons_in_name.py | 3 ++- .../hosts/blender/plugins/publish/validate_object_mode.py | 7 +++++-- .../plugins/publish/validate_render_camera_is_set.py | 7 +++++-- .../blender/plugins/publish/validate_transform_zero.py | 3 ++- 8 files changed, 24 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index f109d9c3ce..65697cb86d 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -5,7 +5,10 @@ import bpy import pyblish.api import openpype.hosts.blender.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): @@ -45,6 +48,6 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( f"Camera must have a keyframe at frame 0: {invalid}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index 14220b5c9c..58047d7e23 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -36,7 +36,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, "Render output folder " "doesn't match the blender scene name! " "Use Repair action to " - "fix the folder file path.." + "fix the folder file path." ) @classmethod diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index c2d63685e2..dd955dc5da 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, + PublishValidationError ) import openpype.hosts.blender.api.action @@ -60,6 +61,6 @@ class ValidateMeshHasUvs( invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( f"Meshes found in instance without valid UV's: {invalid}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 737fd80798..a498a3b4cb 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, + PublishValidationError ) import openpype.hosts.blender.api.action @@ -36,6 +37,6 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( f"Meshes found in instance with negative scale: {invalid}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index fbaf40e91f..17119f8d88 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -8,6 +8,7 @@ import openpype.hosts.blender.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, + PublishValidationError ) @@ -45,6 +46,6 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( f"Objects found with colon in name: {invalid}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 19e8d1bc8d..3b6f29a79e 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -3,7 +3,10 @@ from typing import List import bpy import pyblish.api -from openpype.pipeline.publish import OptionalPyblishPluginMixin +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError +) import openpype.hosts.blender.api.action @@ -34,6 +37,6 @@ class ValidateObjectIsInObjectMode( invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( f"Object found in instance is not in Object Mode: {invalid}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py index 0207b0fd09..86d1fcc681 100644 --- a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -2,7 +2,10 @@ import bpy import pyblish.api -from openpype.pipeline.publish import OptionalPyblishPluginMixin +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError +) class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin, @@ -20,4 +23,4 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin, return if not bpy.context.scene.camera: - raise RuntimeError("No camera is active for rendering.") + raise PublishValidationError("No camera is active for rendering.") diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 3c68e61277..5270a50888 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -9,6 +9,7 @@ import openpype.hosts.blender.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, + PublishValidationError ) @@ -47,7 +48,7 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( "Object found in instance has not" f" transform to zero: {invalid}" ) From 1e7ef83b188b9c415cb403e702f93c380d0afdce Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:53:17 +0100 Subject: [PATCH 205/298] Validate workfile saved once; not per instance - refactored to ContextPlugin --- .../plugins/publish/validate_file_saved.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_file_saved.py b/openpype/hosts/blender/plugins/publish/validate_file_saved.py index 44b33a573a..442f856e05 100644 --- a/openpype/hosts/blender/plugins/publish/validate_file_saved.py +++ b/openpype/hosts/blender/plugins/publish/validate_file_saved.py @@ -18,7 +18,7 @@ class SaveWorkfileAction(pyblish.api.Action): bpy.ops.wm.avalon_workfiles() -class ValidateFileSaved(pyblish.api.InstancePlugin, +class ValidateFileSaved(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Validate that the workfile has been saved.""" @@ -29,18 +29,33 @@ class ValidateFileSaved(pyblish.api.InstancePlugin, exclude_families = [] actions = [SaveWorkfileAction] - def process(self, instance): - if not self.is_active(instance.data): + def process(self, context): + if not self.is_active(context.data): return - if not instance.context.data["currentFile"]: + if not context.data["currentFile"]: # File has not been saved at all and has no filename raise PublishValidationError( "Current file is empty. Save the file before continuing." ) - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: + # Do not validate workfile has unsaved changes if only instances + # present of families that should be excluded + families = { + instance.data["family"] for instance in context + # Consider only enabled instances + if instance.data.get("publish", True) + and instance.data.get("active", True) + } + + def is_excluded(family): + return any(family in exclude_family + for exclude_family in self.exclude_families) + + if all(is_excluded(family) for family in families): + self.log.debug("Only excluded families found, skipping workfile " + "unsaved changes validation..") return + if bpy.data.is_dirty: raise PublishValidationError("Workfile has unsaved changes.") From b8255c6ee3e79d5a9d4466437ed6ec5628edf382 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Nov 2023 23:53:44 +0100 Subject: [PATCH 206/298] Simplify collecting instances (re-use logic) --- openpype/hosts/blender/api/plugin.py | 55 +++++++++++----------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 4bb489dca2..1114d136ce 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -1,5 +1,6 @@ """Shared functionality for pipeline plugins for Blender.""" +import itertools from pathlib import Path from typing import Dict, List, Optional @@ -164,42 +165,30 @@ class BaseCreator(Creator): cache_legacy = {} avalon_instances = bpy.data.collections.get(AVALON_INSTANCES) - if avalon_instances: - for obj in bpy.data.collections.get(AVALON_INSTANCES).objects: - avalon_prop = obj.get(AVALON_PROPERTY, {}) - if avalon_prop.get('id') == 'pyblish.avalon.instance': - creator_id = avalon_prop.get('creator_identifier') + avalon_instance_objs = ( + avalon_instances.objects if avalon_instances else [] + ) - if creator_id: - # Creator instance - cache.setdefault(creator_id, []).append( - avalon_prop - ) - else: - family = avalon_prop.get('family') - if family: - # Legacy creator instance - cache_legacy.setdefault(family, []).append( - avalon_prop - ) + for obj_or_col in itertools.chain( + avalon_instance_objs, + bpy.data.collections + ): + avalon_prop = obj_or_col.get(AVALON_PROPERTY, {}) + if not avalon_prop: + continue - for col in bpy.data.collections: - avalon_prop = col.get(AVALON_PROPERTY, {}) - if avalon_prop.get('id') == 'pyblish.avalon.instance': - creator_id = avalon_prop.get('creator_identifier') + if avalon_prop.get('id') != 'pyblish.avalon.instance': + continue - if creator_id: - # Creator instance - cache.setdefault(creator_id, []).append(avalon_prop) - else: - family = avalon_prop.get('family') - if family: - cache_legacy.setdefault(family, []) - if family: - # Legacy creator instance - cache_legacy.setdefault(family, []).append( - avalon_prop - ) + creator_id = avalon_prop.get('creator_identifier') + if creator_id: + # Creator instance + cache.setdefault(creator_id, []).append(avalon_prop) + else: + family = avalon_prop.get('family') + if family: + # Legacy creator instance + cache_legacy.setdefault(family, []).append(avalon_prop) shared_data["blender_cached_subsets"] = cache shared_data["blender_cached_legacy_subsets"] = cache_legacy From 66981669545046e14551250b14a195132a231887 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 00:24:57 +0100 Subject: [PATCH 207/298] Refactor to correct name --- .../hosts/blender/plugins/publish/validate_instance_empty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 3ebc6515d3..7669d76943 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -13,7 +13,7 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): optional = False def process(self, instance): - asset_group = instance.data["instance_group"] + asset_group = instance.data["instance_node"] if isinstance(asset_group, bpy.types.Collection): if not (asset_group.objects or asset_group.children): From 871bf194da2e7743363697032a984c41a2e54fc4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 00:36:33 +0100 Subject: [PATCH 208/298] Avoid double space in log message --- openpype/plugins/publish/collect_comment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_comment.py b/openpype/plugins/publish/collect_comment.py index 9f41e37f22..38d61a7071 100644 --- a/openpype/plugins/publish/collect_comment.py +++ b/openpype/plugins/publish/collect_comment.py @@ -103,10 +103,10 @@ class CollectComment( instance.data["comment"] = instance_comment if instance_comment: - msg_end = " has comment set to: \"{}\"".format( + msg_end = "has comment set to: \"{}\"".format( instance_comment) else: - msg_end = " does not have set comment" + msg_end = "does not have set comment" self.log.debug("Instance {} {}".format(instance_label, msg_end)) def cleanup_comment(self, comment): From 3d10bfdf0916d9dd74db3ad5e1edf655fdbd3478 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 00:37:13 +0100 Subject: [PATCH 209/298] Cosmetics --- openpype/hosts/blender/api/plugin.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index e126511fd6..0399bf2e50 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -256,19 +256,16 @@ class BaseCreator(Creator): if not cached_subsets: return + # Process only instances that were created by this creator for instance_data in cached_subsets.get(self.identifier, []): - # Process only instances that were created by this creator - data = instance_data.to_dict() - creator_id = data.get('creator_identifier') + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data=instance_data.to_dict(), + creator=self + ) - if creator_id == self.identifier: - # Create instance object from existing data - instance = CreatedInstance.from_existing( - data, self - ) - - # Add instance to create context - self._add_instance_to_context(instance) + # Add instance to create context + self._add_instance_to_context(instance) def update_instances(self, update_list): """Override abstract method from BaseCreator. From 8575c4843caf7074dbdc5376076d4ee805e2fb7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 02:25:42 +0100 Subject: [PATCH 210/298] Fix storing instance and context data correctly, fix removing instances + refactor instance nodes --- openpype/hosts/blender/api/ops.py | 8 ++ openpype/hosts/blender/api/pipeline.py | 7 +- openpype/hosts/blender/api/plugin.py | 89 ++++++++++--------- .../blender/plugins/create/create_action.py | 9 +- .../plugins/create/create_animation.py | 2 +- .../plugins/create/create_blendScene.py | 39 ++------ .../blender/plugins/create/create_camera.py | 41 ++------- .../blender/plugins/create/create_layout.py | 37 ++------ .../blender/plugins/create/create_model.py | 40 ++------- .../plugins/create/create_pointcache.py | 2 +- .../blender/plugins/create/create_review.py | 3 +- .../blender/plugins/create/create_rig.py | 39 ++------ .../blender/plugins/create/create_workfile.py | 52 ++++++----- .../hosts/blender/plugins/load/load_blend.py | 1 + .../blender/plugins/load/load_layout_json.py | 1 + .../blender/plugins/publish/collect_render.py | 5 +- .../blender/plugins/publish/collect_review.py | 4 +- .../publish/validate_instance_empty.py | 2 +- 18 files changed, 140 insertions(+), 241 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 22c590d4bd..c617144b6f 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -31,6 +31,14 @@ PREVIEW_COLLECTIONS: Dict = dict() TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1 +def execute_function_in_main_thread(f): + """Decorator to move a function call into main thread items""" + def wrapper(*args, **kwargs): + mti = MainThreadItem(f, *args, **kwargs) + execute_in_main_thread(mti) + return wrapper + + class BlenderApplication(QtWidgets.QApplication): _instance = None blender_windows = {} diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index c3f8f06694..9ac59f5620 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -138,7 +138,10 @@ class BlenderHost(HostBase, IWorkfileHost, IPublishHost): Returns: dict: Context data stored using 'update_context_data'. """ - return bpy.context.scene.openpype_context + property = bpy.context.scene.get(AVALON_PROPERTY) + if property: + return property.to_dict() + return {} def update_context_data(self, data: dict, changes: dict): """Override abstract method from IPublishHost. @@ -149,7 +152,7 @@ class BlenderHost(HostBase, IWorkfileHost, IPublishHost): changes (dict): Only data that has been changed. Each value has tuple with '(, )' value. """ - bpy.context.scene.openpype_context.update(data) + bpy.context.scene[AVALON_PROPERTY] = data def pype_excepthook_handler(*args): diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 0399bf2e50..d1cefe0a62 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -12,6 +12,8 @@ from openpype.pipeline import ( LoaderPlugin, get_current_task_name, ) +from openpype.lib import BoolDef + from .pipeline import ( AVALON_CONTAINERS, AVALON_INSTANCES, @@ -149,6 +151,8 @@ class BaseCreator(Creator): """Base class for Blender Creator plug-ins.""" defaults = ['Main'] + create_as_asset_group = False + @staticmethod def cache_subsets(shared_data): """Cache instances for Creators shared data. @@ -190,12 +194,12 @@ class BaseCreator(Creator): creator_id = avalon_prop.get('creator_identifier') if creator_id: # Creator instance - cache.setdefault(creator_id, []).append(avalon_prop) + cache.setdefault(creator_id, []).append(obj_or_col) else: family = avalon_prop.get('family') if family: # Legacy creator instance - cache_legacy.setdefault(family, []).append(avalon_prop) + cache_legacy.setdefault(family, []).append(obj_or_col) shared_data["blender_cached_subsets"] = cache shared_data["blender_cached_legacy_subsets"] = cache_legacy @@ -220,27 +224,29 @@ class BaseCreator(Creator): instances = bpy.data.collections.new(name=AVALON_INSTANCES) bpy.context.scene.collection.children.link(instances) - # Create instance collection - collection = bpy.data.collections.new( - name=asset_name(instance_data["asset"], subset_name) + # Create asset group + name = asset_name(instance_data["asset"], subset_name) + if self.create_as_asset_group: + # Create instance as empty + instance_node = bpy.data.objects.new(name=name, object_data=None) + instance_node.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(instance_node) + else: + # Create instance collection + instance_node = bpy.data.collections.new(name=name) + instances.children.link(instance_node) + + self.set_instance_data(subset_name, instance_data) + + instance = CreatedInstance( + self.family, subset_name, instance_data, self ) - instances.children.link(collection) + instance.transient_data["instance_node"] = instance_node + self._add_instance_to_context(instance) - collection[AVALON_PROPERTY] = instance_node = { - "name": collection.name, - } + imprint(instance_node, instance_data) - self.set_instance_data(subset_name, instance_data, instance_node) - - self._add_instance_to_context( - CreatedInstance( - self.family, subset_name, instance_data, self - ) - ) - - imprint(collection, instance_data) - - return collection + return instance_node def collect_instances(self): """Override abstract method from BaseCreator. @@ -257,12 +263,14 @@ class BaseCreator(Creator): return # Process only instances that were created by this creator - for instance_data in cached_subsets.get(self.identifier, []): + for instance_node in cached_subsets.get(self.identifier, []): + property = instance_node.get(AVALON_PROPERTY) # Create instance object from existing data instance = CreatedInstance.from_existing( - instance_data=instance_data.to_dict(), + instance_data=property.to_dict(), creator=self ) + instance.transient_data["instance_node"] = instance_node # Add instance to create context self._add_instance_to_context(instance) @@ -276,41 +284,32 @@ class BaseCreator(Creator): and their changes, as a list of tuples.""" for created_instance, _changes in update_list: data = created_instance.data_to_store() - - imprint(data.get("instance_node", {}), data) + node = created_instance.transient_data["instance_node"] + if node: + imprint(node, data) def remove_instances(self, instances: List[CreatedInstance]): - """Override abstract method from BaseCreator. - Method called when instances are removed. - Args: - instance(List[CreatedInstance]): Instance objects to remove. - """ for instance in instances: - outliner_entity = instance.data.get("instance_node", {}).get( - "datablock" - ) - if not outliner_entity: - continue + node = instance.transient_data["instance_node"] - if isinstance(outliner_entity, bpy.types.Collection): - for children in outliner_entity.children_recursive: + if isinstance(node, bpy.types.Collection): + for children in node.children_recursive: if isinstance(children, bpy.types.Collection): bpy.data.collections.remove(children) else: bpy.data.objects.remove(children) - bpy.data.collections.remove(outliner_entity) - elif isinstance(outliner_entity, bpy.types.Object): - bpy.data.objects.remove(outliner_entity) + bpy.data.collections.remove(node) + elif isinstance(node, bpy.types.Object): + bpy.data.objects.remove(node) self._remove_instance_from_context(instance) def set_instance_data( self, subset_name: str, - instance_data: dict, - instance_node: bpy.types.ID, + instance_data: dict ): """Fill instance data with required items. @@ -329,10 +328,16 @@ class BaseCreator(Creator): "label": subset_name, "task": get_current_task_name(), "subset": subset_name, - "instance_node": instance_node, } ) + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", + label="Use selection", + default=True) + ] + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index ac425dff74..f14023639f 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -2,11 +2,10 @@ import bpy -from openpype.hosts.blender.api.plugin import BaseCreator, asset_name -from openpype.hosts.blender.api import lib +from openpype.hosts.blender.api import lib, plugin -class CreateAction(BaseCreator): +class CreateAction(plugin.BaseCreator): """Action output for character rigs.""" identifier = "io.openpype.creators.blender.action" @@ -24,9 +23,9 @@ class CreateAction(BaseCreator): ) # Get instance name - name = asset_name(instance_data["asset"], subset_name) + name = plugin.asset_name(instance_data["asset"], subset_name) - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): for obj in lib.get_selection(): if (obj.animation_data is not None and obj.animation_data.action is not None): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index f7cb9f88aa..6f0cabb259 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -21,7 +21,7 @@ class CreateAnimation(plugin.BaseCreator): subset_name, instance_data, pre_create_data ) - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): selected = lib.get_selection() for obj in selected: collection.objects.link(obj) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 1dc8f44a63..f1090ae397 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -23,42 +23,17 @@ class CreateBlendScene(plugin.Creator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) + instance_node = super().create(subset_name, + instance_data, + pre_create_data) - # Create instance object - asset = instance_data.get("asset") - name = plugin.asset_name(asset, subset_name) - - # Create the new asset group as collection - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name - } - - self.set_instance_data(subset_name, instance_data, instance_node) - lib.imprint(asset_group, instance_data) - - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): selection = lib.get_selection(include_collections=True) - for data in selection: if isinstance(data, bpy.types.Collection): - asset_group.children.link(data) + instance_node.children.link(data) elif isinstance(data, bpy.types.Object): - asset_group.objects.link(data) + instance_node.objects.link(data) - return asset_group + return instance_node diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 8dbba229a9..d30c60ba21 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance +from openpype.pipeline import CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import ( AVALON_INSTANCES, @@ -19,43 +19,19 @@ class CreateCamera(plugin.BaseCreator): family = "camera" icon = "video-camera" + create_as_asset_group = True + + @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): """Run the creator on Blender main thread.""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) + asset_group = super().create(subset_name, + instance_data, + pre_create_data) - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data["asset"], subset_name) - - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - self.set_instance_data(subset_name, instance_data, instance_node) - lib.imprint(asset_group, instance_data) - - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: @@ -67,6 +43,7 @@ class CreateCamera(plugin.BaseCreator): camera = bpy.data.cameras.new(subset_name) camera_obj = bpy.data.objects.new(subset_name, camera) + instances = bpy.data.collections.get(AVALON_INSTANCES) instances.objects.link(camera_obj) camera_obj.select_set(True) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index a9bf115ea9..f569a81c29 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -19,43 +19,18 @@ class CreateLayout(plugin.BaseCreator): family = "layout" icon = "cubes" + create_as_asset_group = True + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data["asset"], subset_name) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - self.set_instance_data(subset_name, instance_data, instance_node) - lib.imprint(asset_group, instance_data) + asset_group = super().create(subset_name, + instance_data, + pre_create_data) # Add selected objects to instance - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 3501071e8f..9774d4bec3 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -19,48 +19,24 @@ class CreateModel(plugin.BaseCreator): family = "model" icon = "cube" + create_as_asset_group = True + + @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data["asset"], subset_name) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - self.set_instance_data(subset_name, instance_data, instance_node) - lib.imprint(asset_group, instance_data) + asset_group = super().create(subset_name, + instance_data, + pre_create_data) # Add selected objects to instance - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: obj.select_set(True) selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index d05b3adb04..d80a1b07f0 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -20,7 +20,7 @@ class CreatePointcache(plugin.BaseCreator): subset_name, instance_data, pre_create_data ) - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): objects = lib.get_selection() for obj in objects: collection.objects.link(obj) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 91333b7741..2d521c4255 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -21,11 +21,12 @@ class CreateReview(plugin.BaseCreator): subset_name, instance_data, pre_create_data ) - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): selected = lib.get_selection() for obj in selected: collection.objects.link(obj) elif pre_create_data.get("asset_group"): + # TODO: What is the intended behavior for this? obj = (self.options or {}).get("asset_group") collection.objects.link(obj) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index b5b61b3971..87f0124164 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -19,43 +19,18 @@ class CreateRig(plugin.BaseCreator): family = "rig" icon = "wheelchair" + create_as_asset_group = True + + @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" - self._add_instance_to_context( - CreatedInstance(self.family, subset_name, instance_data, self) - ) - - mti = ops.MainThreadItem( - self._process, subset_name, instance_data, pre_create_data - ) - ops.execute_in_main_thread(mti) - - def _process( - self, subset_name: str, instance_data: dict, pre_create_data: dict - ): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - name = plugin.asset_name(instance_data["asset"], subset_name) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = instance_node = { - "name": asset_group.name, - } - - self.set_instance_data(subset_name, instance_data, instance_node) - lib.imprint(asset_group, instance_data) + asset_group = super().create(subset_name, + instance_data, + pre_create_data) # Add selected objects to instance - if pre_create_data.get("useSelection"): + if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index d1529f75f6..8f5adb5583 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -3,8 +3,10 @@ import bpy from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name from openpype.hosts.blender.api.plugin import BaseCreator -from openpype.hosts.blender.api.lib import imprint -from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY +from openpype.hosts.blender.api.pipeline import ( + AVALON_PROPERTY, + AVALON_CONTAINERS +) class CreateWorkfile(BaseCreator, AutoCreator): @@ -53,6 +55,8 @@ class CreateWorkfile(BaseCreator, AutoCreator): current_instance = CreatedInstance( self.family, subset_name, data, self ) + instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {}) + current_instance.transient_data["instance_node"] = instance_node self._add_instance_to_context(current_instance) elif ( current_instance["asset"] != asset_name @@ -73,28 +77,30 @@ class CreateWorkfile(BaseCreator, AutoCreator): ) def collect_instances(self): - """Collect workfile instances.""" - self.cache_subsets(self.collection_shared_data) - cached_subsets = self.collection_shared_data["blender_cached_subsets"] - for node in cached_subsets.get(self.identifier, []): - created_instance = CreatedInstance.from_existing( - self.read_instance_node(node), self - ) - self._add_instance_to_context(created_instance) - def update_instances(self, update_list): - """Update workfile instances.""" - for created_inst, _changes in update_list: - data = created_inst.data_to_store() - node = data.get("instance_node") - if not node: - task_name = self.create_context.get_current_task_name() + print("Collecting!") + instance_node = bpy.data.collections.get(AVALON_CONTAINERS) + if not instance_node: + return + print(instance_node) + property = instance_node.get(AVALON_PROPERTY) + if not property: + return + print(property) - bpy.context.scene[AVALON_PROPERTY] = node = { - "name": f"workfile{task_name}" - } + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data=property.to_dict(), + creator=self + ) + instance.transient_data["instance_node"] = instance_node - created_inst["instance_node"] = node - data = created_inst.data_to_store() + # Add instance to create context + self._add_instance_to_context(instance) - imprint(node, data) + def remove_instances(self, instances): + for instance in instances: + node = instance.transient_data["instance_node"] + del node[AVALON_PROPERTY] + + self._remove_instance_from_context(instance) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index f7bbc630de..1804de29b6 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -60,6 +60,7 @@ class BlendLoader(plugin.AssetLoader): for rig in rigs: creator_plugin = get_legacy_creator_by_name("CreateAnimation") + # TODO: Refactor legacy create usage to new style creators legacy_create( creator_plugin, name=rig.name.split(':')[-1] + "_animation", diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 81683b8de8..a941c77a8e 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -123,6 +123,7 @@ class JsonLayoutLoader(plugin.AssetLoader): # raise ValueError("Creator plugin \"CreateCamera\" was " # "not found.") + # TODO: Refactor legacy create usage to new style creators # legacy_create( # creator_plugin, # name="camera", diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 48b03256ae..00faf85aed 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -73,9 +73,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): def process(self, instance): context = instance.context - render_data = bpy.data.collections[ - instance.data["instance_node"]["name"] - ].get("render_data") + instance_node = instance.data["transientData"]["instance_node"] + render_data = instance_node.get("render_data") assert render_data, "No render data found." diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 4889c66be3..2c077398da 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -16,9 +16,7 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug(f"instance: {instance}") - datablock = bpy.data.collections.get( - instance.data.get("instance_node", {}).get("name", "") - ) + datablock = instance.data["transientData"]["instance_node"] # get cameras cameras = [ diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 7669d76943..6845c29b37 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -13,7 +13,7 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): optional = False def process(self, instance): - asset_group = instance.data["instance_node"] + asset_group = instance.data["transientData"]["instance_node"] if isinstance(asset_group, bpy.types.Collection): if not (asset_group.objects or asset_group.children): From bf49ffea16a6c83c0a7ba099143b3f35ca068d85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 02:33:26 +0100 Subject: [PATCH 211/298] Hound --- openpype/hosts/blender/api/properties.py | 5 +++++ openpype/hosts/blender/plugins/create/create_blendScene.py | 7 +------ openpype/hosts/blender/plugins/create/create_camera.py | 6 +----- openpype/hosts/blender/plugins/create/create_layout.py | 7 +------ openpype/hosts/blender/plugins/create/create_model.py | 6 ------ openpype/hosts/blender/plugins/create/create_rig.py | 5 ----- 6 files changed, 8 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py index c6b5ffe011..44ca7582b9 100644 --- a/openpype/hosts/blender/api/properties.py +++ b/openpype/hosts/blender/api/properties.py @@ -1,13 +1,17 @@ import bpy from bpy.utils import register_classes_factory + class OpenpypeContext(bpy.types.PropertyGroup): pass + classes = [] # [OpenpypeContext] + factory_register, factory_unregister = register_classes_factory(classes) + def register(): """Register the properties.""" factory_register() @@ -17,6 +21,7 @@ def register(): # name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} # ) + def unregister(): """Unregister the properties.""" factory_unregister() diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index f1090ae397..c580cdfe46 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -2,12 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name -from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) +from openpype.hosts.blender.api import plugin, lib class CreateBlendScene(plugin.Creator): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index d30c60ba21..13075f718e 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,12 +2,8 @@ import bpy -from openpype.pipeline import CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateCamera(plugin.BaseCreator): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index f569a81c29..05d11bf315 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -2,12 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance -from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) +from openpype.hosts.blender.api import plugin, lib class CreateLayout(plugin.BaseCreator): diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 9774d4bec3..9f1367ebb2 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -2,13 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) - class CreateModel(plugin.BaseCreator): """Polygonal static geometry.""" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 87f0124164..c7ab9b81e1 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -2,12 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import ( - AVALON_INSTANCES, - AVALON_PROPERTY, -) class CreateRig(plugin.BaseCreator): From e67ecd4065ffa40e4b563762a969d4650300e694 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 02:34:44 +0100 Subject: [PATCH 212/298] Hound --- openpype/hosts/blender/api/pipeline.py | 2 +- openpype/hosts/blender/plugins/create/create_model.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 9ac59f5620..b9f802d221 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -10,7 +10,7 @@ from . import ops, properties import pyblish.api -from openpype.host import( +from openpype.host import ( HostBase, IWorkfileHost, IPublishHost, diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 9f1367ebb2..7047304014 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -4,6 +4,7 @@ import bpy from openpype.hosts.blender.api import plugin, lib, ops + class CreateModel(plugin.BaseCreator): """Polygonal static geometry.""" From b16bc2a80e85282855460c93906783ae46a96577 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 02:43:21 +0100 Subject: [PATCH 213/298] Remove debugging prints --- openpype/hosts/blender/plugins/create/create_workfile.py | 4 +--- openpype/hosts/blender/plugins/load/load_blend.py | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index 8f5adb5583..4dda1a31cc 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -78,15 +78,13 @@ class CreateWorkfile(BaseCreator, AutoCreator): def collect_instances(self): - print("Collecting!") instance_node = bpy.data.collections.get(AVALON_CONTAINERS) if not instance_node: return - print(instance_node) + property = instance_node.get(AVALON_PROPERTY) if not property: return - print(property) # Create instance object from existing data instance = CreatedInstance.from_existing( diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 1804de29b6..84e181cd6c 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -91,7 +91,6 @@ class BlendLoader(plugin.AssetLoader): members.append(data) container = self._get_asset_container(data_to.objects) - print(container) assert container, "No asset group found" container.name = group_name @@ -105,8 +104,6 @@ class BlendLoader(plugin.AssetLoader): print(obj) bpy.context.scene.collection.objects.link(obj) - print("") - # Remove the library from the blend file library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) From 6e89f3301470029ae996b02cb784b66ccfcd6986 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 08:50:03 +0100 Subject: [PATCH 214/298] Clarify Workfile instance data persistence --- openpype/hosts/blender/plugins/create/create_workfile.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index 4dda1a31cc..ffd423ad74 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -10,7 +10,13 @@ from openpype.hosts.blender.api.pipeline import ( class CreateWorkfile(BaseCreator, AutoCreator): - """Workfile auto-creator.""" + """Workfile auto-creator. + + The workfile instance stores its data on the `AVALON_CONTAINERS` collection + as custom attributes, because unlike other instances it doesn't have an + instance node of its own. + + """ identifier = "io.openpype.creators.blender.workfile" label = "Workfile" family = "workfile" From a2a47787db707215d3597e53cf4dc33857a96219 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 09:12:09 +0100 Subject: [PATCH 215/298] Avoid need for executing in main thread by just not using `bpy.ops` functions. I also skipped the "keep_transform" part of the functionality since it's redundant because the created asset group is always at origin by default, and thus needs no matrix inverse computations. --- .../blender/plugins/create/create_camera.py | 16 +++++----------- .../blender/plugins/create/create_layout.py | 7 ++----- .../hosts/blender/plugins/create/create_model.py | 11 +++-------- .../hosts/blender/plugins/create/create_rig.py | 10 +++------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 13075f718e..b0c0fc28db 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,7 +2,7 @@ import bpy -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -17,7 +17,6 @@ class CreateCamera(plugin.BaseCreator): create_as_asset_group = True - @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -27,13 +26,10 @@ class CreateCamera(plugin.BaseCreator): instance_data, pre_create_data) + bpy.context.view_layer.objects.active = asset_group if pre_create_data.get("use_selection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - obj.select_set(True) - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) + for obj in lib.get_selection(): + obj.parent = asset_group else: plugin.deselect_all() camera = bpy.data.cameras.new(subset_name) @@ -42,9 +38,7 @@ class CreateCamera(plugin.BaseCreator): instances = bpy.data.collections.get(AVALON_INSTANCES) instances.objects.link(camera_obj) - camera_obj.select_set(True) - asset_group.select_set(True) bpy.context.view_layer.objects.active = asset_group - bpy.ops.object.parent_set(keep_transform=True) + camera_obj.parent = asset_group return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 05d11bf315..93fb91a324 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -27,10 +27,7 @@ class CreateLayout(plugin.BaseCreator): # Add selected objects to instance if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - obj.select_set(True) - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) + for obj in lib.get_selection(): + obj.parent = asset_group return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 7047304014..8b5eaeb999 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -2,7 +2,7 @@ import bpy -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib class CreateModel(plugin.BaseCreator): @@ -16,7 +16,6 @@ class CreateModel(plugin.BaseCreator): create_as_asset_group = True - @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -27,11 +26,7 @@ class CreateModel(plugin.BaseCreator): # Add selected objects to instance if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - obj.select_set(True) - selected.append(asset_group) - - bpy.ops.object.parent_set(keep_transform=True) + for obj in lib.get_selection(): + obj.parent = asset_group return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index c7ab9b81e1..8228442249 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -2,7 +2,7 @@ import bpy -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib class CreateRig(plugin.BaseCreator): @@ -16,7 +16,6 @@ class CreateRig(plugin.BaseCreator): create_as_asset_group = True - @ops.execute_function_in_main_thread def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -27,10 +26,7 @@ class CreateRig(plugin.BaseCreator): # Add selected objects to instance if pre_create_data.get("use_selection"): bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - obj.select_set(True) - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) + for obj in lib.get_selection(): + obj.parent = asset_group return asset_group From 2053c4f4c970032e275d72535341e70254230055 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 14 Nov 2023 16:13:06 +0800 Subject: [PATCH 216/298] fix the subset name not changing acoordingly after the subset changes --- openpype/hosts/max/api/lib.py | 3 +-- openpype/hosts/max/api/plugin.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index cbaf8a0c33..0a848cb322 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -42,6 +42,7 @@ def imprint(node_name: str, data: dict) -> bool: rt.SetUserProp(node, k, f"{JSON_PREFIX}{json.dumps(v)}") else: rt.SetUserProp(node, k, v) + print(k) return True @@ -359,8 +360,6 @@ def reset_colorspace(): colorspace_mgr.Mode = rt.Name("OCIO_Custom") colorspace_mgr.OCIOConfigPath = ocio_config_path - colorspace_mgr.OCIOConfigPath = ocio_config_path - def check_colorspace(): parent = get_main_window() diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index fa6db073db..2874cfc1ce 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -204,6 +204,8 @@ class MaxCreator(Creator, MaxCreatorBase): def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = rt.GetCurrentSelection() + if rt.getNodeByName(subset_name): + raise CreatorError(f"'{subset_name}' is already created..") instance_node = self.create_instance_node(subset_name) instance_data["instance_node"] = instance_node.name @@ -246,14 +248,25 @@ class MaxCreator(Creator, MaxCreatorBase): def update_instances(self, update_list): for created_inst, changes in update_list: instance_node = created_inst.get("instance_node") - new_values = { key: changes[key].new_value for key in changes.changed_keys } + subset = new_values.get("subset", "") + if subset: + if instance_node != subset: + node = rt.getNodeByName(instance_node) + new_subset_name = new_values["subset"] + if rt.getNodeByName(new_subset_name): + raise CreatorError( + "The subset '{}' already exists.".format( + new_subset_name)) + created_inst["instance_node"] = new_values["subset"] + node.name = created_inst["instance_node"] + imprint( - instance_node, - new_values, + created_inst["instance_node"], + created_inst.data_to_store(), ) def remove_instances(self, instances): From dcfdb25217bf3c72bcad16500582c4a984ad6892 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 10:52:54 +0100 Subject: [PATCH 217/298] Implement `ILoadHost` + refactor `host.ls()` to `host.get_containers()` --- openpype/hosts/blender/api/pipeline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index b9f802d221..ee43108594 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -14,6 +14,7 @@ from openpype.host import ( HostBase, IWorkfileHost, IPublishHost, + ILoadHost ) from openpype.client import get_asset_by_name from openpype.pipeline import ( @@ -60,7 +61,7 @@ IS_HEADLESS = bpy.app.background log = Logger.get_logger(__name__) -class BlenderHost(HostBase, IWorkfileHost, IPublishHost): +class BlenderHost(HostBase, IWorkfileHost, IPublishHost, ILoadHost): name = "blender" def install(self): @@ -68,7 +69,7 @@ class BlenderHost(HostBase, IWorkfileHost, IPublishHost): Install Blender host functionality.""" install() - def ls(self) -> Iterator: + def get_containers(self) -> Iterator: """List containers from active Blender scene.""" return ls() From 28c83c3bb63085b87d861d23644bea2871e7b73b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 11:09:34 +0100 Subject: [PATCH 218/298] Remove duplicated code Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/blender/plugins/publish/extract_blend.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index 991cd040de..a1b49dcd8f 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -19,9 +19,6 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path - if not self.is_active(instance.data): - return - stagingdir = self.staging_dir(instance) filename = f"{instance.name}.blend" filepath = os.path.join(stagingdir, filename) From 6b59b718238f51ec1aef7be40b804b842adf6062 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 11:42:12 +0100 Subject: [PATCH 219/298] Re-add collector to collect instance members --- .../plugins/publish/collect_instance.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/collect_instance.py diff --git a/openpype/hosts/blender/plugins/publish/collect_instance.py b/openpype/hosts/blender/plugins/publish/collect_instance.py new file mode 100644 index 0000000000..f8fc038347 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_instance.py @@ -0,0 +1,43 @@ +import bpy + +import pyblish.api + +from openpype.pipeline.publish import KnownPublishError +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY + + +class CollectBlenderInstanceData(pyblish.api.InstancePlugin): + """Validator to verify that the instance is not empty""" + + order = pyblish.api.CollectorOrder + hosts = ["blender"] + families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"] + label = "Collect Instance" + + def process(self, instance): + instance_node = instance.data["transientData"]["instance_node"] + + # Collect members of the instance + members = [instance_node] + if isinstance(instance_node, bpy.types.Collection): + members.extend(instance_node.objects) + members.extend(instance_node.children) + + # Special case for animation instances, include armatures + # TODO: Does this still work as intended? + if instance.data["family"] == "animation": + for obj in instance_node.objects: + if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): + members.extend( + child for child in obj.children + if child.type == 'ARMATURE' + ) + elif isinstance(instance_node, bpy.types.Object): + members.extend(instance_node.children_recursive) + else: + raise KnownPublishError( + f"Unsupported instance node type '{type(instance_node)}' " + f"for instance '{instance}'" + ) + + instance[:] = members From 19b7abb8ff3def802d3cf9074cd77a20c77b12cb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 11:47:20 +0100 Subject: [PATCH 220/298] Label cosmetics --- openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index dd955dc5da..060bccbd04 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -21,7 +21,7 @@ class ValidateMeshHasUvs( order = ValidateContentsOrder hosts = ["blender"] families = ["model"] - label = "Mesh Has UV's" + label = "Mesh Has UVs" actions = [openpype.hosts.blender.api.action.SelectInvalidAction] optional = True From b8f361f75d2e7f5d4c3097ba1bd67c095b37f3b2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 11:55:14 +0100 Subject: [PATCH 221/298] Refactor Validate Instance Empty + raise PublishValidationError --- .../plugins/publish/validate_instance_empty.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 6845c29b37..51a1dcf6ca 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -1,6 +1,5 @@ -import bpy - import pyblish.api +from openpype.pipeline.publish import PublishValidationError class ValidateInstanceEmpty(pyblish.api.InstancePlugin): @@ -13,11 +12,8 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): optional = False def process(self, instance): - asset_group = instance.data["transientData"]["instance_node"] - - if isinstance(asset_group, bpy.types.Collection): - if not (asset_group.objects or asset_group.children): - raise RuntimeError(f"Instance {instance.name} is empty.") - elif isinstance(asset_group, bpy.types.Object): - if not asset_group.children: - raise RuntimeError(f"Instance {instance.name} is empty.") + # Members are collected by `collect_instance` so we only need to check + # whether any member is included. The instance node will be included + # as a member as well, hence we will check for at least 2 members + if len(instance) < 2: + raise PublishValidationError(f"Instance {instance.name} is empty.") From 3f2bd4c001a8a35dc5f5f510074b170ceda2fbc7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 11:56:08 +0100 Subject: [PATCH 222/298] Improve object name readability in reports --- .../blender/plugins/publish/validate_camera_zero_keyframe.py | 3 ++- .../plugins/publish/validate_mesh_no_negative_scale.py | 3 ++- .../blender/plugins/publish/validate_no_colons_in_name.py | 3 ++- .../hosts/blender/plugins/publish/validate_object_mode.py | 3 ++- .../hosts/blender/plugins/publish/validate_transform_zero.py | 5 +++-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index 65697cb86d..ee0a0e4dc9 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -48,6 +48,7 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: + names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( - f"Camera must have a keyframe at frame 0: {invalid}" + f"Camera must have a keyframe at frame 0: {names}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index a498a3b4cb..7f77bbe38c 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -37,6 +37,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: + names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( - f"Meshes found in instance with negative scale: {invalid}" + f"Meshes found in instance with negative scale: {names}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index 17119f8d88..caf555b535 100644 --- a/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/openpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -46,6 +46,7 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: + names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( - f"Objects found with colon in name: {invalid}" + f"Objects found with colon in name: {names}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 3b6f29a79e..ab5f4bb467 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -37,6 +37,7 @@ class ValidateObjectIsInObjectMode( invalid = self.get_invalid(instance) if invalid: + names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( - f"Object found in instance is not in Object Mode: {invalid}" + f"Object found in instance is not in Object Mode: {names}" ) diff --git a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py index 5270a50888..1fb9535ee4 100644 --- a/openpype/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/blender/plugins/publish/validate_transform_zero.py @@ -48,7 +48,8 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: + names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( - "Object found in instance has not" - f" transform to zero: {invalid}" + "Objects found in instance which do not" + f" have transform set to zero: {names}" ) From f12b4106717be573f6149f52b2419cbcaaaf274b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:01:41 +0100 Subject: [PATCH 223/298] Clean up some todos --- openpype/hosts/blender/api/ops.py | 1 - openpype/hosts/blender/api/plugin.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index c617144b6f..408e38e6cd 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -428,7 +428,6 @@ class TOPBAR_MT_avalon(bpy.types.Menu): layout.operator(SetResolution.bl_idname, text="Set Resolution") layout.separator() layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...") - # TODO (jasper): maybe add 'Reload Pipeline' def draw_avalon_menu(self, context): diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index d1cefe0a62..57fccf9299 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -430,7 +430,7 @@ class AssetLoader(LoaderPlugin): namespace: Use pre-defined namespace options: Additional settings dictionary """ - # TODO (jasper): make it possible to add the asset several times by + # TODO: make it possible to add the asset several times by # just re-using the collection filepath = self.filepath_from_context(context) assert Path(filepath).exists(), f"{filepath} doesn't exist." From 9419fe5d51a23b9378394638eb91b9212a4dc61d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:02:20 +0100 Subject: [PATCH 224/298] Clarify what `asset_group` pre_create_data intends to do, because it is not an Attribute Definition on the Creator --- openpype/hosts/blender/plugins/create/create_animation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 6f0cabb259..b3d10090e3 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -15,7 +15,6 @@ class CreateAnimation(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" # Run parent create method collection = super().create( subset_name, instance_data, pre_create_data @@ -26,7 +25,9 @@ class CreateAnimation(plugin.BaseCreator): for obj in selected: collection.objects.link(obj) elif pre_create_data.get("asset_group"): - obj = (self.options or {}).get("asset_group") + # Use for Load Blend automated creation of animation instances + # upon loading rig files + obj = pre_create_data.get("asset_group") collection.objects.link(obj) return collection From a450eab0ce67b7c2bce903b2008776f7863b401b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:02:46 +0100 Subject: [PATCH 225/298] Remove "asset_group" functionality in CreateReview, since it's unused there --- openpype/hosts/blender/plugins/create/create_review.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 2d521c4255..3dd52395df 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -15,7 +15,6 @@ class CreateReview(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" # Run parent create method collection = super().create( subset_name, instance_data, pre_create_data @@ -25,9 +24,5 @@ class CreateReview(plugin.BaseCreator): selected = lib.get_selection() for obj in selected: collection.objects.link(obj) - elif pre_create_data.get("asset_group"): - # TODO: What is the intended behavior for this? - obj = (self.options or {}).get("asset_group") - collection.objects.link(obj) return collection From 8e6c533ee6dbabd99b322336196e976806a676c0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:03:16 +0100 Subject: [PATCH 226/298] Remove redundant docstring - since `create` now isn't off-loaded to 'main thread' anymore --- openpype/hosts/blender/plugins/create/create_camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index b0c0fc28db..d624a7f05a 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -20,7 +20,6 @@ class CreateCamera(plugin.BaseCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """Run the creator on Blender main thread.""" asset_group = super().create(subset_name, instance_data, From 4d1c887ece74c6dc638bf9df250d8e39cd2e2ee9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:07:08 +0100 Subject: [PATCH 227/298] Fix missing class inheritance --- .../plugins/publish/validate_camera_zero_keyframe.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py index ee0a0e4dc9..9b6e513897 100644 --- a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -7,11 +7,13 @@ import pyblish.api import openpype.hosts.blender.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): +class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Camera must have a keyframe at frame 0. Unreal shifts the first keyframe to frame 0. Forcing the camera to have From 7acc5f9dd61a73841363a9a1000052ddc6f88e7f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:09:24 +0100 Subject: [PATCH 228/298] Collect camera instance members --- openpype/hosts/blender/plugins/publish/collect_instance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instance.py b/openpype/hosts/blender/plugins/publish/collect_instance.py index f8fc038347..b170e3d06f 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instance.py +++ b/openpype/hosts/blender/plugins/publish/collect_instance.py @@ -11,7 +11,8 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["blender"] - families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"] + families = ["model", "pointcache", "rig", "camera" "layout", "blendScene", + "camera"] label = "Collect Instance" def process(self, instance): From 53bcf0e17cc841c901a887e8c09619aacab2d4d4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 12:17:06 +0100 Subject: [PATCH 229/298] Use instance node explicitly as the 'asset group' --- openpype/hosts/blender/plugins/publish/extract_abc.py | 9 +++------ .../blender/plugins/publish/extract_abc_animation.py | 2 +- .../hosts/blender/plugins/publish/extract_camera_abc.py | 7 +------ openpype/hosts/blender/plugins/publish/extract_fbx.py | 6 ++---- .../blender/plugins/publish/extract_fbx_animation.py | 9 +-------- 5 files changed, 8 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 1602c4d266..b0cd6d0d32 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -28,18 +28,15 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): plugin.deselect_all() - selected = [] - active = None + asset_group = instance.data["transientData"]["instance_node"] + selected = [] for obj in instance: obj.select_set(True) selected.append(obj) - # Set as active the asset group - if obj.get(AVALON_PROPERTY): - active = obj context = plugin.create_blender_context( - active=active, selected=selected) + active=asset_group, selected=selected) with bpy.context.temp_override(**context): # We export the abc diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 0ed2819407..1c23fc8acb 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -32,7 +32,7 @@ class ExtractAnimationABC( plugin.deselect_all() selected = [] - asset_group = None + asset_group = instance.data["transientData"]["instance_node"] objects = [] for obj in instance: diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 9db1505d37..9d0b7f132b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -29,12 +29,7 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): plugin.deselect_all() - asset_group = None - for obj in instance: - if obj.get(AVALON_PROPERTY): - asset_group = obj - break - assert asset_group, "No asset group found" + asset_group = instance.data["transientData"]["instance_node"] # Need to cast to list because children is a tuple selected = list(asset_group.children) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index fae438158a..0ba82eca4e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -29,14 +29,12 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): plugin.deselect_all() - selected = [] - asset_group = None + asset_group = instance.data["transientData"]["instance_node"] + selected = [] for obj in instance: obj.select_set(True) selected.append(obj) - if obj.get(AVALON_PROPERTY): - asset_group = obj context = plugin.create_blender_context( active=asset_group, selected=selected) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index ba584ac99a..6fbcdc63c3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -31,14 +31,7 @@ class ExtractAnimationFBX( # Perform extraction self.log.debug("Performing extraction..") - # The first collection object in the instance is taken, as there - # should be only one that contains the asset group. - collection = [ - obj for obj in instance if type(obj) is bpy.types.Collection][0] - - # Again, the first object in the collection is taken , as there - # should be only the asset group in the collection. - asset_group = collection.objects[0] + asset_group = instance.data["transientData"]["instance_node"] armature = [ obj for obj in asset_group.children if obj.type == 'ARMATURE'][0] From 2b5c20e6e0beaf56767a000c09d09c8860d45d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 14 Nov 2023 12:49:46 +0100 Subject: [PATCH 230/298] Update openpype/hosts/fusion/plugins/create/create_saver.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/fusion/plugins/create/create_saver.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index e8ba2880a4..ecf36abdd2 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -149,13 +149,7 @@ class CreateSaver(NewCreator): # get frame padding from anatomy templates anatomy = Anatomy() - render_anatomy_template = anatomy.templates.get("render") - if render_anatomy_template: - frame_padding = int( - render_anatomy_template.get("frame_padding", 4) - ) - else: - frame_padding = int(anatomy.templates.get("frame_padding", 4)) + frame_padding = anatomy.templates["frame_padding"] # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) From b382b7c77609be4b78fbd00e0758a1e3218224b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:57:53 +0100 Subject: [PATCH 231/298] Chore: Create plugin auto-apply settings (#5908) * implemented default logic 'apply_settings' to auto-apply values * added docstring to 'apply_settings' * replace auto apply settings logic in maya * add missing quote Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/hosts/maya/api/plugin.py | 24 ++---- openpype/pipeline/create/creator_plugins.py | 85 ++++++++++++++++++++- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 07167a9a32..212c4df492 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -271,7 +271,7 @@ class MayaCreatorBase(object): @six.add_metaclass(ABCMeta) class MayaCreator(NewCreator, MayaCreatorBase): - settings_name = None + settings_category = "maya" def create(self, subset_name, instance_data, pre_create_data): @@ -317,24 +317,6 @@ class MayaCreator(NewCreator, MayaCreatorBase): default=True) ] - def apply_settings(self, project_settings): - """Method called on initialization of plugin to apply settings.""" - - settings_name = self.settings_name - if settings_name is None: - settings_name = self.__class__.__name__ - - settings = project_settings["maya"]["create"] - settings = settings.get(settings_name) - if settings is None: - self.log.debug( - "No settings found for {}".format(self.__class__.__name__) - ) - return - - for key, value in settings.items(): - setattr(self, key, value) - class MayaAutoCreator(AutoCreator, MayaCreatorBase): """Automatically triggered creator for Maya. @@ -343,6 +325,8 @@ class MayaAutoCreator(AutoCreator, MayaCreatorBase): any arguments. """ + settings_category = "maya" + def collect_instances(self): return self._default_collect_instances() @@ -360,6 +344,8 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): arguments for 'create' method. """ + settings_category = "maya" + def create(self, *args, **kwargs): return MayaCreator.create(self, *args, **kwargs) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 6aa08cae70..b51f69379c 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import copy import collections @@ -193,6 +194,12 @@ class BaseCreator: # QUESTION make this required? host_name = None + # Settings auto-apply helpers + # Root key in project settings (mandatory for auto-apply to work) + settings_category = None + # Name of plugin in create settings > class name is used if not set + settings_name = None + def __init__( self, project_settings, system_settings, create_context, headless=False ): @@ -233,14 +240,90 @@ class BaseCreator: " need to keep system settings." ).format(self.__class__.__name__)) + @staticmethod + def _get_settings_values(project_settings, category_name, plugin_name): + """Helper method to get settings values. + + Args: + project_settings (dict[str, Any]): Project settings. + category_name (str): Category of settings. + plugin_name (str): Name of settings. + + Returns: + Union[dict[str, Any], None]: Settings values or None. + """ + + settings = project_settings.get(category_name) + if not settings: + return None + + create_settings = settings.get("create") + if not create_settings: + return None + + return create_settings.get(plugin_name) + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings. + Default implementation tries to auto-apply settings values if are + in expected hierarchy. + + Data hierarchy to auto-apply settings: + ├─ {self.settings_category} - Root key in settings + │ └─ "create" - Hardcoded key + │ └─ {self.settings_name} | {class name} - Name of plugin + │ ├─ ... attribute values... - Attribute/value pair + + It is mandatory to define 'settings_category' attribute. Attribute + 'settings_name' is optional and class name is used if is not defined. + + Example data: + ProjectSettings { + "maya": { # self.settings_category + "create": { # Hardcoded key + "CreateAnimation": { # self.settings_name / class name + "enabled": True, # --- Attributes to set --- + "optional": True,# + "active": True, # + "fps": 25, # ------------------------- + }, + ... + }, + ... + }, + ... + } + Args: project_settings (dict[str, Any]): Project settings. """ - pass + settings_category = self.settings_category + if not settings_category: + return + + cls_name = self.__class__.__name__ + settings_name = self.settings_name or cls_name + + settings = self._get_settings_values( + project_settings, settings_category, settings_name + ) + if settings is None: + self.log.debug("No settings found for {}".format(cls_name)) + return + + for key, value in settings.items(): + # Log out attributes that are not defined on plugin object + # - those may be potential dangerous typos in settings + if not hasattr(self, key): + self.log.debug(( + "Applying settings to unknown attribute '{}' on '{}'." + ).format( + key, cls_name + )) + setattr(self, key, value) + @property def identifier(self): From 162394a56cfd3f62756603dd821596c478191747 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:17:55 +0100 Subject: [PATCH 232/298] Fix typo, missing comment *facepalm* --- openpype/hosts/blender/plugins/publish/collect_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instance.py b/openpype/hosts/blender/plugins/publish/collect_instance.py index b170e3d06f..72c8f13362 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instance.py +++ b/openpype/hosts/blender/plugins/publish/collect_instance.py @@ -11,7 +11,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["blender"] - families = ["model", "pointcache", "rig", "camera" "layout", "blendScene", + families = ["model", "pointcache", "rig", "camera", "layout", "blendScene", "camera"] label = "Collect Instance" From 8d57f5f3ef7224d4bae5920ff1e82bde93340f2b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:23:45 +0100 Subject: [PATCH 233/298] Fix extract layout --- .../hosts/blender/plugins/publish/extract_layout.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index d0adac6edb..7e8ca7a250 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -128,13 +128,22 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): json_data = [] fbx_files = [] - asset_group = bpy.data.objects[str(instance)] + asset_group = instance.data["transientData"]["instance_node"] fbx_count = 0 project_name = instance.context.data["projectEntity"]["name"] for asset in asset_group.children: metadata = asset.get(AVALON_PROPERTY) + if not metadata: + # Avoid erroring directly if there's just invalid data + # inside the instance + # TODO: This should actually be validated in a validator + self.log.warning( + f"Found content in layout that is not a loaded " + f"asset, skipping: {asset.name_full}" + ) + continue version_id = metadata["parent"] family = metadata["family"] From d1aaefab9c916da3e2ad94a77792489c1300f60b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:24:20 +0100 Subject: [PATCH 234/298] Improve comment grammar --- openpype/hosts/blender/plugins/publish/extract_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 7e8ca7a250..73d92961bc 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -136,8 +136,8 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): for asset in asset_group.children: metadata = asset.get(AVALON_PROPERTY) if not metadata: - # Avoid erroring directly if there's just invalid data - # inside the instance + # Avoid raising error directly if there's just invalid data + # inside the instance; better to log it to the artist # TODO: This should actually be validated in a validator self.log.warning( f"Found content in layout that is not a loaded " From fa5638d06bb73d4f327d3de120718b72d70c6b9f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:33:59 +0100 Subject: [PATCH 235/298] Fix parent class --- openpype/hosts/blender/plugins/create/create_blendScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index c580cdfe46..bda026286d 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -5,7 +5,7 @@ import bpy from openpype.hosts.blender.api import plugin, lib -class CreateBlendScene(plugin.Creator): +class CreateBlendScene(plugin.BaseCreator): """Generic group of assets.""" name = "blendScene" From 0a714778736118d78c6f5c7a3660c7828de26d26 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:42:29 +0100 Subject: [PATCH 236/298] Allow to fall back to a window from the window manager --- openpype/hosts/blender/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 5d8c93dc49..e80ed61bc8 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -278,9 +278,11 @@ def get_selected_collections(): list: A list of `bpy.types.Collection` objects that are currently selected in the outliner. """ + window = bpy.context.window or bpy.context.window_manager.windows[0] + try: area = next( - area for area in bpy.context.window.screen.areas + area for area in window.screen.areas if area.type == 'OUTLINER') region = next( region for region in area.regions @@ -290,10 +292,10 @@ def get_selected_collections(): "must be in the main Blender window.") from e with bpy.context.temp_override( - window=bpy.context.window, + window=window, area=area, region=region, - screen=bpy.context.window.screen + screen=window.screen ): ids = bpy.context.selected_ids From 63ff830d12cdfaf8ed4cee4e681f5c5e650cc76a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:50:07 +0100 Subject: [PATCH 237/298] Ignore collections in instance, only select objects --- openpype/hosts/blender/plugins/publish/extract_abc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index b0cd6d0d32..415cbb186c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -32,8 +32,9 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): selected = [] for obj in instance: - obj.select_set(True) - selected.append(obj) + if isinstance(obj, bpy.types.Object): + obj.select_set(True) + selected.append(obj) context = plugin.create_blender_context( active=asset_group, selected=selected) From 00d5b90568e6a35df49abe3bc8129a4cd6ab7a4a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 15:50:23 +0100 Subject: [PATCH 238/298] Remove unused import --- openpype/hosts/blender/plugins/publish/extract_abc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 415cbb186c..12d062d925 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -4,7 +4,6 @@ import bpy from openpype.pipeline import publish from openpype.hosts.blender.api import plugin -from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): From f281da5784f79cacda9730c5b211a77e48bfeb2b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 16:01:30 +0100 Subject: [PATCH 239/298] Fix missing identifier --- openpype/hosts/blender/plugins/create/create_blendScene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index bda026286d..c09261705f 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -8,6 +8,7 @@ from openpype.hosts.blender.api import plugin, lib class CreateBlendScene(plugin.BaseCreator): """Generic group of assets.""" + identifier = "io.openpype.creators.blender.blendscene" name = "blendScene" label = "Blender Scene" family = "blendScene" From 0556986a3e39c45be8c923f18a87981c17163c3a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 16:40:29 +0100 Subject: [PATCH 240/298] Remove redundant data --- openpype/hosts/blender/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 57fccf9299..5a0026d862 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -325,8 +325,6 @@ class BaseCreator(Creator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": subset_name, - "task": get_current_task_name(), "subset": subset_name, } ) From 9d6352c68dc69383534ab4a0757751a7829d7a01 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 16:43:04 +0100 Subject: [PATCH 241/298] Remove `name` attributes on Creator that are irrelevant to new-style creators --- openpype/hosts/blender/plugins/create/create_action.py | 1 - openpype/hosts/blender/plugins/create/create_animation.py | 1 - openpype/hosts/blender/plugins/create/create_blendScene.py | 1 - openpype/hosts/blender/plugins/create/create_camera.py | 1 - openpype/hosts/blender/plugins/create/create_layout.py | 1 - openpype/hosts/blender/plugins/create/create_model.py | 1 - openpype/hosts/blender/plugins/create/create_pointcache.py | 1 - openpype/hosts/blender/plugins/create/create_render.py | 1 - openpype/hosts/blender/plugins/create/create_review.py | 1 - openpype/hosts/blender/plugins/create/create_rig.py | 1 - 10 files changed, 10 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index f14023639f..0929778d78 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -9,7 +9,6 @@ class CreateAction(plugin.BaseCreator): """Action output for character rigs.""" identifier = "io.openpype.creators.blender.action" - name = "actionMain" label = "Action" family = "action" icon = "male" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index b3d10090e3..3a91b2d5ff 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -7,7 +7,6 @@ class CreateAnimation(plugin.BaseCreator): """Animation output for character rigs.""" identifier = "io.openpype.creators.blender.animation" - name = "animationMain" label = "Animation" family = "animation" icon = "male" diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index c09261705f..e1026282c0 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -9,7 +9,6 @@ class CreateBlendScene(plugin.BaseCreator): """Generic group of assets.""" identifier = "io.openpype.creators.blender.blendscene" - name = "blendScene" label = "Blender Scene" family = "blendScene" icon = "cubes" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index d624a7f05a..2e2e6cec22 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -10,7 +10,6 @@ class CreateCamera(plugin.BaseCreator): """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.camera" - name = "cameraMain" label = "Camera" family = "camera" icon = "video-camera" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 93fb91a324..16d227e50e 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -9,7 +9,6 @@ class CreateLayout(plugin.BaseCreator): """Layout output for character rigs.""" identifier = "io.openpype.creators.blender.layout" - name = "layoutMain" label = "Layout" family = "layout" icon = "cubes" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 8b5eaeb999..2f3f61728b 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -9,7 +9,6 @@ class CreateModel(plugin.BaseCreator): """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.model" - name = "modelMain" label = "Model" family = "model" icon = "cube" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index d80a1b07f0..b3329bcb3b 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -7,7 +7,6 @@ class CreatePointcache(plugin.BaseCreator): """Polygonal static geometry.""" identifier = "io.openpype.creators.blender.pointcache" - name = "pointcacheMain" label = "Point Cache" family = "pointcache" icon = "gears" diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index bffa5696df..7fb3e5eb00 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -9,7 +9,6 @@ class CreateRenderlayer(plugin.BaseCreator): """Single baked camera.""" identifier = "io.openpype.creators.blender.render" - name = "renderingMain" label = "Render" family = "render" icon = "eye" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 3dd52395df..940bcbea22 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -7,7 +7,6 @@ class CreateReview(plugin.BaseCreator): """Single baked camera.""" identifier = "io.openpype.creators.blender.review" - name = "reviewDefault" label = "Review" family = "review" icon = "video-camera" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 8228442249..d63b8d56ff 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -9,7 +9,6 @@ class CreateRig(plugin.BaseCreator): """Artist-friendly rig with controls to direct motion.""" identifier = "io.openpype.creators.blender.rig" - name = "rigMain" label = "Rig" family = "rig" icon = "wheelchair" From d077b7526f289a992bb89f53e589f8909950e556 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 16:57:25 +0100 Subject: [PATCH 242/298] Add legacy instance conversion --- .../blender/plugins/create/convert_legacy.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 openpype/hosts/blender/plugins/create/convert_legacy.py diff --git a/openpype/hosts/blender/plugins/create/convert_legacy.py b/openpype/hosts/blender/plugins/create/convert_legacy.py new file mode 100644 index 0000000000..f05a6b1f5a --- /dev/null +++ b/openpype/hosts/blender/plugins/create/convert_legacy.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Converter for legacy Houdini subsets.""" +from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin +from openpype.hosts.blender.api.lib import imprint + + +class BlenderLegacyConvertor(SubsetConvertorPlugin): + """Find and convert any legacy subsets in the scene. + + This Converter will find all legacy subsets in the scene and will + transform them to the current system. Since the old subsets doesn't + retain any information about their original creators, the only mapping + we can do is based on their families. + + Its limitation is that you can have multiple creators creating subset + of the same family and there is no way to handle it. This code should + nevertheless cover all creators that came with OpenPype. + + """ + identifier = "io.openpype.creators.blender.legacy" + family_to_id = { + "action": "io.openpype.creators.blender.action", + "camera": "io.openpype.creators.blender.camera", + "animation": "io.openpype.creators.blender.animation", + "blendScene": "io.openpype.creators.blender.blendscene", + "layout": "io.openpype.creators.blender.layout", + "model": "io.openpype.creators.blender.model", + "pointcache": "io.openpype.creators.blender.pointcache", + "render": "io.openpype.creators.blender.render", + "review": "io.openpype.creators.blender.review", + "rig": "io.openpype.creators.blender.rig", + } + + def __init__(self, *args, **kwargs): + super(BlenderLegacyConvertor, self).__init__(*args, **kwargs) + self.legacy_subsets = {} + + def find_instances(self): + """Find legacy subsets in the scene. + + Legacy subsets are the ones that doesn't have `creator_identifier` + parameter on them. + + This is using cached entries done in + :py:meth:`~BaseCreator.cache_subsets()` + + """ + self.legacy_subsets = self.collection_shared_data.get( + "blender_cached_legacy_subsets") + if not self.legacy_subsets: + return + self.add_convertor_item( + "Found {} incompatible subset{}".format( + len(self.legacy_subsets), + "s" if len(self.legacy_subsets) > 1 else "" + ) + ) + + def convert(self): + """Convert all legacy subsets to current. + + It is enough to add `creator_identifier` and `instance_node`. + + """ + if not self.legacy_subsets: + return + + for family, instance_nodes in self.legacy_subsets.items(): + if family in self.family_to_id: + for instance_node in instance_nodes: + creator_identifier = self.family_to_id[family] + self.log.info( + "Converting {} to {}".format(instance_node.name, + creator_identifier) + ) + imprint(instance_node, data={ + "creator_identifier": creator_identifier + }) From 1fbe1b3163aa296bb9a007a3c95e61b24b55306d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:11:46 +0100 Subject: [PATCH 243/298] Refactor load layout to create new-style creator animation instances --- .../hosts/blender/plugins/load/load_blend.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 84e181cd6c..8b1af5a0da 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -4,11 +4,11 @@ from pathlib import Path import bpy from openpype.pipeline import ( - legacy_create, get_representation_path, AVALON_CONTAINER_ID, + registered_host ) -from openpype.pipeline.create import get_legacy_creator_by_name +from openpype.pipeline.create import CreateContext from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.lib import imprint from openpype.hosts.blender.api.pipeline import ( @@ -57,20 +57,21 @@ class BlendLoader(plugin.AssetLoader): obj.get(AVALON_PROPERTY).get('family') == 'rig' ) ] + if not rigs: + return + + # Create animation instances for each rig + creator_identifier = "io.openpype.creators.blender.animation" + host = registered_host() + create_context = CreateContext(host) for rig in rigs: - creator_plugin = get_legacy_creator_by_name("CreateAnimation") - # TODO: Refactor legacy create usage to new style creators - legacy_create( - creator_plugin, - name=rig.name.split(':')[-1] + "_animation", - asset=asset, - options={ - "useSelection": False, + create_context.create( + creator_identifier=creator_identifier, + variant=rig.name.split(':')[-1], + pre_create_data={ + "use_selection": False, "asset_group": rig - }, - data={ - "dependencies": representation } ) From b04ae30b821c8a26d68eedf62864100e2fd04989 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:13:46 +0100 Subject: [PATCH 244/298] Collect animation members --- openpype/hosts/blender/plugins/publish/collect_instance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instance.py b/openpype/hosts/blender/plugins/publish/collect_instance.py index 72c8f13362..3d4e634e9e 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instance.py +++ b/openpype/hosts/blender/plugins/publish/collect_instance.py @@ -11,8 +11,8 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["blender"] - families = ["model", "pointcache", "rig", "camera", "layout", "blendScene", - "camera"] + families = ["model", "pointcache", "animation", "rig", "camera", "layout", + "blendScene"] label = "Collect Instance" def process(self, instance): From d1c3eb83cfe1d7bb485d88914b42cdc0640aba68 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:17:35 +0100 Subject: [PATCH 245/298] Remove todo --- openpype/hosts/blender/plugins/publish/collect_instance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instance.py b/openpype/hosts/blender/plugins/publish/collect_instance.py index 3d4e634e9e..4685472213 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instance.py +++ b/openpype/hosts/blender/plugins/publish/collect_instance.py @@ -25,7 +25,6 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): members.extend(instance_node.children) # Special case for animation instances, include armatures - # TODO: Does this still work as intended? if instance.data["family"] == "animation": for obj in instance_node.objects: if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): From 78bcd7ce74a67299c3a389209210f200a22e3654 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:37:53 +0100 Subject: [PATCH 246/298] Pull window to front when menu entries are clicked; no more searching for minimized windows --- openpype/hosts/blender/api/ops.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 408e38e6cd..c1403d44b6 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -246,8 +246,24 @@ class LaunchQtApp(bpy.types.Operator): self.before_window_show() + def pull_to_front(window): + """Pull window forward to screen. + + If Window is minimized this will un-minimize, then it can be raised + and activated to the front. + """ + window.setWindowState( + (window.windowState() & ~QtCore.Qt.WindowMinimized) | + QtCore.Qt.WindowActive + ) + window.raise_() + window.activateWindow() + if isinstance(self._window, ModuleType): self._window.show() + pull_to_front(self.window) + + # Pull window to the front window = None if hasattr(self._window, "window"): window = self._window.window @@ -262,6 +278,7 @@ class LaunchQtApp(bpy.types.Operator): on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint self._window.setWindowFlags(on_top_flags) self._window.show() + pull_to_front(self.window) # if on_top_flags != origin_flags: # self._window.setWindowFlags(origin_flags) From d42d57133b4981b6c9780ee0e2167daf3be42308 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:39:22 +0100 Subject: [PATCH 247/298] Fix typo --- openpype/hosts/blender/api/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index c1403d44b6..f4d96e563a 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -261,7 +261,7 @@ class LaunchQtApp(bpy.types.Operator): if isinstance(self._window, ModuleType): self._window.show() - pull_to_front(self.window) + pull_to_front(self._window) # Pull window to the front window = None @@ -278,7 +278,7 @@ class LaunchQtApp(bpy.types.Operator): on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint self._window.setWindowFlags(on_top_flags) self._window.show() - pull_to_front(self.window) + pull_to_front(self._window) # if on_top_flags != origin_flags: # self._window.setWindowFlags(origin_flags) From 6398dbf1c46dd3dd921995886703a64146a0cad3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:49:13 +0100 Subject: [PATCH 248/298] Remove unused properties --- openpype/hosts/blender/api/pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index ee43108594..b386dd49d3 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -6,7 +6,7 @@ from typing import Callable, Dict, Iterator, List, Optional import bpy from . import lib -from . import ops, properties +from . import ops import pyblish.api @@ -181,7 +181,6 @@ def install(): if not IS_HEADLESS: ops.register() - properties.register() def uninstall(): @@ -196,7 +195,6 @@ def uninstall(): if not IS_HEADLESS: ops.unregister() - properties.unregister() def show_message(title, message): From 31f24f8cc2436eda1930c6affcd92d376d8b13cf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 17:50:03 +0100 Subject: [PATCH 249/298] Remove unused properties lib --- openpype/hosts/blender/api/properties.py | 29 ------------------------ 1 file changed, 29 deletions(-) delete mode 100644 openpype/hosts/blender/api/properties.py diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py deleted file mode 100644 index 44ca7582b9..0000000000 --- a/openpype/hosts/blender/api/properties.py +++ /dev/null @@ -1,29 +0,0 @@ -import bpy -from bpy.utils import register_classes_factory - - -class OpenpypeContext(bpy.types.PropertyGroup): - pass - - -classes = [] # [OpenpypeContext] - - -factory_register, factory_unregister = register_classes_factory(classes) - - -def register(): - """Register the properties.""" - factory_register() - - bpy.types.Scene.openpype_context = {} - # bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( - # name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} - # ) - - -def unregister(): - """Unregister the properties.""" - factory_unregister() - - del bpy.types.Scene.openpype_context From 45699444ef8d4e466d3fed4652d26b4d1c55033a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 18:03:08 +0100 Subject: [PATCH 250/298] Fix setting data - `current_instance` is not a `dict` --- .../hosts/blender/plugins/create/create_workfile.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index ffd423ad74..28c21c5811 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -74,13 +74,9 @@ class CreateWorkfile(BaseCreator, AutoCreator): task_name, task_name, asset_doc, project_name, host_name ) - current_instance.update( - { - "asset": asset_name, - "task": task_name, - "subset": subset_name, - } - ) + current_instance["asset"] = asset_name + current_instance["task"] = task_name + current_instance["subset"] = subset_name def collect_instances(self): From 478afb7f4899189dab0e3a99b0c83dd8e01f7d67 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 18:04:57 +0100 Subject: [PATCH 251/298] Cosmetics; match code of other hosts more --- openpype/hosts/blender/plugins/create/create_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index 28c21c5811..9245434766 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -73,7 +73,6 @@ class CreateWorkfile(BaseCreator, AutoCreator): subset_name = self.get_subset_name( task_name, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name From d148671d02563ce11413e7cb4fd010f9fb3e41f1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Nov 2023 20:56:00 +0100 Subject: [PATCH 252/298] Fix finding ARMATURE objects in asset group Collection --- .../plugins/publish/extract_fbx_animation.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 6fbcdc63c3..712fbb2d14 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -33,8 +33,27 @@ class ExtractAnimationFBX( asset_group = instance.data["transientData"]["instance_node"] - armature = [ - obj for obj in asset_group.children if obj.type == 'ARMATURE'][0] + # Get objects in this collection (but not in children collections) + # and for those objects include the children hierarchy + # TODO: Would it make more sense for the Collect Instance collector + # to also always retrieve all the children? + objects = set(asset_group.objects) + for obj in list(objects): + objects.update(obj.children_recursive) + + # Find all armatures among the objects, assume to find only one + armatures = [obj for obj in objects if obj.type == "ARMATURE"] + if not armatures: + raise RuntimeError( + f"Unable to find ARMATURE in collection: " + f"{asset_group.name}" + ) + elif len(armatures) > 1: + self.log.warning( + "Found more than one ARMATURE, using " + f"only first of: {armatures}" + ) + armature = armatures[0] object_action_pairs = [] original_actions = [] @@ -43,9 +62,6 @@ class ExtractAnimationFBX( ending_frames = [] # For each armature, we make a copy of the current action - curr_action = None - copy_action = None - if armature.animation_data and armature.animation_data.action: curr_action = armature.animation_data.action copy_action = curr_action.copy() @@ -55,7 +71,10 @@ class ExtractAnimationFBX( starting_frames.append(curr_frame_range[0]) ending_frames.append(curr_frame_range[1]) else: - self.log.info("Object has no animation.") + self.log.info( + f"Armature '{armature.name}' has no animation, " + f"skipping FBX animation extraction for {instance}." + ) return asset_group_name = asset_group.name From 8fe8154fec7a74bc7b039457c9cf9e7c68c88eab Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 15 Nov 2023 03:25:41 +0000 Subject: [PATCH 253/298] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 611fdc82ce..b7394c203d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.6-nightly.2" +__version__ = "3.17.6-nightly.3" From 72b1e759e7392fcc8f83b4f73ba3f7c56376eb8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Nov 2023 03:26:29 +0000 Subject: [PATCH 254/298] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e377773007..86e3638ffe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.6-nightly.3 - 3.17.6-nightly.2 - 3.17.6-nightly.1 - 3.17.5 @@ -134,7 +135,6 @@ body: - 3.15.2-nightly.2 - 3.15.2-nightly.1 - 3.15.1 - - 3.15.1-nightly.6 validations: required: true - type: dropdown From 68c3ef37ef358ac71d80c993392e6bbe865ee2f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 Nov 2023 14:43:39 +0800 Subject: [PATCH 255/298] code tweaks --- openpype/hosts/max/api/plugin.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 2874cfc1ce..2cf0d69146 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -253,19 +253,19 @@ class MaxCreator(Creator, MaxCreatorBase): for key in changes.changed_keys } subset = new_values.get("subset", "") - if subset: - if instance_node != subset: - node = rt.getNodeByName(instance_node) - new_subset_name = new_values["subset"] - if rt.getNodeByName(new_subset_name): - raise CreatorError( - "The subset '{}' already exists.".format( - new_subset_name)) - created_inst["instance_node"] = new_values["subset"] - node.name = created_inst["instance_node"] + if subset and instance_node != subset: + node = rt.getNodeByName(instance_node) + new_subset_name = new_values["subset"] + if rt.getNodeByName(new_subset_name): + raise CreatorError( + "The subset '{}' already exists.".format( + new_subset_name)) + instance_node = new_subset_name + created_inst["instance_node"] = instance_node + node.name = instance_node imprint( - created_inst["instance_node"], + instance_node, created_inst.data_to_store(), ) From ca21655c18d7163cfe77ec74a26fe86af2817ad4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 Nov 2023 17:02:37 +0800 Subject: [PATCH 256/298] remove print debug function --- openpype/hosts/max/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 0a848cb322..298084a4e8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -42,7 +42,6 @@ def imprint(node_name: str, data: dict) -> bool: rt.SetUserProp(node, k, f"{JSON_PREFIX}{json.dumps(v)}") else: rt.SetUserProp(node, k, v) - print(k) return True From 91e230a321d6946b612fb9476f8dbe56e179c14b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 10:09:37 +0100 Subject: [PATCH 257/298] Enhancement: Some publish logs cosmetics (#5917) * Write logs in sorted order for better logs * Typo in log message * More descriptive log * Skip logging profile since `filter_profiles` already logs "Profile selected: {profile}" * Fix grammar * Fix grammar * Improve logged information --- .../plugins/load/load_reference_image.py | 2 +- .../action_tranfer_hierarchical_values.py | 2 +- .../event_next_task_update.py | 2 +- .../event_sync_to_avalon.py | 2 +- .../action_component_open.py | 2 +- .../action_delete_asset.py | 2 +- .../event_handlers_user/action_delivery.py | 2 +- .../event_handlers_user/action_job_killer.py | 2 +- openpype/plugins/publish/cleanup_explicit.py | 26 +++++++++++-------- .../plugins/publish/collect_rendered_files.py | 5 ++-- openpype/plugins/publish/extract_burnin.py | 4 +-- .../publish/extract_color_transcode.py | 1 - openpype/plugins/publish/extract_review.py | 2 +- .../plugins/publish/integrate_thumbnail.py | 2 +- .../publish/integrate_thumbnail_ayon.py | 2 +- .../settings/entities/dict_conditional.py | 2 +- .../entities/dict_immutable_keys_entity.py | 2 +- .../entities/dict_mutable_keys_entity.py | 2 +- openpype/settings/entities/item_entities.py | 2 +- openpype/settings/entities/list_entity.py | 2 +- openpype/settings/entities/root_entities.py | 2 +- 21 files changed, 36 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 3707ef97aa..53061c6885 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -190,7 +190,7 @@ class LoadImage(plugin.Loader): if pop_idx is None: self.log.warning( - "Didn't found container in workfile containers. {}".format( + "Didn't find container in workfile containers. {}".format( container ) ) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index f6899843a3..1d73318f6e 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -66,7 +66,7 @@ class TransferHierarchicalValues(ServerAction): "items": [{ "type": "label", "value": ( - "Didn't found custom attributes" + "Didn't find custom attributes" " that can be transferred." ) }] diff --git a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py index 07a8ff433e..8632f038b8 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py +++ b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py @@ -257,7 +257,7 @@ class NextTaskUpdate(BaseEvent): new_task_name = mapping.get(old_status_name) if not new_task_name: self.log.debug( - "Didn't found mapping for status \"{}\".".format( + "Didn't find mapping for status \"{}\".".format( task_status["name"] ) ) diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 0aa0b9f9f5..d4dc53b655 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -387,7 +387,7 @@ class SyncToAvalonEvent(BaseEvent): if not data: # TODO logging self.log.warning( - "Didn't found entity by key/value \"{}\" / \"{}\"".format( + "Didn't find entity by key/value \"{}\" / \"{}\"".format( key, value ) ) diff --git a/openpype/modules/ftrack/event_handlers_user/action_component_open.py b/openpype/modules/ftrack/event_handlers_user/action_component_open.py index c731713c10..0efade9d8f 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_component_open.py +++ b/openpype/modules/ftrack/event_handlers_user/action_component_open.py @@ -51,7 +51,7 @@ class ComponentOpen(BaseAction): else: return { 'success': False, - 'message': "Didn't found file: " + fpath + 'message': "Didn't find file: " + fpath } return { diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py index 72a5efbcfe..e1df8e1537 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py @@ -169,7 +169,7 @@ class DeleteAssetSubset(BaseAction): return { "success": True, "message": ( - "Didn't found entities in avalon." + "Didn't find entities in avalon." " You can use Ftrack's Delete button for the selection." ) } diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index 559de3a24d..c198389b98 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -61,7 +61,7 @@ class Delivery(BaseAction): return { "success": False, "message": ( - "Didn't found project \"{}\" in avalon." + "Didn't find project \"{}\" in avalon." ).format(project_name) } diff --git a/openpype/modules/ftrack/event_handlers_user/action_job_killer.py b/openpype/modules/ftrack/event_handlers_user/action_job_killer.py index dd68c75f84..250670f016 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_job_killer.py +++ b/openpype/modules/ftrack/event_handlers_user/action_job_killer.py @@ -29,7 +29,7 @@ class JobKiller(BaseAction): if not jobs: return { "success": True, - "message": "Didn't found any running jobs" + "message": "Didn't find any running jobs" } # Collect user ids from jobs diff --git a/openpype/plugins/publish/cleanup_explicit.py b/openpype/plugins/publish/cleanup_explicit.py index 983c9223c6..cc6b99e8f8 100644 --- a/openpype/plugins/publish/cleanup_explicit.py +++ b/openpype/plugins/publish/cleanup_explicit.py @@ -58,21 +58,21 @@ class ExplicitCleanUp(pyblish.api.ContextPlugin): # Store failed paths with exception failed = [] # Store removed filepaths for logging - succeded_files = set() + succeeded_files = set() # Remove file by file for filepath in filepaths: try: os.remove(filepath) - succeded_files.add(filepath) + succeeded_files.add(filepath) except Exception as exc: failed.append((filepath, exc)) - if succeded_files: + if succeeded_files: self.log.info( - "Removed files:\n{}".format("\n".join(succeded_files)) + "Removed files:\n{}".format("\n".join(sorted(succeeded_files))) ) - # Delete folders with it's content + # Delete folders with its content succeeded = set() for dirpath in dirpaths: # Check if directory still exists @@ -87,17 +87,21 @@ class ExplicitCleanUp(pyblish.api.ContextPlugin): if succeeded: self.log.info( - "Removed directories:\n{}".format("\n".join(succeeded)) + "Removed directories:\n{}".format( + "\n".join(sorted(succeeded)) + ) ) - # Prepare lines for report of failed removements + # Prepare lines for report of failed removals lines = [] for filepath, exc in failed: lines.append("{}: {}".format(filepath, str(exc))) if lines: self.log.warning( - "Failed to remove filepaths:\n{}".format("\n".join(lines)) + "Failed to remove filepaths:\n{}".format( + "\n".join(sorted(lines)) + ) ) def _remove_empty_dirs(self, empty_dirpaths): @@ -134,8 +138,8 @@ class ExplicitCleanUp(pyblish.api.ContextPlugin): if to_skip_dirpaths: self.log.debug( - "Skipped directories because contain files:\n{}".format( - "\n".join(to_skip_dirpaths) + "Skipped directories because they contain files:\n{}".format( + "\n".join(sorted(to_skip_dirpaths)) ) ) @@ -147,6 +151,6 @@ class ExplicitCleanUp(pyblish.api.ContextPlugin): if to_delete_dirpaths: self.log.debug( "Deleted empty directories:\n{}".format( - "\n".join(to_delete_dirpaths) + "\n".join(sorted(to_delete_dirpaths)) ) ) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index a249b3acda..6160b4f5c8 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -54,6 +54,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): staging_dir = data_object.get("stagingDir") if staging_dir: data_object["stagingDir"] = anatomy.fill_root(staging_dir) + self.log.debug("Filling stagingDir with root to: %s", + data_object["stagingDir"]) def _process_path(self, data, anatomy): """Process data of a single JSON publish metadata file. @@ -108,7 +110,6 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): instance = self._context.create_instance( instance_data.get("subset") ) - self.log.debug("Filling stagingDir...") self._fill_staging_dir(instance_data, anatomy) instance.data.update(instance_data) @@ -161,7 +162,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): anatomy.project_name )) - self.log.debug("anatomy: {}".format(anatomy.roots)) + self.log.debug("Anatomy roots: {}".format(anatomy.roots)) try: session_is_set = False for path in paths: diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index dc8aab6ce4..9a978ed286 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -171,8 +171,6 @@ class ExtractBurnin(publish.Extractor): ).format(host_name, family, task_name, task_type, subset)) return - self.log.debug("profile: {}".format(profile)) - # Pre-filter burnin definitions by instance families burnin_defs = self.filter_burnins_defs(profile, instance) if not burnin_defs: @@ -450,7 +448,7 @@ class ExtractBurnin(publish.Extractor): filling burnin strings. `temp_data` are for repre pre-process preparation. """ - self.log.debug("Prepring basic data for burnins") + self.log.debug("Preparing basic data for burnins") context = instance.context version = instance.data.get("version") diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index dbf1b6c8a6..faacb7af2e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -326,7 +326,6 @@ class ExtractOIIOTranscode(publish.Extractor): " | Task type \"{}\" | Subset \"{}\" " ).format(host_name, family, task_name, task_type, subset)) - self.log.debug("profile: {}".format(profile)) return profile def _repre_is_valid(self, repre): diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index db8a030dfa..cd0f78530a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -143,7 +143,7 @@ class ExtractReview(pyblish.api.InstancePlugin): custom_tags = repre.get("custom_tags") if "review" not in tags: self.log.debug(( - "Repre: {} - Didn't found \"review\" in tags. Skipping" + "Repre: {} - Didn't find \"review\" in tags. Skipping" ).format(repre_name)) continue diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 0c12255d38..b154940469 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -200,7 +200,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): if thumb_repre_doc is None: self.log.debug( - "There is not representation with name \"thumbnail\"" + "There is no representation with name \"thumbnail\"" ) return None diff --git a/openpype/plugins/publish/integrate_thumbnail_ayon.py b/openpype/plugins/publish/integrate_thumbnail_ayon.py index cf05327ce8..f9b48eebec 100644 --- a/openpype/plugins/publish/integrate_thumbnail_ayon.py +++ b/openpype/plugins/publish/integrate_thumbnail_ayon.py @@ -137,7 +137,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): if thumb_repre_doc is None: self.log.debug( - "There is not representation with name \"thumbnail\"" + "There is no representation with name \"thumbnail\"" ) return None diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 88d2dc8296..f26d86e6df 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -352,7 +352,7 @@ class DictConditionalEntity(ItemEntity): break if result_key is None: - raise ValueError("Didn't found child {}".format(child_obj)) + raise ValueError("Didn't find child {}".format(child_obj)) return "/".join([self.path, result_key]) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 0209681e95..a25c22aa19 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -232,7 +232,7 @@ class DictImmutableKeysEntity(ItemEntity): break if result_key is None: - raise ValueError("Didn't found child {}".format(child_obj)) + raise ValueError("Didn't find child {}".format(child_obj)) return "/".join([self.path, result_key]) diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index e6d332b9ad..c11a7cf059 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -284,7 +284,7 @@ class DictMutableKeysEntity(EndpointEntity): break if result_key is None: - raise ValueError("Didn't found child {}".format(child_obj)) + raise ValueError("Didn't find child {}".format(child_obj)) return "/".join([self.path, result_key]) diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 3b756e4ede..c888cf3b78 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -295,7 +295,7 @@ class ListStrictEntity(ItemEntity): break if result_idx is None: - raise ValueError("Didn't found child {}".format(child_obj)) + raise ValueError("Didn't find child {}".format(child_obj)) return "/".join([self.path, str(result_idx)]) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 5d6a64b3ea..d9a18e0177 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -258,7 +258,7 @@ class ListEntity(EndpointEntity): break if result_idx is None: - raise ValueError("Didn't found child {}".format(child_obj)) + raise ValueError("Didn't find child {}".format(child_obj)) return "/".join([self.path, str(result_idx)]) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index f2e24fb522..bd617c6b7c 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -270,7 +270,7 @@ class RootEntity(BaseItemEntity): for key, _child_entity in self.non_gui_children.items(): if _child_entity is child_entity: return key - raise ValueError("Didn't found child {}".format(child_entity)) + raise ValueError("Didn't find child {}".format(child_entity)) @property def value(self): From 8c9463aa2a3a91ced4cdb4792e03f06f7a0119b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 15 Nov 2023 10:25:11 +0100 Subject: [PATCH 258/298] Update openpype/hosts/traypublisher/plugins/create/create_editorial.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 128010cef9..03b36dd232 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -727,8 +727,6 @@ or updating already created. Publishing will create OTIO file. }) base_instance_data["asset"] = parent_asset_name - - # add creator attributes to shared instance data base_instance_data["creator_attributes"] = creator_attributes # add hierarchy shot metadata From 4d48a6981bb77c93c7f99e386bb6a82ab4603239 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 11:25:47 +0100 Subject: [PATCH 259/298] Bugfix: Ayon Deadline env vars + error message on no executable found (#5815) * Fix ingesting env vars correctly for `DeadlinePlugin.RunProcess` * Fix error message reporting `;` between each character of the string * Cosmetics * Use `SetEnvironmentVariable` instead of `SetProcessEnvironmentVariable` because it's a Simple Plugin See: https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/environment.html#job-rendering --- .../deadline/repository/custom/plugins/Ayon/Ayon.py | 12 ++++++------ .../repository/custom/plugins/GlobalJobPreLoad.py | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index 2c55e7c951..a1f752605d 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -85,7 +85,7 @@ class AyonDeadlinePlugin(DeadlinePlugin): } for env, val in environment.items(): - self.SetProcessEnvironmentVariable(env, val) + self.SetEnvironmentVariable(env, val) exe_list = self.GetConfigEntry("AyonExecutable") # clean '\ ' for MacOS pasting @@ -101,11 +101,11 @@ class AyonDeadlinePlugin(DeadlinePlugin): if exe == "": self.FailRender( - "Ayon executable was not found " + - "in the semicolon separated list " + - "\"" + ";".join(exe_list) + "\". " + - "The path to the render executable can be configured " + - "from the Plugin Configuration in the Deadline Monitor.") + "Ayon executable was not found in the semicolon separated " + "list: \"{}\". The path to the render executable can be " + "configured from the Plugin Configuration in the Deadline " + "Monitor.".format(exe_list) + ) return exe def RenderArgument(self): diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index e9b81369ca..642608f991 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -495,7 +495,10 @@ def inject_ayon_environment(deadlinePlugin): "AYON_BUNDLE_NAME": ayon_bundle_name, } for env, val in environment.items(): + # Add the env var for the Render Plugin that is about to render deadlinePlugin.SetEnvironmentVariable(env, val) + # Add the env var for current calls to `DeadlinePlugin.RunProcess` + deadlinePlugin.SetProcessEnvironmentVariable(env, val) args_str = subprocess.list2cmdline(args) print(">>> Executing: {} {}".format(exe, args_str)) From 59c8e05ffbb5ebdf9c74892b08fc3ba27639cc29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Nov 2023 11:39:16 +0100 Subject: [PATCH 260/298] use 'folderPath' in create widget instead of asset --- openpype/tools/publisher/widgets/create_widget.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 64fed1d70c..73dcae51a5 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -816,8 +816,13 @@ class CreateWidget(QtWidgets.QWidget): # Where to define these data? # - what data show be stored? + if AYON_SERVER_ENABLED: + asset_key = "folderPath" + else: + asset_key = "asset" + instance_data = { - "asset": asset_name, + asset_key: asset_name, "task": task_name, "variant": variant, "family": family From c326a8bfd4e9032162bde742d13da628a6f70d1c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Nov 2023 11:44:28 +0100 Subject: [PATCH 261/298] in ayon folderPath should be on creator instance data --- .../hosts/traypublisher/plugins/create/create_editorial.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index c3453f1eae..198e05d593 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -224,8 +224,10 @@ or updating already created. Publishing will create OTIO file. i["family"] for i in self._creator_settings["family_presets"] ] } - # Create otio editorial instance - asset_name = instance_data["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) From 89dcb9452b9537701168a6a4a457e025fbd2ef63 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Nov 2023 12:04:59 +0100 Subject: [PATCH 262/298] fix '_check_existing' method in create hda --- openpype/hosts/houdini/api/plugin.py | 14 +++++++++++--- .../hosts/houdini/plugins/create/create_hda.py | 9 +++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 72565f7211..e162d0e461 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -6,6 +6,8 @@ from abc import ( ) import six import hou + +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import ( CreatorError, LegacyCreator, @@ -142,12 +144,13 @@ class HoudiniCreatorBase(object): @staticmethod def create_instance_node( - node_name, parent, - node_type="geometry"): + asset_name, node_name, parent, node_type="geometry" + ): # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: + asset_name (str): Asset name. node_name (str): Name of the new node. parent (str): Name of the parent node. node_type (str, optional): Type of the node. @@ -182,8 +185,13 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): if node_type is None: node_type = "geometry" + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] + instance_node = self.create_instance_node( - subset_name, "/out", node_type) + asset_name, subset_name, "/out", node_type) self.customize_node_look(instance_node) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index ac075d2072..f670b55eb6 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -17,13 +17,13 @@ class CreateHDA(plugin.HoudiniCreator): icon = "gears" maintain_selection = False - def _check_existing(self, subset_name): + def _check_existing(self, asset_name, subset_name): # type: (str) -> bool """Check if existing subset name versions already exists.""" # Get all subsets of the current asset project_name = self.project_name asset_doc = get_asset_by_name( - project_name, self.data["asset"], fields=["_id"] + project_name, asset_name, fields=["_id"] ) subset_docs = get_subsets( project_name, asset_ids=[asset_doc["_id"]], fields=["name"] @@ -35,7 +35,8 @@ class CreateHDA(plugin.HoudiniCreator): return subset_name.lower() in existing_subset_names_low def create_instance_node( - self, node_name, parent, node_type="geometry"): + self, asset_name, node_name, parent, node_type="geometry" + ): parent_node = hou.node("/obj") if self.selected_nodes: @@ -61,7 +62,7 @@ class CreateHDA(plugin.HoudiniCreator): hda_file_name="$HIP/{}.hda".format(node_name) ) hda_node.layoutChildren() - elif self._check_existing(node_name): + elif self._check_existing(asset_name, node_name): raise plugin.OpenPypeCreatorError( ("subset {} is already published with different HDA" "definition.").format(node_name)) From da8285d2cea1413ab45415dbe3a3c6b3a2aafd6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Nov 2023 12:25:06 +0100 Subject: [PATCH 263/298] fix create multishot layout --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 7cd3fdbd17..8f5c423202 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -158,7 +158,7 @@ class CreateMultishotLayout(plugin.MayaCreator): # Create layout instance by the layout creator instance_data = { - "asset": shot["name"], + "folderPath": shot["path"], "variant": layout_creator.get_default_variant() } if layout_task: From 43a50ecb2afc164d3243c90a91ca46bbf8c8025e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Nov 2023 12:28:19 +0100 Subject: [PATCH 264/298] fix folder path fix in validate instance in context --- .../maya/plugins/publish/validate_instance_in_context.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py index edfb002278..4222e63898 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import pyblish.api +from openpype import AYON_SERVER_ENABLED import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, @@ -66,8 +67,12 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, def repair(cls, instance): context_asset = cls.get_context_asset(instance) instance_node = instance.data["instance_node"] + if AYON_SERVER_ENABLED: + asset_name_attr = "folderPath" + else: + asset_name_attr = "asset" cmds.setAttr( - "{}.asset".format(instance_node), + "{}.{}".format(instance_node, asset_name_attr), context_asset, type="string" ) From 480c37533b8ce0e542e7b052ae28ff4d7a775acb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 12:43:19 +0100 Subject: [PATCH 265/298] Fix extract animation - `asset_group` is a Collection and can't be selected, and thus not exported from so we now take the 'root' node in that collection --- .../plugins/publish/extract_fbx_animation.py | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 712fbb2d14..a705345edb 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -10,6 +10,37 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY +def get_all_parents(obj): + """Get all recursive parents of object""" + result = [] + while True: + obj = obj.parent + if not obj: + break + result.append(obj) + return result + + +def get_highest_root(objects): + # Get the highest object that is also in the collection + included_objects = {obj.name_full for obj in objects} + num_parents_to_obj = {} + for obj in objects: + if isinstance(obj, bpy.types.Object): + parents = get_all_parents(obj) + # included parents + parents = [parent for parent in parents if + parent.name_full in included_objects] + if not parents: + # A node without parents must be a highest root + return obj + + num_parents_to_obj.setdefault(len(parents), obj) + + minimum_parent = min(num_parents_to_obj) + return num_parents_to_obj[minimum_parent] + + class ExtractAnimationFBX( publish.Extractor, publish.OptionalPyblishPluginMixin, @@ -38,6 +69,11 @@ class ExtractAnimationFBX( # TODO: Would it make more sense for the Collect Instance collector # to also always retrieve all the children? objects = set(asset_group.objects) + + # From the direct children of the collection find the 'root' node + # that we want to export - it is the 'highest' node in a hierarchy + root = get_highest_root(objects) + for obj in list(objects): objects.update(obj.children_recursive) @@ -78,8 +114,13 @@ class ExtractAnimationFBX( return asset_group_name = asset_group.name - asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name") + asset_name = asset_group.get(AVALON_PROPERTY).get("asset_name") + if asset_name: + # Rename for the export; this data is only present when loaded + # from a JSON Layout (layout family) + asset_group.name = asset_name + # Remove : from the armature name for the export armature_name = armature.name original_name = armature_name.split(':')[1] armature.name = original_name @@ -102,13 +143,13 @@ class ExtractAnimationFBX( for obj in bpy.data.objects: obj.select_set(False) - asset_group.select_set(True) + root.select_set(True) armature.select_set(True) fbx_filename = f"{instance.name}_{armature.name}.fbx" filepath = os.path.join(stagingdir, fbx_filename) override = plugin.create_blender_context( - active=asset_group, selected=[asset_group, armature]) + active=root, selected=[root, armature]) bpy.ops.export_scene.fbx( override, filepath=filepath, @@ -122,7 +163,7 @@ class ExtractAnimationFBX( ) armature.name = armature_name asset_group.name = asset_group_name - asset_group.select_set(False) + root.select_set(True) armature.select_set(False) # We delete the baked action and set the original one back From dd54866fe0925d606cb10be0b70d42e50cae2ad7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 13:08:59 +0100 Subject: [PATCH 266/298] Rename instance node if asset or subset changed to match with new name --- openpype/hosts/blender/api/plugin.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 5a0026d862..7ac12b5549 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -282,11 +282,26 @@ class BaseCreator(Creator): Args: update_list(List[UpdateData]): Changed instances and their changes, as a list of tuples.""" - for created_instance, _changes in update_list: + for created_instance, changes in update_list: data = created_instance.data_to_store() node = created_instance.transient_data["instance_node"] - if node: - imprint(node, data) + if not node: + # We can't update if we don't know the node + self.log.error( + f"Unable to update instance {created_instance} " + f"without instance node." + ) + return + + # Rename the instance node in the scene if subset or asset changed + if ( + "subset" in changes.changed_keys + or "asset" in changes.changed_keys + ): + name = asset_name(asset=data["asset"], subset=data["subset"]) + node.name = name + + imprint(node, data) def remove_instances(self, instances: List[CreatedInstance]): From 5d10f7f2dd0e7668a14ff7630ddf97b85d42e0cb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 13:15:50 +0100 Subject: [PATCH 267/298] Remove global `RENDER_ATTRS`, refactor to a `RenderSettings.get_padding_attr` method (#5801) - Partial cleanup extracted from #3880 --- openpype/hosts/maya/api/lib.py | 13 ------------- openpype/hosts/maya/api/lib_rendersettings.py | 8 ++++++++ .../plugins/publish/validate_rendersettings.py | 17 +++++++---------- .../plugins/publish/submit_maya_muster.py | 5 ++--- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 7c49c837e9..2ecaf87fce 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -62,19 +62,6 @@ SHAPE_ATTRS = {"castsShadows", "doubleSided", "opposite"} -RENDER_ATTRS = {"vray": { - "node": "vraySettings", - "prefix": "fileNamePrefix", - "padding": "fileNamePadding", - "ext": "imageFormatStr" -}, - "default": { - "node": "defaultRenderGlobals", - "prefix": "imageFilePrefix", - "padding": "extensionPadding" -} -} - DEFAULT_MATRIX = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 20264c2cdf..8b57c2e481 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -33,6 +33,14 @@ class RenderSettings(object): def get_image_prefix_attr(cls, renderer): return cls._image_prefix_nodes[renderer] + @staticmethod + def get_padding_attr(renderer): + """Return attribute for renderer that defines frame padding amount""" + if renderer == "vray": + return "vraySettings.fileNamePadding" + else: + return "defaultRenderGlobals.extensionPadding" + def __init__(self, project_settings=None): if not project_settings: project_settings = get_project_settings( diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index dccb4ade78..3409b4ec91 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -12,6 +12,7 @@ from openpype.pipeline.publish import ( PublishValidationError, ) from openpype.hosts.maya.api import lib +from openpype.hosts.maya.api.lib_rendersettings import RenderSettings def convert_to_int_or_float(string_value): @@ -129,13 +130,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): layer = instance.data['renderlayer'] cameras = instance.data.get("cameras", []) - # Get the node attributes for current renderer - attrs = lib.RENDER_ATTRS.get(renderer, lib.RENDER_ATTRS['default']) # Prefix attribute can return None when a value was never set prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer], layer=layer) or "" - padding = lib.get_attr_in_layer("{node}.{padding}".format(**attrs), - layer=layer) + padding = lib.get_attr_in_layer( + attr=RenderSettings.get_padding_attr(renderer), + layer=layer + ) anim_override = lib.get_attr_in_layer("defaultRenderGlobals.animation", layer=layer) @@ -372,8 +373,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): lib.set_attribute(data["attribute"], data["values"][0], node) with lib.renderlayer(layer_node): - default = lib.RENDER_ATTRS['default'] - render_attrs = lib.RENDER_ATTRS.get(renderer, default) # Repair animation must be enabled cmds.setAttr("defaultRenderGlobals.animation", True) @@ -391,15 +390,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): default_prefix = default_prefix.replace(variant, "") if renderer != "renderman": - node = render_attrs["node"] - prefix_attr = render_attrs["prefix"] - + prefix_attr = RenderSettings.get_image_prefix_attr(renderer) fname_prefix = default_prefix cmds.setAttr("{}.{}".format(node, prefix_attr), fname_prefix, type="string") # Repair padding - padding_attr = render_attrs["padding"] + padding_attr = RenderSettings.get_padding_attr(renderer) cmds.setAttr("{}.{}".format(node, padding_attr), cls.DEFAULT_PADDING) else: diff --git a/openpype/modules/muster/plugins/publish/submit_maya_muster.py b/openpype/modules/muster/plugins/publish/submit_maya_muster.py index 5c95744876..f6b3bfbbfd 100644 --- a/openpype/modules/muster/plugins/publish/submit_maya_muster.py +++ b/openpype/modules/muster/plugins/publish/submit_maya_muster.py @@ -10,6 +10,7 @@ from maya import cmds import pyblish.api from openpype.lib import requests_post from openpype.hosts.maya.api import lib +from openpype.hosts.maya.api.lib_rendersettings import RenderSettings from openpype.pipeline import legacy_io from openpype.settings import get_system_settings @@ -68,10 +69,8 @@ def get_renderer_variables(renderlayer=None): """ renderer = lib.get_renderer(renderlayer or lib.get_current_renderlayer()) - render_attrs = lib.RENDER_ATTRS.get(renderer, lib.RENDER_ATTRS["default"]) - padding = cmds.getAttr("{}.{}".format(render_attrs["node"], - render_attrs["padding"])) + padding = cmds.getAttr(RenderSettings.get_padding_attr(renderer)) filename_0 = cmds.renderSettings(fullPath=True, firstImageName=True)[0] From 8794b9ca9a5d821c12903fd613c3fbba0bc2bb28 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 15 Nov 2023 21:06:42 +0800 Subject: [PATCH 268/298] code tweaks on loaded plugins validator --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index ea2fee353d..efa06795b0 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -47,8 +47,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not mapping: return - match_families = {fam for fam in mapping["families"] - if fam.strip()} + match_families = {fam.strip() for fam in mapping["families"]} has_match = "*" in match_families or match_families.intersection( instance_families) From 025c114de854688bbf99e60a3d8fe3843c05025c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Nov 2023 16:40:46 +0100 Subject: [PATCH 269/298] Fix repair --- .../hosts/blender/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index 58047d7e23..d8826adc9c 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -41,7 +41,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - container = bpy.data.collections[str(instance)] + container = instance.data["transientData"]["instance_node"] prepare_rendering(container) bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) cls.log.debug("Reset the render output folder...") From 6fece8f6eacdc31a57bfaebe6b8075baa038ffea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Nov 2023 18:29:59 +0100 Subject: [PATCH 270/298] fix workfile create plugin in blender --- .../blender/plugins/create/create_workfile.py | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index 9245434766..ceec3e0552 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -1,5 +1,6 @@ import bpy +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name from openpype.hosts.blender.api.plugin import BaseCreator @@ -24,7 +25,7 @@ class CreateWorkfile(BaseCreator, AutoCreator): def create(self): """Create workfile instances.""" - current_instance = next( + existing_instance = next( ( instance for instance in self.create_context.instances if instance.creator_identifier == self.identifier @@ -37,16 +38,27 @@ class CreateWorkfile(BaseCreator, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - if not current_instance: + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + + if existing_asset_name is None: + existing_asset_name = existing_instance["asset"] + + if not existing_instance: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( task_name, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": task_name, } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update( self.get_dynamic_data( task_name, @@ -54,7 +66,7 @@ class CreateWorkfile(BaseCreator, AutoCreator): asset_doc, project_name, host_name, - current_instance, + existing_instance, ) ) self.log.info("Auto-creating workfile instance...") @@ -65,17 +77,21 @@ class CreateWorkfile(BaseCreator, AutoCreator): current_instance.transient_data["instance_node"] = instance_node self._add_instance_to_context(current_instance) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + existing_asset_name != asset_name + or existing_instance["task"] != task_name ): # Update instance context if it's different asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( task_name, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name - current_instance["task"] = task_name - current_instance["subset"] = subset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name + + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name def collect_instances(self): From 23291ac53c1fcc00ed603ecdd3d23897b1c844e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 15 Nov 2023 18:35:54 +0100 Subject: [PATCH 271/298] AYON workfiles tools: Revisit workfiles tool (#5897) * implemented base hierarchy expected selection model * use existing models and widgets in ayon workfiles tool * move private method under public method * added more methods to cache * reset models during controller reset * create workfile info all the time --- openpype/tools/ayon_utils/models/__init__.py | 3 + openpype/tools/ayon_utils/models/cache.py | 49 +- openpype/tools/ayon_utils/models/selection.py | 179 ++++++++ .../ayon_utils/widgets/projects_widget.py | 22 +- openpype/tools/ayon_workfiles/abstract.py | 48 +- openpype/tools/ayon_workfiles/control.py | 224 ++++++---- .../tools/ayon_workfiles/models/__init__.py | 2 - .../tools/ayon_workfiles/models/hierarchy.py | 236 ---------- .../tools/ayon_workfiles/models/selection.py | 23 +- .../tools/ayon_workfiles/models/workfiles.py | 24 +- .../ayon_workfiles/widgets/files_widget.py | 2 +- .../widgets/files_widget_published.py | 34 +- .../widgets/files_widget_workarea.py | 22 +- .../ayon_workfiles/widgets/folders_widget.py | 324 -------------- .../ayon_workfiles/widgets/side_panel.py | 2 +- .../ayon_workfiles/widgets/tasks_widget.py | 420 ------------------ .../tools/ayon_workfiles/widgets/window.py | 57 +-- 17 files changed, 501 insertions(+), 1170 deletions(-) create mode 100644 openpype/tools/ayon_utils/models/selection.py delete mode 100644 openpype/tools/ayon_workfiles/models/hierarchy.py delete mode 100644 openpype/tools/ayon_workfiles/widgets/folders_widget.py delete mode 100644 openpype/tools/ayon_workfiles/widgets/tasks_widget.py diff --git a/openpype/tools/ayon_utils/models/__init__.py b/openpype/tools/ayon_utils/models/__init__.py index 69722b5e21..8895515b1a 100644 --- a/openpype/tools/ayon_utils/models/__init__.py +++ b/openpype/tools/ayon_utils/models/__init__.py @@ -13,6 +13,7 @@ from .hierarchy import ( HIERARCHY_MODEL_SENDER, ) from .thumbnails import ThumbnailsModel +from .selection import HierarchyExpectedSelection __all__ = ( @@ -29,4 +30,6 @@ __all__ = ( "HIERARCHY_MODEL_SENDER", "ThumbnailsModel", + + "HierarchyExpectedSelection", ) diff --git a/openpype/tools/ayon_utils/models/cache.py b/openpype/tools/ayon_utils/models/cache.py index 44b97e930d..221a14160c 100644 --- a/openpype/tools/ayon_utils/models/cache.py +++ b/openpype/tools/ayon_utils/models/cache.py @@ -81,11 +81,11 @@ class NestedCacheItem: """Helper for cached items stored in nested structure. Example: - >>> cache = NestedCacheItem(levels=2) + >>> cache = NestedCacheItem(levels=2, default_factory=lambda: 0) >>> cache["a"]["b"].is_valid False >>> cache["a"]["b"].get_data() - None + 0 >>> cache["a"]["b"] = 1 >>> cache["a"]["b"].is_valid True @@ -167,8 +167,51 @@ class NestedCacheItem: return self[key] + def cached_count(self): + """Amount of cached items. + + Returns: + int: Amount of cached items. + """ + + return len(self._data_by_key) + + def clear_key(self, key): + """Clear cached item by key. + + Args: + key (str): Key of the cache item. + """ + + self._data_by_key.pop(key, None) + + def clear_invalid(self): + """Clear all invalid cache items. + + Note: + To clear all cache items use 'reset'. + """ + + changed = {} + children_are_nested = self._levels > 1 + for key, cache in tuple(self._data_by_key.items()): + if children_are_nested: + output = cache.clear_invalid() + if output: + changed[key] = output + if not cache.cached_count(): + self._data_by_key.pop(key) + elif not cache.is_valid: + changed[key] = cache.get_data() + self._data_by_key.pop(key) + return changed + def reset(self): - """Reset cache.""" + """Reset cache. + + Note: + To clear only invalid cache items use 'clear_invalid'. + """ self._data_by_key = {} diff --git a/openpype/tools/ayon_utils/models/selection.py b/openpype/tools/ayon_utils/models/selection.py new file mode 100644 index 0000000000..0ff239882b --- /dev/null +++ b/openpype/tools/ayon_utils/models/selection.py @@ -0,0 +1,179 @@ +class _ExampleController: + def emit_event(self, topic, data, **kwargs): + pass + + +class HierarchyExpectedSelection: + """Base skeleton of expected selection model. + + Expected selection model holds information about which entities should be + selected. The order of selection is very important as change of project + will affect what folders are available in folders UI and so on. Because + of that should expected selection model know what is current entity + to select. + + If any of 'handle_project', 'handle_folder' or 'handle_task' is set to + 'False' expected selection data won't contain information about the + entity type at all. Also if project is not handled then it is not + necessary to call 'expected_project_selected'. Same goes for folder and + task. + + Model is triggering event with 'expected_selection_changed' topic and + data > data structure is matching 'get_expected_selection_data' method. + + Questions: + Require '_ExampleController' as abstraction? + + Args: + controller (Any): Controller object. ('_ExampleController') + handle_project (bool): Project can be considered as can have expected + selection. + handle_folder (bool): Folder can be considered as can have expected + selection. + handle_task (bool): Task can be considered as can have expected + selection. + """ + + def __init__( + self, + controller, + handle_project=True, + handle_folder=True, + handle_task=True + ): + self._project_name = None + self._folder_id = None + self._task_name = None + + self._project_selected = True + self._folder_selected = True + self._task_selected = True + + self._controller = controller + + self._handle_project = handle_project + self._handle_folder = handle_folder + self._handle_task = handle_task + + def set_expected_selection( + self, + project_name=None, + folder_id=None, + task_name=None + ): + """Sets expected selection. + + Args: + project_name (Optional[str]): Project name. + folder_id (Optional[str]): Folder id. + task_name (Optional[str]): Task name. + """ + + self._project_name = project_name + self._folder_id = folder_id + self._task_name = task_name + + self._project_selected = not self._handle_project + self._folder_selected = not self._handle_folder + self._task_selected = not self._handle_task + self._emit_change() + + def get_expected_selection_data(self): + project_current = False + folder_current = False + task_current = False + if not self._project_selected: + project_current = True + elif not self._folder_selected: + folder_current = True + elif not self._task_selected: + task_current = True + data = {} + if self._handle_project: + data["project"] = { + "name": self._project_name, + "current": project_current, + "selected": self._project_selected, + } + if self._handle_folder: + data["folder"] = { + "id": self._folder_id, + "current": folder_current, + "selected": self._folder_selected, + } + if self._handle_task: + data["task"] = { + "name": self._task_name, + "current": task_current, + "selected": self._task_selected, + } + + return data + + def is_expected_project_selected(self, project_name): + if not self._handle_project: + return True + return project_name == self._project_name and self._project_selected + + def is_expected_folder_selected(self, folder_id): + if not self._handle_folder: + return True + return folder_id == self._folder_id and self._folder_selected + + def expected_project_selected(self, project_name): + """UI selected requested project. + + Other entity types can be requested for selection. + + Args: + project_name (str): Name of project. + """ + + if project_name != self._project_name: + return False + self._project_selected = True + self._emit_change() + return True + + def expected_folder_selected(self, folder_id): + """UI selected requested folder. + + Other entity types can be requested for selection. + + Args: + folder_id (str): Folder id. + """ + + if folder_id != self._folder_id: + return False + self._folder_selected = True + self._emit_change() + return True + + def expected_task_selected(self, folder_id, task_name): + """UI selected requested task. + + Other entity types can be requested for selection. + + Because task name is not unique across project a folder id is also + required to confirm the right task has been selected. + + Args: + folder_id (str): Folder id. + task_name (str): Task name. + """ + + if self._folder_id != folder_id: + return False + + if task_name != self._task_name: + return False + self._task_selected = True + self._emit_change() + return True + + def _emit_change(self): + self._controller.emit_event( + "expected_selection_changed", + self.get_expected_selection_data(), + ) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index f98bfcdf8a..728433f929 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -503,17 +503,6 @@ class ProjectsCombobox(QtWidgets.QWidget): self._projects_model.set_current_context_project(project_name) self._projects_proxy_model.invalidateFilter() - def _update_select_item_visiblity(self, **kwargs): - if not self._select_item_visible: - return - if "project_name" not in kwargs: - project_name = self.get_selected_project_name() - else: - project_name = kwargs.get("project_name") - - # Hide the item if a project is selected - self._projects_model.set_selected_project(project_name) - def set_select_item_visible(self, visible): self._select_item_visible = visible self._projects_model.set_select_item_visible(visible) @@ -534,6 +523,17 @@ class ProjectsCombobox(QtWidgets.QWidget): def set_library_filter_enabled(self, enabled): return self._projects_proxy_model.set_library_filter_enabled(enabled) + def _update_select_item_visiblity(self, **kwargs): + if not self._select_item_visible: + return + if "project_name" not in kwargs: + project_name = self.get_selected_project_name() + else: + project_name = kwargs.get("project_name") + + # Hide the item if a project is selected + self._projects_model.set_selected_project(project_name) + def _on_current_index_changed(self, idx): if not self._listen_selection_change: return diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py index ce399fd4c6..260f701d4b 100644 --- a/openpype/tools/ayon_workfiles/abstract.py +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -443,8 +443,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon): pass @abstractmethod - def get_project_entity(self): - """Get current project entity. + def get_project_entity(self, project_name): + """Get project entity by name. + + Args: + project_name (str): Project name. Returns: dict[str, Any]: Project entity data. @@ -453,10 +456,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon): pass @abstractmethod - def get_folder_entity(self, folder_id): + def get_folder_entity(self, project_name, folder_id): """Get folder entity by id. Args: + project_name (str): Project name. folder_id (str): Folder id. Returns: @@ -466,10 +470,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon): pass @abstractmethod - def get_task_entity(self, task_id): + def get_task_entity(self, project_name, task_id): """Get task entity by id. Args: + project_name (str): Project name. task_id (str): Task id. Returns: @@ -574,12 +579,10 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def set_selected_task(self, folder_id, task_id, task_name): + def set_selected_task(self, task_id, task_name): """Change selected task. Args: - folder_id (Union[str, None]): Folder id or None if no folder - is selected. task_id (Union[str, None]): Task id or None if no task is selected. task_name (Union[str, None]): Task name or None if no task @@ -711,21 +714,27 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def expected_representation_selected(self, representation_id): + def expected_representation_selected( + self, folder_id, task_name, representation_id + ): """Expected representation was selected in UI. Args: + folder_id (str): Folder id under which representation is. + task_name (str): Task name under which representation is. representation_id (str): Representation id which was selected. """ pass @abstractmethod - def expected_workfile_selected(self, workfile_path): + def expected_workfile_selected(self, folder_id, task_name, workfile_name): """Expected workfile was selected in UI. Args: - workfile_path (str): Workfile path which was selected. + folder_id (str): Folder id under which workfile is. + task_name (str): Task name under which workfile is. + workfile_name (str): Workfile filename which was selected. """ pass @@ -738,7 +747,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): # Model functions @abstractmethod - def get_folder_items(self, sender): + def get_folder_items(self, project_name, sender): """Folder items to visualize project hierarchy. This function may trigger events 'folders.refresh.started' and @@ -746,6 +755,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): That may help to avoid re-refresh of folder items in UI elements. Args: + project_name (str): Project name for which are folders requested. sender (str): Who requested folder items. Returns: @@ -756,7 +766,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_task_items(self, folder_id, sender): + def get_task_items(self, project_name, folder_id, sender): """Task items. This function may trigger events 'tasks.refresh.started' and @@ -764,6 +774,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): That may help to avoid re-refresh of task items in UI elements. Args: + project_name (str): Project name for which are tasks requested. folder_id (str): Folder ID for which are tasks requested. sender (str): Who requested folder items. @@ -892,22 +903,25 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): At this moment the only information which can be saved about workfile is 'note'. + When 'note' is 'None' it is only validated if workfile info exists, + and if not then creates one with empty note. + Args: folder_id (str): Folder id. task_id (str): Task id. filepath (str): Workfile path. - note (str): Note. + note (Union[str, None]): Note. """ pass # General commands @abstractmethod - def refresh(self): - """Refresh everything, models, ui etc. + def reset(self): + """Reset everything, models, ui etc. - Triggers 'controller.refresh.started' event at the beginning and - 'controller.refresh.finished' at the end. + Triggers 'controller.reset.started' event at the beginning and + 'controller.reset.finished' at the end. """ pass diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index 3784959caf..fbe6df3155 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -16,93 +16,120 @@ from openpype.pipeline.context_tools import ( ) from openpype.pipeline.workfile import create_workdir_extra_folders +from openpype.tools.ayon_utils.models import ( + HierarchyModel, + HierarchyExpectedSelection, + ProjectsModel, +) + from .abstract import ( AbstractWorkfilesFrontend, AbstractWorkfilesBackend, ) -from .models import SelectionModel, EntitiesModel, WorkfilesModel +from .models import SelectionModel, WorkfilesModel -class ExpectedSelection: - def __init__(self): - self._folder_id = None - self._task_name = None +class WorkfilesToolExpectedSelection(HierarchyExpectedSelection): + def __init__(self, controller): + super(WorkfilesToolExpectedSelection, self).__init__( + controller, + handle_project=False, + handle_folder=True, + handle_task=True, + ) + self._workfile_name = None self._representation_id = None - self._folder_selected = True - self._task_selected = True - self._workfile_name_selected = True - self._representation_id_selected = True + + self._workfile_selected = True + self._representation_selected = True def set_expected_selection( self, - folder_id, - task_name, + project_name=None, + folder_id=None, + task_name=None, workfile_name=None, - representation_id=None + representation_id=None, ): - self._folder_id = folder_id - self._task_name = task_name self._workfile_name = workfile_name self._representation_id = representation_id - self._folder_selected = False - self._task_selected = False - self._workfile_name_selected = workfile_name is None - self._representation_id_selected = representation_id is None + + self._workfile_selected = False + self._representation_selected = False + + super(WorkfilesToolExpectedSelection, self).set_expected_selection( + project_name, + folder_id, + task_name, + ) def get_expected_selection_data(self): - return { - "folder_id": self._folder_id, - "task_name": self._task_name, - "workfile_name": self._workfile_name, - "representation_id": self._representation_id, - "folder_selected": self._folder_selected, - "task_selected": self._task_selected, - "workfile_name_selected": self._workfile_name_selected, - "representation_id_selected": self._representation_id_selected, + data = super( + WorkfilesToolExpectedSelection, self + ).get_expected_selection_data() + + _is_current = ( + self._project_selected + and self._folder_selected + and self._task_selected + ) + workfile_is_current = False + repre_is_current = False + if _is_current: + workfile_is_current = not self._workfile_selected + repre_is_current = not self._representation_selected + + data["workfile"] = { + "name": self._workfile_name, + "current": workfile_is_current, + "selected": self._workfile_selected, } + data["representation"] = { + "id": self._representation_id, + "current": repre_is_current, + "selected": self._workfile_selected, + } + return data - def is_expected_folder_selected(self, folder_id): - return folder_id == self._folder_id and self._folder_selected + def is_expected_workfile_selected(self, workfile_name): + return ( + workfile_name == self._workfile_name + and self._workfile_selected + ) - def is_expected_task_selected(self, folder_id, task_name): - if not self.is_expected_folder_selected(folder_id): - return False - return task_name == self._task_name and self._task_selected + def is_expected_representation_selected(self, representation_id): + return ( + representation_id == self._representation_id + and self._representation_selected + ) - def expected_folder_selected(self, folder_id): + def expected_workfile_selected(self, folder_id, task_name, workfile_name): if folder_id != self._folder_id: return False - self._folder_selected = True - return True - - def expected_task_selected(self, folder_id, task_name): - if not self.is_expected_folder_selected(folder_id): - return False if task_name != self._task_name: return False - self._task_selected = True - return True - - def expected_workfile_selected(self, folder_id, task_name, workfile_name): - if not self.is_expected_task_selected(folder_id, task_name): - return False - if workfile_name != self._workfile_name: return False - self._workfile_name_selected = True + self._workfile_selected = True + self._emit_change() return True def expected_representation_selected( self, folder_id, task_name, representation_id ): - if not self.is_expected_task_selected(folder_id, task_name): + if folder_id != self._folder_id: return False + + if task_name != self._task_name: + return False + if representation_id != self._representation_id: return False - self._representation_id_selected = True + self._representation_selected = True + self._emit_change() return True @@ -136,9 +163,9 @@ class BaseWorkfileController( # Expected selected folder and task self._expected_selection = self._create_expected_selection_obj() - self._selection_model = self._create_selection_model() - self._entities_model = self._create_entities_model() + self._projects_model = self._create_projects_model() + self._hierarchy_model = self._create_hierarchy_model() self._workfiles_model = self._create_workfiles_model() @property @@ -151,13 +178,16 @@ class BaseWorkfileController( return self._host_is_valid def _create_expected_selection_obj(self): - return ExpectedSelection() + return WorkfilesToolExpectedSelection(self) + + def _create_projects_model(self): + return ProjectsModel(self) def _create_selection_model(self): return SelectionModel(self) - def _create_entities_model(self): - return EntitiesModel(self) + def _create_hierarchy_model(self): + return HierarchyModel(self) def _create_workfiles_model(self): return WorkfilesModel(self) @@ -193,14 +223,17 @@ class BaseWorkfileController( self._project_anatomy = Anatomy(self.get_current_project_name()) return self._project_anatomy - def get_project_entity(self): - return self._entities_model.get_project_entity() + def get_project_entity(self, project_name): + return self._projects_model.get_project_entity( + project_name) - def get_folder_entity(self, folder_id): - return self._entities_model.get_folder_entity(folder_id) + def get_folder_entity(self, project_name, folder_id): + return self._hierarchy_model.get_folder_entity( + project_name, folder_id) - def get_task_entity(self, task_id): - return self._entities_model.get_task_entity(task_id) + def get_task_entity(self, project_name, task_id): + return self._hierarchy_model.get_task_entity( + project_name, task_id) # --------------------------------- # Implementation of abstract methods @@ -293,9 +326,8 @@ class BaseWorkfileController( def get_selected_task_name(self): return self._selection_model.get_selected_task_name() - def set_selected_task(self, folder_id, task_id, task_name): - return self._selection_model.set_selected_task( - folder_id, task_id, task_name) + def set_selected_task(self, task_id, task_name): + return self._selection_model.set_selected_task(task_id, task_name) def get_selected_workfile_path(self): return self._selection_model.get_selected_workfile_path() @@ -318,7 +350,11 @@ class BaseWorkfileController( representation_id=None ): self._expected_selection.set_expected_selection( - folder_id, task_name, workfile_name, representation_id + self.get_current_project_name(), + folder_id, + task_name, + workfile_name, + representation_id ) self._trigger_expected_selection_changed() @@ -355,11 +391,13 @@ class BaseWorkfileController( ) # Model functions - def get_folder_items(self, sender): - return self._entities_model.get_folder_items(sender) + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) - def get_task_items(self, folder_id, sender): - return self._entities_model.get_tasks_items(folder_id, sender) + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender + ) def get_workarea_dir_by_context(self, folder_id, task_id): return self._workfiles_model.get_workarea_dir_by_context( @@ -394,7 +432,9 @@ class BaseWorkfileController( def get_published_file_items(self, folder_id, task_id): task_name = None if task_id: - task = self.get_task_entity(task_id) + task = self.get_task_entity( + self.get_current_project_name(), task_id + ) task_name = task.get("name") return self._workfiles_model.get_published_file_items( @@ -410,21 +450,27 @@ class BaseWorkfileController( folder_id, task_id, filepath, note ) - def refresh(self): + def reset(self): if not self._host_is_valid: - self._emit_event("controller.refresh.started") - self._emit_event("controller.refresh.finished") + self._emit_event("controller.reset.started") + self._emit_event("controller.reset.finished") return expected_folder_id = self.get_selected_folder_id() expected_task_name = self.get_selected_task_name() + expected_work_path = self.get_selected_workfile_path() + expected_repre_id = self.get_selected_representation_id() + expected_work_name = None + if expected_work_path: + expected_work_name = os.path.basename(expected_work_path) - self._emit_event("controller.refresh.started") + self._emit_event("controller.reset.started") context = self._get_host_current_context() project_name = context["project_name"] folder_name = context["asset_name"] task_name = context["task_name"] + current_file = self.get_current_workfile() folder_id = None if folder_name: folder = ayon_api.get_folder_by_name(project_name, folder_name) @@ -439,18 +485,25 @@ class BaseWorkfileController( self._current_folder_id = folder_id self._current_task_name = task_name + self._projects_model.reset() + self._hierarchy_model.reset() + if not expected_folder_id: expected_folder_id = folder_id expected_task_name = task_name + if current_file: + expected_work_name = os.path.basename(current_file) + + self._emit_event("controller.reset.finished") self._expected_selection.set_expected_selection( - expected_folder_id, expected_task_name + project_name, + expected_folder_id, + expected_task_name, + expected_work_name, + expected_repre_id, ) - self._entities_model.refresh() - - self._emit_event("controller.refresh.finished") - # Controller actions def open_workfile(self, folder_id, task_id, filepath): self._emit_event("open_workfile.started") @@ -579,9 +632,9 @@ class BaseWorkfileController( self, project_name, folder_id, task_id, folder=None, task=None ): if folder is None: - folder = self.get_folder_entity(folder_id) + folder = self.get_folder_entity(project_name, folder_id) if task is None: - task = self.get_task_entity(task_id) + task = self.get_task_entity(project_name, task_id) # NOTE keys should be OpenPype compatible return { "project_name": project_name, @@ -633,8 +686,8 @@ class BaseWorkfileController( ): # Trigger before save event project_name = self.get_current_project_name() - folder = self.get_folder_entity(folder_id) - task = self.get_task_entity(task_id) + folder = self.get_folder_entity(project_name, folder_id) + task = self.get_task_entity(project_name, task_id) task_name = task["name"] # QUESTION should the data be different for 'before' and 'after'? @@ -674,6 +727,9 @@ class BaseWorkfileController( else: self._host_save_workfile(dst_filepath) + # Make sure workfile info exists + self.save_workfile_info(folder_id, task_id, dst_filepath, None) + # Create extra folders create_workdir_extra_folders( workdir, @@ -685,4 +741,4 @@ class BaseWorkfileController( # Trigger after save events emit_event("workfile.save.after", event_data, source="workfiles.tool") - self.refresh() + self.reset() diff --git a/openpype/tools/ayon_workfiles/models/__init__.py b/openpype/tools/ayon_workfiles/models/__init__.py index d906b9e7bd..734cb08cb6 100644 --- a/openpype/tools/ayon_workfiles/models/__init__.py +++ b/openpype/tools/ayon_workfiles/models/__init__.py @@ -1,10 +1,8 @@ -from .hierarchy import EntitiesModel from .selection import SelectionModel from .workfiles import WorkfilesModel __all__ = ( "SelectionModel", - "EntitiesModel", "WorkfilesModel", ) diff --git a/openpype/tools/ayon_workfiles/models/hierarchy.py b/openpype/tools/ayon_workfiles/models/hierarchy.py deleted file mode 100644 index a1d51525da..0000000000 --- a/openpype/tools/ayon_workfiles/models/hierarchy.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Hierarchy model that handles folders and tasks. - -The model can be extracted for common usage. In that case it will be required -to add more handling of project name changes. -""" - -import time -import collections -import contextlib - -import ayon_api - -from openpype.tools.ayon_workfiles.abstract import ( - FolderItem, - TaskItem, -) - - -def _get_task_items_from_tasks(tasks): - """ - - Returns: - TaskItem: Task item. - """ - - output = [] - for task in tasks: - folder_id = task["folderId"] - output.append(TaskItem( - task["id"], - task["name"], - task["type"], - folder_id, - None, - None - )) - return output - - -def _get_folder_item_from_hierarchy_item(item): - return FolderItem( - item["id"], - item["parentId"], - item["name"], - item["label"], - None, - None, - ) - - -class CacheItem: - def __init__(self, lifetime=120): - self._lifetime = lifetime - self._last_update = None - self._data = None - - @property - def is_valid(self): - if self._last_update is None: - return False - - return (time.time() - self._last_update) < self._lifetime - - def set_invalid(self, data=None): - self._last_update = None - self._data = data - - def get_data(self): - return self._data - - def update_data(self, data): - self._data = data - self._last_update = time.time() - - -class EntitiesModel(object): - event_source = "entities.model" - - def __init__(self, controller): - project_cache = CacheItem() - project_cache.set_invalid({}) - folders_cache = CacheItem() - folders_cache.set_invalid({}) - self._project_cache = project_cache - self._folders_cache = folders_cache - self._tasks_cache = {} - - self._folders_by_id = {} - self._tasks_by_id = {} - - self._folders_refreshing = False - self._tasks_refreshing = set() - self._controller = controller - - def reset(self): - self._project_cache.set_invalid({}) - self._folders_cache.set_invalid({}) - self._tasks_cache = {} - - self._folders_by_id = {} - self._tasks_by_id = {} - - def refresh(self): - self._refresh_folders_cache() - - def get_project_entity(self): - if not self._project_cache.is_valid: - project_name = self._controller.get_current_project_name() - project_entity = ayon_api.get_project(project_name) - self._project_cache.update_data(project_entity) - return self._project_cache.get_data() - - def get_folder_items(self, sender): - if not self._folders_cache.is_valid: - self._refresh_folders_cache(sender) - return self._folders_cache.get_data() - - def get_tasks_items(self, folder_id, sender): - if not folder_id: - return [] - - task_cache = self._tasks_cache.get(folder_id) - if task_cache is None or not task_cache.is_valid: - self._refresh_tasks_cache(folder_id, sender) - task_cache = self._tasks_cache.get(folder_id) - return task_cache.get_data() - - def get_folder_entity(self, folder_id): - if folder_id not in self._folders_by_id: - entity = None - if folder_id: - project_name = self._controller.get_current_project_name() - entity = ayon_api.get_folder_by_id(project_name, folder_id) - self._folders_by_id[folder_id] = entity - return self._folders_by_id[folder_id] - - def get_task_entity(self, task_id): - if task_id not in self._tasks_by_id: - entity = None - if task_id: - project_name = self._controller.get_current_project_name() - entity = ayon_api.get_task_by_id(project_name, task_id) - self._tasks_by_id[task_id] = entity - return self._tasks_by_id[task_id] - - @contextlib.contextmanager - def _folder_refresh_event_manager(self, project_name, sender): - self._folders_refreshing = True - self._controller.emit_event( - "folders.refresh.started", - {"project_name": project_name, "sender": sender}, - self.event_source - ) - try: - yield - - finally: - self._controller.emit_event( - "folders.refresh.finished", - {"project_name": project_name, "sender": sender}, - self.event_source - ) - self._folders_refreshing = False - - @contextlib.contextmanager - def _task_refresh_event_manager( - self, project_name, folder_id, sender - ): - self._tasks_refreshing.add(folder_id) - self._controller.emit_event( - "tasks.refresh.started", - { - "project_name": project_name, - "folder_id": folder_id, - "sender": sender, - }, - self.event_source - ) - try: - yield - - finally: - self._controller.emit_event( - "tasks.refresh.finished", - { - "project_name": project_name, - "folder_id": folder_id, - "sender": sender, - }, - self.event_source - ) - self._tasks_refreshing.discard(folder_id) - - def _refresh_folders_cache(self, sender=None): - if self._folders_refreshing: - return - project_name = self._controller.get_current_project_name() - with self._folder_refresh_event_manager(project_name, sender): - folder_items = self._query_folders(project_name) - self._folders_cache.update_data(folder_items) - - def _query_folders(self, project_name): - hierarchy = ayon_api.get_folders_hierarchy(project_name) - - folder_items = {} - hierachy_queue = collections.deque(hierarchy["hierarchy"]) - while hierachy_queue: - item = hierachy_queue.popleft() - folder_item = _get_folder_item_from_hierarchy_item(item) - folder_items[folder_item.entity_id] = folder_item - hierachy_queue.extend(item["children"] or []) - return folder_items - - def _refresh_tasks_cache(self, folder_id, sender=None): - if folder_id in self._tasks_refreshing: - return - - project_name = self._controller.get_current_project_name() - with self._task_refresh_event_manager( - project_name, folder_id, sender - ): - cache_item = self._tasks_cache.get(folder_id) - if cache_item is None: - cache_item = CacheItem() - self._tasks_cache[folder_id] = cache_item - - task_items = self._query_tasks(project_name, folder_id) - cache_item.update_data(task_items) - - def _query_tasks(self, project_name, folder_id): - tasks = list(ayon_api.get_tasks( - project_name, - folder_ids=[folder_id], - fields={"id", "name", "label", "folderId", "type"} - )) - return _get_task_items_from_tasks(tasks) diff --git a/openpype/tools/ayon_workfiles/models/selection.py b/openpype/tools/ayon_workfiles/models/selection.py index ad034794d8..2f0896842d 100644 --- a/openpype/tools/ayon_workfiles/models/selection.py +++ b/openpype/tools/ayon_workfiles/models/selection.py @@ -4,7 +4,7 @@ class SelectionModel(object): Triggering events: - "selection.folder.changed" - "selection.task.changed" - - "workarea.selection.changed" + - "selection.workarea.changed" - "selection.representation.changed" """ @@ -29,7 +29,10 @@ class SelectionModel(object): self._folder_id = folder_id self._controller.emit_event( "selection.folder.changed", - {"folder_id": folder_id}, + { + "project_name": self._controller.get_current_project_name(), + "folder_id": folder_id + }, self.event_source ) @@ -39,10 +42,7 @@ class SelectionModel(object): def get_selected_task_id(self): return self._task_id - def set_selected_task(self, folder_id, task_id, task_name): - if folder_id != self._folder_id: - self.set_selected_folder(folder_id) - + def set_selected_task(self, task_id, task_name): if task_id == self._task_id: return @@ -51,7 +51,8 @@ class SelectionModel(object): self._controller.emit_event( "selection.task.changed", { - "folder_id": folder_id, + "project_name": self._controller.get_current_project_name(), + "folder_id": self._folder_id, "task_name": task_name, "task_id": task_id }, @@ -67,8 +68,9 @@ class SelectionModel(object): self._workfile_path = path self._controller.emit_event( - "workarea.selection.changed", + "selection.workarea.changed", { + "project_name": self._controller.get_current_project_name(), "path": path, "folder_id": self._folder_id, "task_name": self._task_name, @@ -86,6 +88,9 @@ class SelectionModel(object): self._representation_id = representation_id self._controller.emit_event( "selection.representation.changed", - {"representation_id": representation_id}, + { + "project_name": self._controller.get_current_project_name(), + "representation_id": representation_id, + }, self.event_source ) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py index 4d989ed22c..907b9b5383 100644 --- a/openpype/tools/ayon_workfiles/models/workfiles.py +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -148,7 +148,9 @@ class WorkareaModel: def _get_folder_data(self, folder_id): fill_data = self._fill_data_by_folder_id.get(folder_id) if fill_data is None: - folder = self._controller.get_folder_entity(folder_id) + folder = self._controller.get_folder_entity( + self.project_name, folder_id + ) fill_data = get_folder_template_data(folder) self._fill_data_by_folder_id[folder_id] = fill_data return copy.deepcopy(fill_data) @@ -156,7 +158,9 @@ class WorkareaModel: def _get_task_data(self, project_entity, folder_id, task_id): task_data = self._task_data_by_folder_id.setdefault(folder_id, {}) if task_id not in task_data: - task = self._controller.get_task_entity(task_id) + task = self._controller.get_task_entity( + self.project_name, task_id + ) if task: task_data[task_id] = get_task_template_data( project_entity, task) @@ -167,8 +171,9 @@ class WorkareaModel: return {} base_data = self._get_base_data() + project_name = base_data["project"]["name"] folder_data = self._get_folder_data(folder_id) - project_entity = self._controller.get_project_entity() + project_entity = self._controller.get_project_entity(project_name) task_data = self._get_task_data(project_entity, folder_id, task_id) base_data.update(folder_data) @@ -292,9 +297,13 @@ class WorkareaModel: folder = None task = None if folder_id: - folder = self._controller.get_folder_entity(folder_id) + folder = self._controller.get_folder_entity( + self.project_name, folder_id + ) if task_id: - task = self._controller.get_task_entity(task_id) + task = self._controller.get_task_entity( + self.project_name, task_id + ) if not folder or not task: return { @@ -491,10 +500,13 @@ class WorkfileEntitiesModel: ) if not workfile_info: self._cache[identifier] = self._create_workfile_info_entity( - task_id, rootless_path, note) + task_id, rootless_path, note or "") self._items.pop(identifier, None) return + if note is None: + return + new_workfile_info = copy.deepcopy(workfile_info) attrib = new_workfile_info.setdefault("attrib", {}) attrib["description"] = note diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget.py b/openpype/tools/ayon_workfiles/widgets/files_widget.py index 656ddf1dd8..16f0b6fce3 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget.py @@ -69,7 +69,7 @@ class FilesWidget(QtWidgets.QWidget): main_layout.addWidget(btns_widget, 0) controller.register_event_callback( - "workarea.selection.changed", + "selection.workarea.changed", self._on_workarea_path_changed ) controller.register_event_callback( diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py index 576cf18d73..704f7b2f39 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py @@ -59,14 +59,6 @@ class PublishedFilesModel(QtGui.QStandardItemModel): self._add_empty_item() - def _clear_items(self): - self._remove_missing_context_item() - self._remove_empty_item() - if self._items_by_id: - root = self.invisibleRootItem() - root.removeRows(0, root.rowCount()) - self._items_by_id = {} - def set_published_mode(self, published_mode): if self._published_mode == published_mode: return @@ -89,6 +81,18 @@ class PublishedFilesModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() return self.indexFromItem(item) + def refresh(self): + if self._published_mode: + self._fill_items() + + def _clear_items(self): + self._remove_missing_context_item() + self._remove_empty_item() + if self._items_by_id: + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + self._items_by_id = {} + def _get_missing_context_item(self): if self._missing_context_item is None: message = "Select folder" @@ -149,7 +153,6 @@ class PublishedFilesModel(QtGui.QStandardItemModel): def _on_folder_changed(self, event): self._last_folder_id = event["folder_id"] - self._last_task_id = None if self._context_select_mode: return @@ -356,14 +359,13 @@ class PublishedFilesWidget(QtWidgets.QWidget): self.save_as_requested.emit() def _on_expected_selection_change(self, event): - if ( - event["representation_id_selected"] - or not event["folder_selected"] - or (event["task_name"] and not event["task_selected"]) - ): + repre_info = event["representation"] + if not repre_info["current"]: return - representation_id = event["representation_id"] + self._model.refresh() + + representation_id = repre_info["id"] selected_repre_id = self.get_selected_repre_id() if ( representation_id is not None @@ -376,5 +378,5 @@ class PublishedFilesWidget(QtWidgets.QWidget): self._view.setCurrentIndex(proxy_index) self._controller.expected_representation_selected( - event["folder_id"], event["task_name"], representation_id + event["folder"]["id"], event["task"]["name"], representation_id ) diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py index 3a8e90f933..8eefd3cf81 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py @@ -28,6 +28,10 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_changed + ) controller.register_event_callback( "selection.task.changed", self._on_task_changed @@ -63,6 +67,10 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() return self.indexFromItem(item) + def refresh(self): + if not self._published_mode: + self._fill_items() + def _get_missing_context_item(self): if self._missing_context_item is None: message = "Select folder and task" @@ -129,6 +137,11 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): root_item.takeRow(self._empty_root_item.row()) self._empty_item_used = False + def _on_folder_changed(self, event): + self._selected_folder_id = event["folder_id"] + if not self._published_mode: + self._fill_items() + def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] self._selected_task_id = event["task_id"] @@ -362,10 +375,13 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): self.duplicate_requested.emit() def _on_expected_selection_change(self, event): - if event["workfile_name_selected"]: + workfile_info = event["workfile"] + if not workfile_info["current"]: return - workfile_name = event["workfile_name"] + self._model.refresh() + + workfile_name = workfile_info["name"] if ( workfile_name is not None and workfile_name != self._get_selected_info()["filename"] @@ -376,5 +392,5 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): self._view.setCurrentIndex(proxy_index) self._controller.expected_workfile_selected( - event["folder_id"], event["task_name"], workfile_name + event["folder"]["id"], event["task"]["name"], workfile_name ) diff --git a/openpype/tools/ayon_workfiles/widgets/folders_widget.py b/openpype/tools/ayon_workfiles/widgets/folders_widget.py deleted file mode 100644 index b04f8e4098..0000000000 --- a/openpype/tools/ayon_workfiles/widgets/folders_widget.py +++ /dev/null @@ -1,324 +0,0 @@ -import uuid -import collections - -import qtawesome -from qtpy import QtWidgets, QtGui, QtCore - -from openpype.tools.utils import ( - RecursiveSortFilterProxyModel, - DeselectableTreeView, -) - -from .constants import ITEM_ID_ROLE, ITEM_NAME_ROLE - -SENDER_NAME = "qt_folders_model" - - -class FoldersRefreshThread(QtCore.QThread): - """Thread for refreshing folders. - - Call controller to get folders and emit signal when finished. - - Args: - controller (AbstractWorkfilesFrontend): The control object. - """ - - refresh_finished = QtCore.Signal(str) - - def __init__(self, controller): - super(FoldersRefreshThread, self).__init__() - self._id = uuid.uuid4().hex - self._controller = controller - self._result = None - - @property - def id(self): - """Thread id. - - Returns: - str: Unique id of the thread. - """ - - return self._id - - def run(self): - self._result = self._controller.get_folder_items(SENDER_NAME) - self.refresh_finished.emit(self.id) - - def get_result(self): - return self._result - - -class FoldersModel(QtGui.QStandardItemModel): - """Folders model which cares about refresh of folders. - - Args: - controller (AbstractWorkfilesFrontend): The control object. - """ - - refreshed = QtCore.Signal() - - def __init__(self, controller): - super(FoldersModel, self).__init__() - - self._controller = controller - self._items_by_id = {} - self._parent_id_by_id = {} - - self._refresh_threads = {} - self._current_refresh_thread = None - - self._has_content = False - self._is_refreshing = False - - @property - def is_refreshing(self): - """Model is refreshing. - - Returns: - bool: True if model is refreshing. - """ - return self._is_refreshing - - @property - def has_content(self): - """Has at least one folder. - - Returns: - bool: True if model has at least one folder. - """ - - return self._has_content - - def clear(self): - self._items_by_id = {} - self._parent_id_by_id = {} - self._has_content = False - super(FoldersModel, self).clear() - - def get_index_by_id(self, item_id): - """Get index by folder id. - - Returns: - QtCore.QModelIndex: Index of the folder. Can be invalid if folder - is not available. - """ - item = self._items_by_id.get(item_id) - if item is None: - return QtCore.QModelIndex() - return self.indexFromItem(item) - - def refresh(self): - """Refresh folders items. - - Refresh start thread because it can cause that controller can - start query from database if folders are not cached. - """ - - self._is_refreshing = True - - thread = FoldersRefreshThread(self._controller) - self._current_refresh_thread = thread.id - self._refresh_threads[thread.id] = thread - thread.refresh_finished.connect(self._on_refresh_thread) - thread.start() - - def _on_refresh_thread(self, thread_id): - """Callback when refresh thread is finished. - - Technically can be running multiple refresh threads at the same time, - to avoid using values from wrong thread, we check if thread id is - current refresh thread id. - - Folders are stored by id. - - Args: - thread_id (str): Thread id. - """ - - thread = self._refresh_threads.pop(thread_id) - if thread_id != self._current_refresh_thread: - return - - folder_items_by_id = thread.get_result() - if not folder_items_by_id: - if folder_items_by_id is not None: - self.clear() - self._is_refreshing = False - return - - self._has_content = True - - folder_ids = set(folder_items_by_id) - ids_to_remove = set(self._items_by_id) - folder_ids - - folder_items_by_parent = collections.defaultdict(list) - for folder_item in folder_items_by_id.values(): - folder_items_by_parent[folder_item.parent_id].append(folder_item) - - hierarchy_queue = collections.deque() - hierarchy_queue.append(None) - - while hierarchy_queue: - parent_id = hierarchy_queue.popleft() - folder_items = folder_items_by_parent[parent_id] - if parent_id is None: - parent_item = self.invisibleRootItem() - else: - parent_item = self._items_by_id[parent_id] - - new_items = [] - for folder_item in folder_items: - item_id = folder_item.entity_id - item = self._items_by_id.get(item_id) - if item is None: - is_new = True - item = QtGui.QStandardItem() - item.setEditable(False) - else: - is_new = self._parent_id_by_id[item_id] != parent_id - - icon = qtawesome.icon( - folder_item.icon_name, - color=folder_item.icon_color, - ) - item.setData(item_id, ITEM_ID_ROLE) - item.setData(folder_item.name, ITEM_NAME_ROLE) - item.setData(folder_item.label, QtCore.Qt.DisplayRole) - item.setData(icon, QtCore.Qt.DecorationRole) - if is_new: - new_items.append(item) - self._items_by_id[item_id] = item - self._parent_id_by_id[item_id] = parent_id - - hierarchy_queue.append(item_id) - - if new_items: - parent_item.appendRows(new_items) - - for item_id in ids_to_remove: - item = self._items_by_id[item_id] - parent_id = self._parent_id_by_id[item_id] - if parent_id is None: - parent_item = self.invisibleRootItem() - else: - parent_item = self._items_by_id[parent_id] - parent_item.takeChild(item.row()) - - for item_id in ids_to_remove: - self._items_by_id.pop(item_id) - self._parent_id_by_id.pop(item_id) - - self._is_refreshing = False - self.refreshed.emit() - - -class FoldersWidget(QtWidgets.QWidget): - """Folders widget. - - Widget that handles folders view, model and selection. - - Args: - controller (AbstractWorkfilesFrontend): The control object. - parent (QtWidgets.QWidget): The parent widget. - """ - - def __init__(self, controller, parent): - super(FoldersWidget, self).__init__(parent) - - folders_view = DeselectableTreeView(self) - folders_view.setHeaderHidden(True) - - folders_model = FoldersModel(controller) - folders_proxy_model = RecursiveSortFilterProxyModel() - folders_proxy_model.setSourceModel(folders_model) - - folders_view.setModel(folders_proxy_model) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(folders_view, 1) - - controller.register_event_callback( - "folders.refresh.finished", - self._on_folders_refresh_finished - ) - controller.register_event_callback( - "controller.refresh.finished", - self._on_controller_refresh - ) - controller.register_event_callback( - "expected_selection_changed", - self._on_expected_selection_change - ) - - selection_model = folders_view.selectionModel() - selection_model.selectionChanged.connect(self._on_selection_change) - - folders_model.refreshed.connect(self._on_model_refresh) - - self._controller = controller - self._folders_view = folders_view - self._folders_model = folders_model - self._folders_proxy_model = folders_proxy_model - - self._expected_selection = None - - def set_name_filter(self, name): - self._folders_proxy_model.setFilterFixedString(name) - - def _clear(self): - self._folders_model.clear() - - def _on_folders_refresh_finished(self, event): - if event["sender"] != SENDER_NAME: - self._folders_model.refresh() - - def _on_controller_refresh(self): - self._update_expected_selection() - - def _update_expected_selection(self, expected_data=None): - if expected_data is None: - expected_data = self._controller.get_expected_selection_data() - - # We're done - if expected_data["folder_selected"]: - return - - folder_id = expected_data["folder_id"] - self._expected_selection = folder_id - if not self._folders_model.is_refreshing: - self._set_expected_selection() - - def _set_expected_selection(self): - folder_id = self._expected_selection - self._expected_selection = None - if ( - folder_id is not None - and folder_id != self._get_selected_item_id() - ): - index = self._folders_model.get_index_by_id(folder_id) - if index.isValid(): - proxy_index = self._folders_proxy_model.mapFromSource(index) - self._folders_view.setCurrentIndex(proxy_index) - self._controller.expected_folder_selected(folder_id) - - def _on_model_refresh(self): - if self._expected_selection: - self._set_expected_selection() - self._folders_proxy_model.sort(0) - - def _on_expected_selection_change(self, event): - self._update_expected_selection(event.data) - - def _get_selected_item_id(self): - selection_model = self._folders_view.selectionModel() - for index in selection_model.selectedIndexes(): - item_id = index.data(ITEM_ID_ROLE) - if item_id is not None: - return item_id - return None - - def _on_selection_change(self): - item_id = self._get_selected_item_id() - self._controller.set_selected_folder(item_id) diff --git a/openpype/tools/ayon_workfiles/widgets/side_panel.py b/openpype/tools/ayon_workfiles/widgets/side_panel.py index 7f06576a00..5085f4701e 100644 --- a/openpype/tools/ayon_workfiles/widgets/side_panel.py +++ b/openpype/tools/ayon_workfiles/widgets/side_panel.py @@ -66,7 +66,7 @@ class SidePanelWidget(QtWidgets.QWidget): btn_note_save.clicked.connect(self._on_save_click) controller.register_event_callback( - "workarea.selection.changed", self._on_selection_change + "selection.workarea.changed", self._on_selection_change ) self._details_input = details_input diff --git a/openpype/tools/ayon_workfiles/widgets/tasks_widget.py b/openpype/tools/ayon_workfiles/widgets/tasks_widget.py deleted file mode 100644 index 04f5b286b1..0000000000 --- a/openpype/tools/ayon_workfiles/widgets/tasks_widget.py +++ /dev/null @@ -1,420 +0,0 @@ -import uuid -import qtawesome -from qtpy import QtWidgets, QtGui, QtCore - -from openpype.style import get_disabled_entity_icon_color -from openpype.tools.utils import DeselectableTreeView - -from .constants import ( - ITEM_NAME_ROLE, - ITEM_ID_ROLE, - PARENT_ID_ROLE, -) - -SENDER_NAME = "qt_tasks_model" - - -class RefreshThread(QtCore.QThread): - """Thread for refreshing tasks. - - Call controller to get tasks and emit signal when finished. - - Args: - controller (AbstractWorkfilesFrontend): The control object. - folder_id (str): Folder id. - """ - - refresh_finished = QtCore.Signal(str) - - def __init__(self, controller, folder_id): - super(RefreshThread, self).__init__() - self._id = uuid.uuid4().hex - self._controller = controller - self._folder_id = folder_id - self._result = None - - @property - def id(self): - return self._id - - def run(self): - self._result = self._controller.get_task_items( - self._folder_id, SENDER_NAME) - self.refresh_finished.emit(self.id) - - def get_result(self): - return self._result - - -class TasksModel(QtGui.QStandardItemModel): - """Tasks model which cares about refresh of tasks by folder id. - - Args: - controller (AbstractWorkfilesFrontend): The control object. - """ - - refreshed = QtCore.Signal() - - def __init__(self, controller): - super(TasksModel, self).__init__() - - self._controller = controller - - self._items_by_name = {} - self._has_content = False - self._is_refreshing = False - - self._invalid_selection_item_used = False - self._invalid_selection_item = None - self._empty_tasks_item_used = False - self._empty_tasks_item = None - - self._last_folder_id = None - - self._refresh_threads = {} - self._current_refresh_thread = None - - # Initial state - self._add_invalid_selection_item() - - def clear(self): - self._items_by_name = {} - self._has_content = False - self._remove_invalid_items() - super(TasksModel, self).clear() - - def refresh(self, folder_id): - """Refresh tasks for folder. - - Args: - folder_id (Union[str, None]): Folder id. - """ - - self._refresh(folder_id) - - def get_index_by_name(self, task_name): - """Find item by name and return its index. - - Returns: - QtCore.QModelIndex: Index of item. Is invalid if task is not - found by name. - """ - - item = self._items_by_name.get(task_name) - if item is None: - return QtCore.QModelIndex() - return self.indexFromItem(item) - - def get_last_folder_id(self): - """Get last refreshed folder id. - - Returns: - Union[str, None]: Folder id. - """ - - return self._last_folder_id - - def _get_invalid_selection_item(self): - if self._invalid_selection_item is None: - item = QtGui.QStandardItem("Select a folder") - item.setFlags(QtCore.Qt.NoItemFlags) - icon = qtawesome.icon( - "fa.times", - color=get_disabled_entity_icon_color() - ) - item.setData(icon, QtCore.Qt.DecorationRole) - self._invalid_selection_item = item - return self._invalid_selection_item - - def _get_empty_task_item(self): - if self._empty_tasks_item is None: - item = QtGui.QStandardItem("No task") - icon = qtawesome.icon( - "fa.exclamation-circle", - color=get_disabled_entity_icon_color() - ) - item.setData(icon, QtCore.Qt.DecorationRole) - item.setFlags(QtCore.Qt.NoItemFlags) - self._empty_tasks_item = item - return self._empty_tasks_item - - def _add_invalid_item(self, item): - self.clear() - root_item = self.invisibleRootItem() - root_item.appendRow(item) - - def _remove_invalid_item(self, item): - root_item = self.invisibleRootItem() - root_item.takeRow(item.row()) - - def _remove_invalid_items(self): - self._remove_invalid_selection_item() - self._remove_empty_task_item() - - def _add_invalid_selection_item(self): - if not self._invalid_selection_item_used: - self._add_invalid_item(self._get_invalid_selection_item()) - self._invalid_selection_item_used = True - - def _remove_invalid_selection_item(self): - if self._invalid_selection_item: - self._remove_invalid_item(self._get_invalid_selection_item()) - self._invalid_selection_item_used = False - - def _add_empty_task_item(self): - if not self._empty_tasks_item_used: - self._add_invalid_item(self._get_empty_task_item()) - self._empty_tasks_item_used = True - - def _remove_empty_task_item(self): - if self._empty_tasks_item_used: - self._remove_invalid_item(self._get_empty_task_item()) - self._empty_tasks_item_used = False - - def _refresh(self, folder_id): - self._is_refreshing = True - self._last_folder_id = folder_id - if not folder_id: - self._add_invalid_selection_item() - self._current_refresh_thread = None - self._is_refreshing = False - self.refreshed.emit() - return - - thread = RefreshThread(self._controller, folder_id) - self._current_refresh_thread = thread.id - self._refresh_threads[thread.id] = thread - thread.refresh_finished.connect(self._on_refresh_thread) - thread.start() - - def _on_refresh_thread(self, thread_id): - """Callback when refresh thread is finished. - - Technically can be running multiple refresh threads at the same time, - to avoid using values from wrong thread, we check if thread id is - current refresh thread id. - - Tasks are stored by name, so if a folder has same task name as - previously selected folder it keeps the selection. - - Args: - thread_id (str): Thread id. - """ - - thread = self._refresh_threads.pop(thread_id) - if thread_id != self._current_refresh_thread: - return - - task_items = thread.get_result() - # Task items are refreshed - if task_items is None: - return - - # No tasks are available on folder - if not task_items: - self._add_empty_task_item() - return - self._remove_invalid_items() - - new_items = [] - new_names = set() - for task_item in task_items: - name = task_item.name - new_names.add(name) - item = self._items_by_name.get(name) - if item is None: - item = QtGui.QStandardItem() - item.setEditable(False) - new_items.append(item) - self._items_by_name[name] = item - - # TODO cache locally - icon = qtawesome.icon( - task_item.icon_name, - color=task_item.icon_color, - ) - item.setData(task_item.label, QtCore.Qt.DisplayRole) - item.setData(name, ITEM_NAME_ROLE) - item.setData(task_item.id, ITEM_ID_ROLE) - item.setData(task_item.parent_id, PARENT_ID_ROLE) - item.setData(icon, QtCore.Qt.DecorationRole) - - root_item = self.invisibleRootItem() - - for name in set(self._items_by_name) - new_names: - item = self._items_by_name.pop(name) - root_item.removeRow(item.row()) - - if new_items: - root_item.appendRows(new_items) - - self._has_content = root_item.rowCount() > 0 - self._is_refreshing = False - self.refreshed.emit() - - @property - def is_refreshing(self): - """Model is refreshing. - - Returns: - bool: Model is refreshing - """ - - return self._is_refreshing - - @property - def has_content(self): - """Model has content. - - Returns: - bools: Have at least one task. - """ - - return self._has_content - - def headerData(self, section, orientation, role): - # Show nice labels in the header - if ( - role == QtCore.Qt.DisplayRole - and orientation == QtCore.Qt.Horizontal - ): - if section == 0: - return "Tasks" - - return super(TasksModel, self).headerData( - section, orientation, role - ) - - -class TasksWidget(QtWidgets.QWidget): - """Tasks widget. - - Widget that handles tasks view, model and selection. - - Args: - controller (AbstractWorkfilesFrontend): Workfiles controller. - """ - - def __init__(self, controller, parent): - super(TasksWidget, self).__init__(parent) - - tasks_view = DeselectableTreeView(self) - tasks_view.setIndentation(0) - - tasks_model = TasksModel(controller) - tasks_proxy_model = QtCore.QSortFilterProxyModel() - tasks_proxy_model.setSourceModel(tasks_model) - - tasks_view.setModel(tasks_proxy_model) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(tasks_view, 1) - - controller.register_event_callback( - "tasks.refresh.finished", - self._on_tasks_refresh_finished - ) - controller.register_event_callback( - "selection.folder.changed", - self._folder_selection_changed - ) - controller.register_event_callback( - "expected_selection_changed", - self._on_expected_selection_change - ) - - selection_model = tasks_view.selectionModel() - selection_model.selectionChanged.connect(self._on_selection_change) - - tasks_model.refreshed.connect(self._on_tasks_model_refresh) - - self._controller = controller - self._tasks_view = tasks_view - self._tasks_model = tasks_model - self._tasks_proxy_model = tasks_proxy_model - - self._selected_folder_id = None - - self._expected_selection_data = None - - def _clear(self): - self._tasks_model.clear() - - def _on_tasks_refresh_finished(self, event): - """Tasks were refreshed in controller. - - Ignore if refresh was triggered by tasks model, or refreshed folder is - not the same as currently selected folder. - - Args: - event (Event): Event object. - """ - - # Refresh only if current folder id is the same - if ( - event["sender"] == SENDER_NAME - or event["folder_id"] != self._selected_folder_id - ): - return - self._tasks_model.refresh(self._selected_folder_id) - - def _folder_selection_changed(self, event): - self._selected_folder_id = event["folder_id"] - self._tasks_model.refresh(self._selected_folder_id) - - def _on_tasks_model_refresh(self): - if not self._set_expected_selection(): - self._on_selection_change() - self._tasks_proxy_model.sort(0) - - def _set_expected_selection(self): - if self._expected_selection_data is None: - return False - folder_id = self._expected_selection_data["folder_id"] - task_name = self._expected_selection_data["task_name"] - self._expected_selection_data = None - model_folder_id = self._tasks_model.get_last_folder_id() - if folder_id != model_folder_id: - return False - if task_name is not None: - index = self._tasks_model.get_index_by_name(task_name) - if index.isValid(): - proxy_index = self._tasks_proxy_model.mapFromSource(index) - self._tasks_view.setCurrentIndex(proxy_index) - self._controller.expected_task_selected(folder_id, task_name) - return True - - def _on_expected_selection_change(self, event): - if event["task_selected"] or not event["folder_selected"]: - return - - model_folder_id = self._tasks_model.get_last_folder_id() - folder_id = event["folder_id"] - self._expected_selection_data = { - "task_name": event["task_name"], - "folder_id": folder_id, - } - - if folder_id != model_folder_id or self._tasks_model.is_refreshing: - return - self._set_expected_selection() - - def _get_selected_item_ids(self): - selection_model = self._tasks_view.selectionModel() - for index in selection_model.selectedIndexes(): - task_id = index.data(ITEM_ID_ROLE) - task_name = index.data(ITEM_NAME_ROLE) - parent_id = index.data(PARENT_ID_ROLE) - if task_name is not None: - return parent_id, task_id, task_name - return self._selected_folder_id, None, None - - def _on_selection_change(self): - # Don't trigger task change during refresh - # - a task was deselected if that happens - # - can cause crash triggered during tasks refreshing - if self._tasks_model.is_refreshing: - return - parent_id, task_id, task_name = self._get_selected_item_ids() - self._controller.set_selected_task(parent_id, task_id, task_name) diff --git a/openpype/tools/ayon_workfiles/widgets/window.py b/openpype/tools/ayon_workfiles/widgets/window.py index 6218d2dd06..eb2f2bc1c7 100644 --- a/openpype/tools/ayon_workfiles/widgets/window.py +++ b/openpype/tools/ayon_workfiles/widgets/window.py @@ -5,32 +5,16 @@ from openpype.tools.utils import ( PlaceholderLineEdit, MessageOverlayObject, ) -from openpype.tools.utils.lib import get_qta_icon_by_name_and_color +from openpype.tools.ayon_utils.widgets import FoldersWidget, TasksWidget from openpype.tools.ayon_workfiles.control import BaseWorkfileController +from openpype.tools.utils import GoToCurrentButton, RefreshButton from .side_panel import SidePanelWidget -from .folders_widget import FoldersWidget -from .tasks_widget import TasksWidget from .files_widget import FilesWidget from .utils import BaseOverlayFrame -# TODO move to utils -# from openpype.tools.utils.lib import ( -# get_refresh_icon, get_go_to_current_icon) -def get_refresh_icon(): - return get_qta_icon_by_name_and_color( - "fa.refresh", style.get_default_tools_icon_color() - ) - - -def get_go_to_current_icon(): - return get_qta_icon_by_name_and_color( - "fa.arrow-down", style.get_default_tools_icon_color() - ) - - class InvalidHostOverlay(BaseOverlayFrame): def __init__(self, parent): super(InvalidHostOverlay, self).__init__(parent) @@ -80,7 +64,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._default_window_flags = flags - self._folder_widget = None + self._folders_widget = None self._folder_filter_input = None self._files_widget = None @@ -100,7 +84,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget): home_body_widget = QtWidgets.QWidget(home_page_widget) col_1_widget = self._create_col_1_widget(controller, parent) - tasks_widget = TasksWidget(controller, home_body_widget) + tasks_widget = TasksWidget( + controller, home_body_widget, handle_expected_selection=True + ) col_3_widget = self._create_col_3_widget(controller, home_body_widget) side_panel = SidePanelWidget(controller, home_body_widget) @@ -151,11 +137,11 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._on_open_finished ) controller.register_event_callback( - "controller.refresh.started", + "controller.reset.started", self._on_controller_refresh_started, ) controller.register_event_callback( - "controller.refresh.finished", + "controller.reset.finished", self._on_controller_refresh_finished, ) @@ -188,19 +174,12 @@ class WorkfilesToolWindow(QtWidgets.QWidget): folder_filter_input = PlaceholderLineEdit(header_widget) folder_filter_input.setPlaceholderText("Filter folders..") - go_to_current_btn = QtWidgets.QPushButton(header_widget) - go_to_current_btn.setIcon(get_go_to_current_icon()) - go_to_current_btn_sp = go_to_current_btn.sizePolicy() - go_to_current_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) - go_to_current_btn.setSizePolicy(go_to_current_btn_sp) + go_to_current_btn = GoToCurrentButton(header_widget) + refresh_btn = RefreshButton(header_widget) - refresh_btn = QtWidgets.QPushButton(header_widget) - refresh_btn.setIcon(get_refresh_icon()) - refresh_btn_sp = refresh_btn.sizePolicy() - refresh_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) - refresh_btn.setSizePolicy(refresh_btn_sp) - - folder_widget = FoldersWidget(controller, col_widget) + folder_widget = FoldersWidget( + controller, col_widget, handle_expected_selection=True + ) header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) @@ -218,7 +197,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): refresh_btn.clicked.connect(self._on_refresh_clicked) self._folder_filter_input = folder_filter_input - self._folder_widget = folder_widget + self._folders_widget = folder_widget return col_widget @@ -300,7 +279,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): def refresh(self): """Trigger refresh of workfiles tool controller.""" - self._controller.refresh() + self._controller.reset() def showEvent(self, event): super(WorkfilesToolWindow, self).showEvent(event) @@ -338,7 +317,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._side_panel.set_published_mode(published_mode) def _on_folder_filter_change(self, text): - self._folder_widget.set_name_filter(text) + self._folders_widget.set_name_filter(text) def _on_go_to_current_clicked(self): self._controller.go_to_current_context() @@ -357,6 +336,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget): if not self._host_is_valid: return + self._folders_widget.set_project_name( + self._controller.get_current_project_name() + ) + def _on_save_as_finished(self, event): if event["failed"]: self._overlay_messages_widget.add_message( From e340b9c7bc772d41c272bdbb4dfd42321c03df5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 10:57:09 +0100 Subject: [PATCH 272/298] rename function 'asset_name' to 'prepare_scene_name' --- openpype/hosts/blender/api/plugin.py | 10 +++++----- openpype/hosts/blender/plugins/create/create_action.py | 2 +- openpype/hosts/blender/plugins/load/import_workfile.py | 2 +- openpype/hosts/blender/plugins/load/load_abc.py | 4 ++-- openpype/hosts/blender/plugins/load/load_action.py | 10 +++++----- openpype/hosts/blender/plugins/load/load_audio.py | 4 ++-- openpype/hosts/blender/plugins/load/load_blend.py | 4 ++-- openpype/hosts/blender/plugins/load/load_blendscene.py | 4 ++-- openpype/hosts/blender/plugins/load/load_camera_abc.py | 4 ++-- openpype/hosts/blender/plugins/load/load_camera_fbx.py | 4 ++-- openpype/hosts/blender/plugins/load/load_fbx.py | 4 ++-- .../hosts/blender/plugins/load/load_layout_json.py | 4 ++-- openpype/hosts/blender/plugins/load/load_look.py | 4 ++-- 13 files changed, 30 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 7ac12b5549..d7155a1b53 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -28,7 +28,7 @@ from .lib import imprint VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] -def asset_name( +def prepare_scene_name( asset: str, subset: str, namespace: Optional[str] = None ) -> str: """Return a consistent name for an asset.""" @@ -225,7 +225,7 @@ class BaseCreator(Creator): bpy.context.scene.collection.children.link(instances) # Create asset group - name = asset_name(instance_data["asset"], subset_name) + name = prepare_scene_name(instance_data["asset"], subset_name) if self.create_as_asset_group: # Create instance as empty instance_node = bpy.data.objects.new(name=name, object_data=None) @@ -298,7 +298,7 @@ class BaseCreator(Creator): "subset" in changes.changed_keys or "asset" in changes.changed_keys ): - name = asset_name(asset=data["asset"], subset=data["subset"]) + name = prepare_scene_name(asset=data["asset"], subset=data["subset"]) node.name = name imprint(node, data) @@ -454,7 +454,7 @@ class AssetLoader(LoaderPlugin): asset, subset ) namespace = namespace or f"{asset}_{unique_number}" - name = name or asset_name( + name = name or prepare_scene_name( asset, subset, unique_number ) @@ -483,7 +483,7 @@ class AssetLoader(LoaderPlugin): # asset = context["asset"]["name"] # subset = context["subset"]["name"] - # instance_name = asset_name(asset, subset, unique_number) + '_CON' + # instance_name = prepare_scene_name(asset, subset, unique_number) + '_CON' # return self._get_instance_collection(instance_name, nodes) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 0929778d78..caaa72fe8d 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -22,7 +22,7 @@ class CreateAction(plugin.BaseCreator): ) # Get instance name - name = plugin.asset_name(instance_data["asset"], subset_name) + name = plugin.prepare_scene_name(instance_data["asset"], subset_name) if pre_create_data.get("use_selection"): for obj in lib.get_selection(): diff --git a/openpype/hosts/blender/plugins/load/import_workfile.py b/openpype/hosts/blender/plugins/load/import_workfile.py index 4f5016d422..331f6a8bdb 100644 --- a/openpype/hosts/blender/plugins/load/import_workfile.py +++ b/openpype/hosts/blender/plugins/load/import_workfile.py @@ -7,7 +7,7 @@ def append_workfile(context, fname, do_import): asset = context['asset']['name'] subset = context['subset']['name'] - group_name = plugin.asset_name(asset, subset) + group_name = plugin.prepare_scene_name(asset, subset) # We need to preserve the original names of the scenes, otherwise, # if there are duplicate names in the current workfile, the imported diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 8d1863d4d5..d7e82d1900 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -137,9 +137,9 @@ class CacheModelLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" containers = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_action.py b/openpype/hosts/blender/plugins/load/load_action.py index 3447e67ebf..f7d32f92a5 100644 --- a/openpype/hosts/blender/plugins/load/load_action.py +++ b/openpype/hosts/blender/plugins/load/load_action.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional import bpy from openpype.pipeline import get_representation_path -import openpype.hosts.blender.api.plugin +from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( containerise_existing, AVALON_PROPERTY, @@ -16,7 +16,7 @@ from openpype.hosts.blender.api.pipeline import ( logger = logging.getLogger("openpype").getChild("blender").getChild("load_action") -class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader): +class BlendActionLoader(plugin.AssetLoader): """Load action from a .blend file. Warning: @@ -46,8 +46,8 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader): libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - container_name = openpype.hosts.blender.api.plugin.asset_name( + lib_container = plugin.prepare_scene_name(asset, subset) + container_name = plugin.prepare_scene_name( asset, subset, namespace ) @@ -152,7 +152,7 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader): assert libpath.is_file(), ( f"The file doesn't exist: {libpath}" ) - assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, ( + assert extension in plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) diff --git a/openpype/hosts/blender/plugins/load/load_audio.py b/openpype/hosts/blender/plugins/load/load_audio.py index ac8f363316..1e5bd39a32 100644 --- a/openpype/hosts/blender/plugins/load/load_audio.py +++ b/openpype/hosts/blender/plugins/load/load_audio.py @@ -42,9 +42,9 @@ class AudioLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 8b1af5a0da..f437e66795 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -133,9 +133,9 @@ class BlendLoader(plugin.AssetLoader): representation = str(context["representation"]["_id"]) - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 2c955af9e8..6cc7f39d03 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -85,9 +85,9 @@ class BlendSceneLoader(plugin.AssetLoader): except ValueError: family = "model" - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py index 05d3fb764d..ecd6bb98f1 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_abc.py +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -87,9 +87,9 @@ class AbcCameraLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index 3cca6e7fd3..2d53d3e573 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -90,9 +90,9 @@ class FbxCameraLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_fbx.py b/openpype/hosts/blender/plugins/load/load_fbx.py index e129ea6754..8fce53a5d5 100644 --- a/openpype/hosts/blender/plugins/load/load_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_fbx.py @@ -134,9 +134,9 @@ class FbxModelLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index a941c77a8e..748ac619b6 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -149,9 +149,9 @@ class JsonLayoutLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - asset_name = plugin.asset_name(asset, subset) + asset_name = plugin.prepare_scene_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) + group_name = plugin.prepare_scene_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) diff --git a/openpype/hosts/blender/plugins/load/load_look.py b/openpype/hosts/blender/plugins/load/load_look.py index c121f55633..8d3118d83b 100644 --- a/openpype/hosts/blender/plugins/load/load_look.py +++ b/openpype/hosts/blender/plugins/load/load_look.py @@ -96,14 +96,14 @@ class BlendLookLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = plugin.asset_name( + lib_container = plugin.prepare_scene_name( asset, subset ) unique_number = plugin.get_unique_number( asset, subset ) namespace = namespace or f"{asset}_{unique_number}" - container_name = plugin.asset_name( + container_name = plugin.prepare_scene_name( asset, subset, unique_number ) From e8f7f146ab670e5c21a8374143456858df80260b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 11:02:29 +0100 Subject: [PATCH 273/298] formatting fix --- openpype/hosts/blender/api/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index d7155a1b53..8d33187da3 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -298,7 +298,9 @@ class BaseCreator(Creator): "subset" in changes.changed_keys or "asset" in changes.changed_keys ): - name = prepare_scene_name(asset=data["asset"], subset=data["subset"]) + name = prepare_scene_name( + asset=data["asset"], subset=data["subset"] + ) node.name = name imprint(node, data) @@ -483,7 +485,9 @@ class AssetLoader(LoaderPlugin): # asset = context["asset"]["name"] # subset = context["subset"]["name"] - # instance_name = prepare_scene_name(asset, subset, unique_number) + '_CON' + # instance_name = prepare_scene_name( + # asset, subset, unique_number + # ) + '_CON' # return self._get_instance_collection(instance_name, nodes) From 4cd7d72dce91c88cf7e1aa36f44e439606eb227b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 11:12:00 +0100 Subject: [PATCH 274/298] use folder path in AYON mode --- openpype/hosts/nuke/api/lib.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8b1ba0ab0d..51096b5dcc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -13,6 +13,7 @@ from collections import OrderedDict import nuke from qtpy import QtCore, QtWidgets +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_project, get_asset_by_name, @@ -1107,7 +1108,9 @@ def format_anatomy(data): Return: path (str) ''' - anatomy = Anatomy() + + project_name = get_current_project_name() + anatomy = Anatomy(project_name) log.debug("__ anatomy.templates: {}".format(anatomy.templates)) padding = None @@ -1125,8 +1128,10 @@ def format_anatomy(data): file = script_name() data["version"] = get_version_from_path(file) - project_name = anatomy.project_name - asset_name = data["asset"] + if AYON_SERVER_ENABLED: + asset_name = data["folderPath"] + else: + asset_name = data["asset"] task_name = data["task"] host_name = get_current_host_name() context_data = get_template_data_with_names( From e0790a1cb1d5660ea887b387d44e422575111c7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 11:26:54 +0100 Subject: [PATCH 275/298] fix create logic in blender --- openpype/hosts/blender/api/plugin.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 8d33187da3..568d8f6695 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,11 +6,11 @@ from typing import Dict, List, Optional import bpy +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import ( Creator, CreatedInstance, LoaderPlugin, - get_current_task_name, ) from openpype.lib import BoolDef @@ -225,7 +225,12 @@ class BaseCreator(Creator): bpy.context.scene.collection.children.link(instances) # Create asset group - name = prepare_scene_name(instance_data["asset"], subset_name) + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] + + name = prepare_scene_name(asset_name, subset_name) if self.create_as_asset_group: # Create instance as empty instance_node = bpy.data.objects.new(name=name, object_data=None) @@ -281,7 +286,14 @@ class BaseCreator(Creator): Args: update_list(List[UpdateData]): Changed instances - and their changes, as a list of tuples.""" + and their changes, as a list of tuples. + """ + + if AYON_SERVER_ENABLED: + asset_name_key = "folderPath" + else: + asset_name_key = "asset" + for created_instance, changes in update_list: data = created_instance.data_to_store() node = created_instance.transient_data["instance_node"] @@ -295,11 +307,12 @@ class BaseCreator(Creator): # Rename the instance node in the scene if subset or asset changed if ( - "subset" in changes.changed_keys - or "asset" in changes.changed_keys + "subset" in changes.changed_keys + or asset_name_key in changes.changed_keys ): + asset_name = data[asset_name_key] name = prepare_scene_name( - asset=data["asset"], subset=data["subset"] + asset=asset_name, subset=data["subset"] ) node.name = name From 781a1047de260a8afa570b2e67c957fb5622fac4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 16 Nov 2023 12:45:04 +0000 Subject: [PATCH 276/298] [Automated] Release --- CHANGELOG.md | 380 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 382 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3daf581ac..5909c26f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,386 @@ # Changelog +## [3.17.6](https://github.com/ynput/OpenPype/tree/3.17.6) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.5...3.17.6) + +### **🚀 Enhancements** + + +
+Testing: Validate Maya Logs #5775 + +This PR adds testing of the logs within Maya such as Python and Pyblish errors.The reason why we need to touch so many files outside of Maya is because of the pyblish errors below; +``` +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "collect_otio_review" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "collect_otio_review" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "extract_otio_file" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "extract_otio_file" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "extract_otio_review" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "extract_otio_review" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio') +# Error: pyblish.plugin : Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "submit_blender_deadline" (No module named 'bpy') +# Error: pyblish.plugin : Skipped: "submit_blender_deadline" (No module named 'bpy') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "submit_houdini_remote_publish" (No module named 'hou') +# Error: pyblish.plugin : Skipped: "submit_houdini_remote_publish" (No module named 'hou') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "submit_houdini_render_deadline" (No module named 'hou') +# Error: pyblish.plugin : Skipped: "submit_houdini_render_deadline" (No module named 'hou') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "submit_max_deadline" (No module named 'pymxs') +# Error: pyblish.plugin : Skipped: "submit_max_deadline" (No module named 'pymxs') # +pyblish (ERROR) (line: 1371) pyblish.plugin: +Skipped: "submit_nuke_deadline" (No module named 'nuke') +# Error: pyblish.plugin : Skipped: "submit_nuke_deadline" (No module named 'nuke') # +``` +We also needed to `stdout` and `stderr` from the launched application to capture the output.Split from #5644.Dependent on #5734 + + +___ + +
+ + +
+Maya: Render Settings cleanup remove global `RENDER_ATTRS` #5801 + +Remove global `lib.RENDER_ATTRS` and implement a `RenderSettings.get_padding_attr(renderer)` method instead. + + +___ + +
+ + +
+Testing: Ingest expected files and input workfile #5840 + +This ingests the Maya workfile from the Drive storage. Have changed the format to MayaAscii so its easier to see what changes are happening in a PR. This meant changing the expected files and database entries as well. + + +___ + +
+ + +
+Chore: Create plugin auto-apply settings #5908 + +Create plugins can auto-apply settings. + + +___ + +
+ + +
+Resolve: Add save current file button + "Save" shortcut when menu is active #5691 + +Adds a "Save current file" to the OpenPype menu.Also adds a "Save" shortcut key sequence (CTRL+S on Windows) to the button, so that clicking CTRL+S when the menu is active will save the current workfile. However this of course does not work if the menu does not receive the key press event (e.g. when Resolve UI is active instead)Resolves #5684 + + +___ + +
+ + +
+Reference USD file as maya native geometry #5781 + +Add MayaUsdReferenceLoader to reference USD as Maya native geometry using `mayaUSDImport` file translator. + + +___ + +
+ + +
+Max: Bug fix on wrong aspect ratio and viewport not being maximized during context in review family #5839 + +This PR will fix the bug on wrong aspect ratio and viewport not being maximized when creating preview animationBesides, the support of tga image format and the options for AA quality are implemented in this PR + + +___ + +
+ + +
+Blender: Incorporate blender "Collections" into Publish/Load #5841 + +Allow `blendScene` family to include collections. + + +___ + +
+ + +
+Max: Allows user preset the setting of preview animation in OP/AYON Setting #5859 + +Allows user preset the setting of preview animation in OP/AYON Setting for review family. +- [x] Openpype +- [x] AYON + + +___ + +
+ + +
+Publisher: Center publisher window on first show #5877 + +Move publisher window to center of a screen on first show. + + +___ + +
+ + +
+Publisher: Instance context changes confirm works #5881 + +Confirmation of context changes in publisher on existing instances does not cause glitches. + + +___ + +
+ + +
+AYON workfiles tools: Revisit workfiles tool #5897 + +Revisited workfiles tool for AYON mode to reuse common models and widgets. + + +___ + +
+ + +
+Nuke: updated colorspace settings #5906 + +Updating nuke colorspace settings into more convenient way with usage of ocio config roles rather then particular colorspace names. This way we should not have troubles to switch between linear Rec709 or ACES configs without any additional settings changes. + + +___ + +
+ + +
+Blender: Refactor to new publisher #5910 + +Refactor Blender integration to use the new publisher + + +___ + +
+ + +
+Enhancement: Some publish logs cosmetics #5917 + +General logging message tweaks: +- Sort some lists of folder/filenames so they appear sorted in the logs +- Fix some grammar / typos +- In some cases provide slightly more information in a log + + +___ + +
+ + +
+Blender: Better name of 'asset_name' function #5927 + +Renamed function `asset_name` to `prepare_scene_name`. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Bug fix the fbx animation export errored out when the skeletonAnim set is empty #5875 + +Resolve this bug discordIf the skeletonAnim SET is empty and fbx animation collect, the fbx animation extractor would skip the fbx extraction + + +___ + +
+ + +
+Bugfix: fix few typos in houdini's and Maya's Ayon settings #5882 + +Fixing few typos +- [x] Maya unreal static mesh +- [x] Houdini static mesh +- [x] Houdini collect asset handles + + +___ + +
+ + +
+Bugfix: Ayon Deadline env vars + error message on no executable found #5815 + +Fix some Ayon x Deadline issues as came up in this topic: +- missing Environment Variables issue explained here for `deadlinePlugin.RunProcess` for the AYON _extract environments_ call. +- wrong error formatting described here with a `;` between each character like this: `Ayon executable was not found in the semicolon separated list "C;:;/;P;r;o;g;r;a;m; ;F;i;l;e;s;/;Y;n;p;u;t;/;A;Y;O;N; ;1;.;0;.;0;-;b;e;t;a;.;5;/;a;y;o;n;_;c;o;n;s;o;l;e;.;e;x;e". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor.` + + +___ + +
+ + +
+AYON: Fix bundles access in settings #5856 + +Fixed access to bundles data in settings to define correct develop variant. + + +___ + +
+ + +
+AYON 3dsMax settings: 'ValidateAttributes' settings converte only if available #5878 + +Convert `ValidateAttributes` settings only if are available in AYON settings. + + +___ + +
+ + +
+AYON: Fix TrayPublisher editorial settings #5880 + +Fixing Traypublisher settings for adding task in simple editorial. + + +___ + +
+ + +
+TrayPublisher: editorial frame range check not needed #5884 + +Validator for frame ranges is not needed during editorial publishing since entity data are not yet in database. + + +___ + +
+ + +
+Update houdini license validator #5886 + +As reported in this community commentHoudini USD publishing is only restricted in Houdini apprentice. + + +___ + +
+ + +
+Blender: Fix blend extraction and packed images #5888 + +Fixed a with blend extractor and packed images. + + +___ + +
+ + +
+AYON: Initialize connection with all information #5890 + +Create global AYON api connection with all informations all the time. + + +___ + +
+ + +
+AYON: Scene inventory tool without site sync #5896 + +Skip 'get_site_icons' if site sync addon is disabled. + + +___ + +
+ + +
+Publish report tool: Fix PySide6 #5898 + +Use constants from classes instead of objects. + + +___ + +
+ + +
+fusion: removing hardcoded template name for saver #5907 + +Fusion is not hardcoded for `render` anatomy template only anymore. This was blocking AYON deployment. + + +___ + +
+ + + + ## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5) diff --git a/openpype/version.py b/openpype/version.py index b7394c203d..adb62abd9d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.6-nightly.3" +__version__ = "3.17.6" diff --git a/pyproject.toml b/pyproject.toml index c6f4880cdd..21ba7d1199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.5" # OpenPype +version = "3.17.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 95209df395b995782cc5bba872ca34d70bec21a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Nov 2023 12:46:03 +0000 Subject: [PATCH 277/298] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 86e3638ffe..6f1b01bd2f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.6 - 3.17.6-nightly.3 - 3.17.6-nightly.2 - 3.17.6-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.2-nightly.3 - 3.15.2-nightly.2 - 3.15.2-nightly.1 - - 3.15.1 validations: required: true - type: dropdown From 05673e934d6c872c953243d059d468a3bc9f1d77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:26:52 +0100 Subject: [PATCH 278/298] AYON: Loader tool bugs hunt (#5915) * handle cases when server is missing product type in project database * ignore representations which do not match uuid * add comments --- openpype/tools/ayon_loader/control.py | 18 ++++++++++++++++-- openpype/tools/ayon_loader/models/products.py | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_loader/control.py b/openpype/tools/ayon_loader/control.py index 2b779f5c2e..c38973d0b3 100644 --- a/openpype/tools/ayon_loader/control.py +++ b/openpype/tools/ayon_loader/control.py @@ -1,4 +1,5 @@ import logging +import uuid import ayon_api @@ -314,8 +315,21 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): containers = self._host.get_containers() else: containers = self._host.ls() - repre_ids = {c.get("representation") for c in containers} - repre_ids.discard(None) + repre_ids = set() + for container in containers: + repre_id = container.get("representation") + # Ignore invalid representation ids. + # - invalid representation ids may be available if e.g. is + # opened scene from OpenPype whe 'ObjectId' was used instead + # of 'uuid'. + # NOTE: Server call would crash if there is any invalid id. + # That would cause crash we won't get any information. + try: + uuid.UUID(repre_id) + repre_ids.add(repre_id) + except ValueError: + pass + product_ids = self._products_model.get_product_ids_by_repre_ids( project_name, repre_ids ) diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py index 33023cc164..816dabaf90 100644 --- a/openpype/tools/ayon_loader/models/products.py +++ b/openpype/tools/ayon_loader/models/products.py @@ -77,7 +77,15 @@ def product_item_from_entity( product_attribs = product_entity["attrib"] group = product_attribs.get("productGroup") product_type = product_entity["productType"] - product_type_item = product_type_items_by_name[product_type] + product_type_item = product_type_items_by_name.get(product_type) + # NOTE This is needed for cases when products were not created on server + # using api functions. In that case product type item may not be + # available and we need to create a default. + if product_type_item is None: + product_type_item = create_default_product_type_item(product_type) + # Cache the item for future use + product_type_items_by_name[product_type] = product_type_item + product_type_icon = product_type_item.icon product_icon = { @@ -117,6 +125,15 @@ def product_type_item_from_data(product_type_data): return ProductTypeItem(product_type_data["name"], icon, True) +def create_default_product_type_item(product_type): + icon = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductTypeItem(product_type, icon, True) + + class ProductsModel: """Model for products, version and representation. From a3fc30b408161b6d6aa116142661acffbf9659ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:27:33 +0100 Subject: [PATCH 279/298] use project, task and host name from context data (#5918) --- .../hosts/photoshop/plugins/publish/collect_auto_image.py | 4 ++-- .../hosts/photoshop/plugins/publish/collect_auto_review.py | 4 ++-- .../hosts/photoshop/plugins/publish/collect_auto_workfile.py | 4 ++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- .../modules/slack/plugins/publish/collect_slack_family.py | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index 77f1a3e91f..d4b5f480b1 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -22,9 +22,9 @@ class CollectAutoImage(pyblish.api.ContextPlugin): self.log.debug("Auto image instance found, won't create new") return - project_name = context.data["anatomyData"]["project"]["name"] + project_name = context.data["projectName"] proj_settings = context.data["project_settings"] - task_name = context.data["anatomyData"]["task"]["name"] + task_name = context.data["task"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] asset_name = asset_doc["name"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py index 82ba0ac09c..8964582a45 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -60,9 +60,9 @@ class CollectAutoReview(pyblish.api.ContextPlugin): variant = (context.data.get("variant") or auto_creator["default_variant"]) - project_name = context.data["anatomyData"]["project"]["name"] + project_name = context.data["projectName"] proj_settings = context.data["project_settings"] - task_name = context.data["anatomyData"]["task"]["name"] + task_name = context.data["task"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] asset_name = asset_doc["name"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py index 01dc50af40..d3cc8d44d0 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -51,7 +51,7 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): self.log.debug("Workfile instance disabled") return - project_name = context.data["anatomyData"]["project"]["name"] + project_name = context.data["projectName"] proj_settings = context.data["project_settings"] auto_creator = proj_settings.get( "photoshop", {}).get( @@ -66,7 +66,7 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): variant = (context.data.get("variant") or auto_creator["default_variant"]) - task_name = context.data["anatomyData"]["task"]["name"] + task_name = context.data["task"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] asset_name = asset_doc["name"] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 6ed5819f2b..c9019b496b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -708,6 +708,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, """ project_name = context.data["projectName"] + host_name = context.data["hostName"] if not version: version = get_last_version_by_subset_name( project_name, @@ -719,7 +720,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, else: version = get_versioning_start( project_name, - template_data["app"], + host_name, task_name=template_data["task"]["name"], task_type=template_data["task"]["type"], family="render", diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py index b3e7bbdcec..cbed2d1012 100644 --- a/openpype/modules/slack/plugins/publish/collect_slack_family.py +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -38,7 +38,7 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin, "families": family, "tasks": task_data.get("name"), "task_types": task_data.get("type"), - "hosts": instance.data["anatomyData"]["app"], + "hosts": instance.context.data["hostName"], "subsets": instance.data["subset"] } profile = filter_profiles(self.profiles, key_values, From 7c86115b7e40625e670e0fa69ca5e28929b3ee57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:41:41 +0100 Subject: [PATCH 280/298] AYON: Handle staging templates category (#5905) * do template replacements on all templates * handle and convert staging templates * use key 'staging' instead of 'staging_dir' --- openpype/client/server/conversion_utils.py | 45 ++++++++++++++++------ 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 8c18cb1c13..51af99e722 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -138,16 +138,22 @@ def _template_replacements_to_v3(template): ) -def _convert_template_item(template): - # Others won't have 'directory' - if "directory" not in template: - return - folder = _template_replacements_to_v3(template.pop("directory")) - template["folder"] = folder - template["file"] = _template_replacements_to_v3(template["file"]) - template["path"] = "/".join( - (folder, template["file"]) - ) +def _convert_template_item(template_item): + for key, value in tuple(template_item.items()): + template_item[key] = _template_replacements_to_v3(value) + + # Change 'directory' to 'folder' + if "directory" in template_item: + template_item["folder"] = template_item.pop("directory") + + if ( + "path" not in template_item + and "file" in template_item + and "folder" in template_item + ): + template_item["path"] = "/".join( + (template_item["folder"], template_item["file"]) + ) def _fill_template_category(templates, cat_templates, cat_key): @@ -212,10 +218,27 @@ def convert_v4_project_to_v3(project): _convert_template_item(template) new_others_templates[name] = template + staging_templates = templates.pop("staging", None) + # Key 'staging_directories' is legacy key that changed + # to 'staging_dir' + _legacy_staging_templates = templates.pop("staging_directories", None) + if staging_templates is None: + staging_templates = _legacy_staging_templates + + if staging_templates is None: + staging_templates = {} + + # Prefix all staging template names with 'staging_' prefix + # and add them to 'others' + for name, template in staging_templates.items(): + _convert_template_item(template) + new_name = "staging_{}".format(name) + new_others_templates[new_name] = template + for key in ( "work", "publish", - "hero" + "hero", ): cat_templates = templates.pop(key) _fill_template_category(templates, cat_templates, key) From fe8711cf7804336593de7d0ac1ff94ec6e1b3b13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:42:44 +0100 Subject: [PATCH 281/298] Publisher: Bugfixes and enhancements (#5924) * fix logger getter * catch crashes of create plugin initializations * use 'product' instead of 'subset' in AYON mode * fix import --- openpype/tools/publisher/control.py | 19 ++++++++++++++----- .../publisher/widgets/overview_widget.py | 7 ++++++- openpype/tools/publisher/widgets/widgets.py | 10 ++++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a6264303d5..3192fe949f 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1453,7 +1453,7 @@ class BasePublisherController(AbstractPublisherController): """ if self._log is None: - self._log = logging.getLogget(self.__class__.__name__) + self._log = logging.getLogger(self.__class__.__name__) return self._log @property @@ -1881,10 +1881,19 @@ class PublisherController(BasePublisherController): self._emit_event("plugins.refresh.finished") def _collect_creator_items(self): - return { - identifier: CreatorItem.from_creator(creator) - for identifier, creator in self._create_context.creators.items() - } + # TODO add crashed initialization of create plugins to report + output = {} + for identifier, creator in self._create_context.creators.items(): + try: + output[identifier] = CreatorItem.from_creator(creator) + except Exception: + self.log.error( + "Failed to create creator item for '%s'", + identifier, + exc_info=True + ) + + return output def _reset_instances(self): """Reset create instances.""" diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 778aa1139f..10151250f6 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -1,5 +1,7 @@ from qtpy import QtWidgets, QtCore +from openpype import AYON_SERVER_ENABLED + from .border_label_widget import BorderedLabelWidget from .card_view_widgets import InstanceCardView @@ -35,7 +37,10 @@ class OverviewWidget(QtWidgets.QFrame): # --- Created Subsets/Instances --- # Common widget for creation and overview subset_views_widget = BorderedLabelWidget( - "Subsets to publish", subset_content_widget + "{} to publish".format( + "Products" if AYON_SERVER_ENABLED else "Subsets" + ), + subset_content_widget ) subset_view_cards = InstanceCardView(controller, subset_views_widget) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 6dbeaad821..1860287fcf 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -210,7 +210,9 @@ class CreateBtn(PublishIconBtn): def __init__(self, parent=None): icon_path = get_icon_path("create") super(CreateBtn, self).__init__(icon_path, "Create", parent) - self.setToolTip("Create new subset/s") + self.setToolTip("Create new {}/s".format( + "product" if AYON_SERVER_ENABLED else "subset" + )) self.setLayoutDirection(QtCore.Qt.RightToLeft) @@ -655,7 +657,11 @@ class TasksCombobox(QtWidgets.QComboBox): self._proxy_model.set_filter_empty(invalid) if invalid: self._set_is_valid(False) - self.set_text("< One or more subsets require Task selected >") + self.set_text( + "< One or more {} require Task selected >".format( + "products" if AYON_SERVER_ENABLED else "subsets" + ) + ) else: self.set_text(None) From d9c8950f346886d25d09ff5e8fa6b4d4a9a49fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 16 Nov 2023 16:26:28 +0100 Subject: [PATCH 282/298] :bug: fix asset path use in maya skeletal mesh --- .../maya/plugins/create/create_unreal_skeletalmesh.py | 2 +- .../plugins/publish/extract_unreal_skeletalmesh_abc.py | 10 ---------- .../plugins/publish/extract_unreal_skeletalmesh_fbx.py | 4 ++++ 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py index 3c9a79156a..b4151bac99 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -51,7 +51,7 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator): # We reorganize the geometry that was originally added into the # set into either 'joints_SET' or 'geometry_SET' based on the # joint_hints from project settings - members = cmds.sets(instance_node, query=True) + members = cmds.sets(instance_node, query=True) or [] cmds.sets(clear=instance_node) geometry_set = cmds.sets(name="geometry_SET", empty=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py index 9c2f55a1ef..780ed2377c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py @@ -13,16 +13,6 @@ from openpype.hosts.maya.api.lib import ( ) -@contextmanager -def renamed(original_name, renamed_name): - # type: (str, str) -> None - try: - cmds.rename(original_name, renamed_name) - yield - finally: - cmds.rename(renamed_name, original_name) - - class ExtractUnrealSkeletalMeshAbc(publish.Extractor): """Extract Unreal Skeletal Mesh as FBX from Maya. """ diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py index 96175a07d7..4b36134694 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py @@ -62,6 +62,10 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor): original_parent = to_extract[0].split("|")[1] parent_node = instance.data.get("asset") + # this needs to be done for AYON + # WARNING: since AYON supports duplicity of asset names, + # this needs to be refactored throughout the pipeline. + parent_node = parent_node.split("/")[-1] renamed_to_extract = [] for node in to_extract: From 19f0a77966dbc84898e48425b06015416c48eeef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:22:04 +0100 Subject: [PATCH 283/298] Nuke: Change context label enhancement (#5887) * use QAction to change label of context action * do not handle context label change in nuke assist --- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/nuke/api/pipeline.py | 35 ++++++++++++++--------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8b1ba0ab0d..de5d13d347 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -120,7 +120,7 @@ def deprecated(new_destination): class Context: main_window = None - context_label = None + context_action_item = None project_name = os.getenv("AVALON_PROJECT") # Workfile related code workfiles_launched = False diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index ba4d66ab63..7bc17ff504 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -236,9 +236,13 @@ def _install_menu(): if not ASSIST: label = get_context_label() - Context.context_label = label - context_action = menu.addCommand(label) - context_action.setEnabled(False) + context_action_item = menu.addCommand("Context") + context_action_item.setEnabled(False) + + Context.context_action_item = context_action_item + + context_action = context_action_item.action() + context_action.setText(label) # add separator after context label menu.addSeparator() @@ -348,26 +352,21 @@ def _install_menu(): def change_context_label(): - menubar = nuke.menu("Nuke") - menu = menubar.findItem(MENU_LABEL) + if ASSIST: + return - label = get_context_label() + context_action_item = Context.context_action_item + if context_action_item is None: + return + context_action = context_action_item.action() - rm_item = [ - (i, item) for i, item in enumerate(menu.items()) - if Context.context_label in item.name() - ][0] + old_label = context_action.text() + new_label = get_context_label() - menu.removeItem(rm_item[1].name()) - - context_action = menu.addCommand( - label, - index=(rm_item[0]) - ) - context_action.setEnabled(False) + context_action.setText(new_label) log.info("Task label changed from `{}` to `{}`".format( - Context.context_label, label)) + old_label, new_label)) def add_shortcuts_from_presets(): From f4d6199b8db5eed96770996c7aca111319628033 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Nov 2023 12:33:10 +0100 Subject: [PATCH 284/298] fix 'create' from 'CreateContext' --- openpype/pipeline/create/context.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index f32477dfb7..683699a0d1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2020,8 +2020,14 @@ class CreateContext: project_name, self.host_name ) + asset_name = get_asset_name_identifier(asset_doc) + if AYON_SERVER_ENABLED: + asset_name_key = "folderPath" + else: + asset_name_key = "asset" + instance_data = { - "asset": asset_doc["name"], + asset_name_key: asset_name, "task": task_name, "family": creator.family, "variant": variant From 4e4277bd0383e0354965af819fd29e1dabe4479c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 18 Nov 2023 03:24:59 +0000 Subject: [PATCH 285/298] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index adb62abd9d..9d8724f926 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.6" +__version__ = "3.17.7-nightly.1" From 771e40cf5105cf6acc4130c6797ac582f5b3d291 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 18 Nov 2023 03:25:34 +0000 Subject: [PATCH 286/298] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6f1b01bd2f..f484016bfe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.7-nightly.1 - 3.17.6 - 3.17.6-nightly.3 - 3.17.6-nightly.2 @@ -134,7 +135,6 @@ body: - 3.15.2-nightly.4 - 3.15.2-nightly.3 - 3.15.2-nightly.2 - - 3.15.2-nightly.1 validations: required: true - type: dropdown From 8348fe954ed759d934dc1369cd036848382be9d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:02:35 +0100 Subject: [PATCH 287/298] AYON: Prepare for 'data' via graphql (#5923) * function 'get_folders_with_tasks' can expect 'data' in graphql result * fix docstring --- openpype/client/server/openpype_comp.py | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/client/server/openpype_comp.py b/openpype/client/server/openpype_comp.py index a123fe3167..71a141e913 100644 --- a/openpype/client/server/openpype_comp.py +++ b/openpype/client/server/openpype_comp.py @@ -1,4 +1,7 @@ import collections +import json + +import six from ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict from .constants import DEFAULT_FOLDER_FIELDS @@ -84,12 +87,12 @@ def get_folders_with_tasks( for folder. All possible folder fields are returned if 'None' is passed. - Returns: - List[Dict[str, Any]]: Queried folder entities. + Yields: + Dict[str, Any]: Queried folder entities. """ if not project_name: - return [] + return filters = { "projectName": project_name @@ -97,25 +100,25 @@ def get_folders_with_tasks( if folder_ids is not None: folder_ids = set(folder_ids) if not folder_ids: - return [] + return filters["folderIds"] = list(folder_ids) if folder_paths is not None: folder_paths = set(folder_paths) if not folder_paths: - return [] + return filters["folderPaths"] = list(folder_paths) if folder_names is not None: folder_names = set(folder_names) if not folder_names: - return [] + return filters["folderNames"] = list(folder_names) if parent_ids is not None: parent_ids = set(parent_ids) if not parent_ids: - return [] + return if None in parent_ids: # Replace 'None' with '"root"' which is used during GraphQl # query for parent ids filter for folders without folder @@ -147,10 +150,10 @@ def get_folders_with_tasks( parsed_data = query.query(con) folders = parsed_data["project"]["folders"] - if active is None: - return folders - return [ - folder - for folder in folders - if folder["active"] is active - ] + for folder in folders: + if active is not None and folder["active"] is not active: + continue + folder_data = folder.get("data") + if isinstance(folder_data, six.string_types): + folder["data"] = json.loads(folder_data) + yield folder From 07d00ee787fe287292075ca4914b810e890c3778 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Mon, 20 Nov 2023 21:30:09 +0800 Subject: [PATCH 288/298] Chore: Substance Painter Addons for Ayon (#5914) * Substance Painter Addons for Ayon * hound * make sure the class name is SubstancePainterAddon * use AYON as tab menu name when it is launched with AYON --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/substancepainter/api/pipeline.py | 3 +- openpype/settings/ayon_settings.py | 18 ++++++ .../applications/server/applications.json | 26 ++++++++ server_addon/applications/server/settings.py | 2 + server_addon/applications/server/version.py | 2 +- .../substancepainter/server/__init__.py | 17 ++++++ .../server/settings/__init__.py | 10 +++ .../server/settings/imageio.py | 61 +++++++++++++++++++ .../substancepainter/server/settings/main.py | 26 ++++++++ .../substancepainter/server/version.py | 1 + 10 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 server_addon/substancepainter/server/__init__.py create mode 100644 server_addon/substancepainter/server/settings/__init__.py create mode 100644 server_addon/substancepainter/server/settings/imageio.py create mode 100644 server_addon/substancepainter/server/settings/main.py create mode 100644 server_addon/substancepainter/server/version.py diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index e96064b2bf..a13075127f 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -170,7 +170,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): parent = substance_painter.ui.get_main_window() - menu = QtWidgets.QMenu("OpenPype") + tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" + menu = QtWidgets.QMenu(tab_menu_label) action = menu.addAction("Create...") action.triggered.connect( diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 771598f51f..5171517232 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -940,6 +940,23 @@ def _convert_photoshop_project_settings(ayon_settings, output): output["photoshop"] = ayon_photoshop +def _convert_substancepainter_project_settings(ayon_settings, output): + if "substancepainter" not in ayon_settings: + return + + ayon_substance_painter = ayon_settings["substancepainter"] + _convert_host_imageio(ayon_substance_painter) + if "shelves" in ayon_substance_painter: + shelves_items = ayon_substance_painter["shelves"] + new_shelves_items = { + item["name"]: item["value"] + for item in shelves_items + } + ayon_substance_painter["shelves"] = new_shelves_items + + output["substancepainter"] = ayon_substance_painter + + def _convert_tvpaint_project_settings(ayon_settings, output): if "tvpaint" not in ayon_settings: return @@ -1398,6 +1415,7 @@ def convert_project_settings(ayon_settings, default_settings): _convert_nuke_project_settings(ayon_settings, output) _convert_hiero_project_settings(ayon_settings, output) _convert_photoshop_project_settings(ayon_settings, output) + _convert_substancepainter_project_settings(ayon_settings, output) _convert_tvpaint_project_settings(ayon_settings, output) _convert_traypublisher_project_settings(ayon_settings, output) _convert_webpublisher_project_settings(ayon_settings, output) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index db7f86e357..f846b04215 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -1092,6 +1092,32 @@ } ] }, + "substancepainter": { + "enabled": true, + "label": "Substance Painter", + "icon": "{}/app_icons/substancepainter.png", + "host_name": "substancepainter", + "environment": "{}", + "variants": [ + { + "name": "8-2-0", + "label": "8.2", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Substance 3D Painter\\Adobe Substance 3D Painter.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, "unreal": { "enabled": true, "label": "Unreal Editor", diff --git a/server_addon/applications/server/settings.py b/server_addon/applications/server/settings.py index be9a2ea07e..981d56c30f 100644 --- a/server_addon/applications/server/settings.py +++ b/server_addon/applications/server/settings.py @@ -164,6 +164,8 @@ class ApplicationsSettings(BaseSettingsModel): default_factory=AppGroupWithPython, title="Adobe After Effects") celaction: AppGroup = Field( default_factory=AppGroupWithPython, title="Celaction 2D") + substancepainter: AppGroup = Field( + default_factory=AppGroupWithPython, title="Substance Painter") unreal: AppGroup = Field( default_factory=AppGroupWithPython, title="Unreal Editor") additional_apps: list[AdditionalAppGroup] = Field( diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/applications/server/version.py +++ b/server_addon/applications/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/server_addon/substancepainter/server/__init__.py b/server_addon/substancepainter/server/__init__.py new file mode 100644 index 0000000000..2bf808d508 --- /dev/null +++ b/server_addon/substancepainter/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import SubstancePainterSettings, DEFAULT_SPAINTER_SETTINGS + + +class SubstancePainterAddon(BaseServerAddon): + name = "substancepainter" + title = "Substance Painter" + version = __version__ + settings_model: Type[SubstancePainterSettings] = SubstancePainterSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_SPAINTER_SETTINGS) diff --git a/server_addon/substancepainter/server/settings/__init__.py b/server_addon/substancepainter/server/settings/__init__.py new file mode 100644 index 0000000000..f47f064536 --- /dev/null +++ b/server_addon/substancepainter/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + SubstancePainterSettings, + DEFAULT_SPAINTER_SETTINGS, +) + + +__all__ = ( + "SubstancePainterSettings", + "DEFAULT_SPAINTER_SETTINGS", +) diff --git a/server_addon/substancepainter/server/settings/imageio.py b/server_addon/substancepainter/server/settings/imageio.py new file mode 100644 index 0000000000..e301d3d865 --- /dev/null +++ b/server_addon/substancepainter/server/settings/imageio.py @@ -0,0 +1,61 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel +from ayon_server.settings.validators import ensure_unique_names + + +class ImageIOConfigModel(BaseSettingsModel): + override_global_config: bool = Field( + False, + title="Override global OCIO config" + ) + filepath: list[str] = Field( + default_factory=list, + title="Config path" + ) + + +class ImageIOFileRuleModel(BaseSettingsModel): + name: str = Field("", title="Rule name") + pattern: str = Field("", title="Regex pattern") + colorspace: str = Field("", title="Colorspace name") + ext: str = Field("", title="File extension") + + +class ImageIOFileRulesModel(BaseSettingsModel): + activate_host_rules: bool = Field(False) + rules: list[ImageIOFileRuleModel] = Field( + default_factory=list, + title="Rules" + ) + + @validator("rules") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class ImageIOSettings(BaseSettingsModel): + activate_host_color_management: bool = Field( + True, title="Enable Color Management" + ) + ocio_config: ImageIOConfigModel = Field( + default_factory=ImageIOConfigModel, + title="OCIO config" + ) + file_rules: ImageIOFileRulesModel = Field( + default_factory=ImageIOFileRulesModel, + title="File Rules" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "activate_host_color_management": True, + "ocio_config": { + "override_global_config": False, + "filepath": [] + }, + "file_rules": { + "activate_host_rules": False, + "rules": [] + } +} diff --git a/server_addon/substancepainter/server/settings/main.py b/server_addon/substancepainter/server/settings/main.py new file mode 100644 index 0000000000..f8397c3c08 --- /dev/null +++ b/server_addon/substancepainter/server/settings/main.py @@ -0,0 +1,26 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel +from .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS + + +class ShelvesSettingsModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: str = Field(title="Path") + + +class SubstancePainterSettings(BaseSettingsModel): + imageio: ImageIOSettings = Field( + default_factory=ImageIOSettings, + title="Color Management (ImageIO)" + ) + shelves: list[ShelvesSettingsModel] = Field( + default_factory=list, + title="Shelves" + ) + + +DEFAULT_SPAINTER_SETTINGS = { + "imageio": DEFAULT_IMAGEIO_SETTINGS, + "shelves": [] +} diff --git a/server_addon/substancepainter/server/version.py b/server_addon/substancepainter/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/substancepainter/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" From 898f728a065b1b3ea19bfba5f65a3f6251172565 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:28:11 +0100 Subject: [PATCH 289/298] simplify expression --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 1acbbb3d88..590d7b7050 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -216,8 +216,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "_ instance.data: {}".format(pformat(instance.data))) def _get_asset_data(self, data): - folder_path = ( - data.pop("folderPath") if data.get("folderPath") else None) + folder_path = data.pop("folderPath", None) if data.get("asset_name"): asset_name = data["asset_name"] From 477bc613e210af70c4ed4a1d06fc4654d954de18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 20 Nov 2023 18:28:18 +0100 Subject: [PATCH 290/298] Hiero | Global: editorial with folder path (#5829) * hiero: adding folderPath to creator - some minor typos fixes - modules sorting * Hiero: adding asset_name and processing folderPath - refactor labels * fixing extract hierarchy to ayon * hound * Update openpype/hosts/hiero/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * simplify expression --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/hiero/api/plugin.py | 24 +++++--- .../plugins/publish/precollect_instances.py | 59 +++++++++++++------ .../plugins/publish/precollect_workfile.py | 15 +++-- .../publish/extract_hierarchy_to_ayon.py | 17 +++--- 4 files changed, 76 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 0e0632e032..b0c73e41fb 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -31,7 +31,7 @@ def load_stylesheet(): class CreatorWidget(QtWidgets.QDialog): # output items - items = dict() + items = {} def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) @@ -642,8 +642,8 @@ class PublishClip: Returns: hiero.core.TrackItem: hiero track item object with pype tag """ - vertical_clip_match = dict() - tag_data = dict() + vertical_clip_match = {} + tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -705,9 +705,10 @@ class PublishClip: self._create_parents() def convert(self): - # solve track item data and add them to tag data - self._convert_to_tag_data() + tag_hierarchy_data = self._convert_to_tag_data() + + self.tag_data.update(tag_hierarchy_data) # if track name is in review track name and also if driving track name # is not in review track name: skip tag creation @@ -721,16 +722,23 @@ class PublishClip: if self.rename: # rename track item self.track_item.setName(new_name) - self.tag_data["asset"] = new_name + self.tag_data["asset_name"] = new_name else: - self.tag_data["asset"] = self.ti_name + self.tag_data["asset_name"] = self.ti_name self.tag_data["hierarchyData"]["shot"] = self.ti_name + # AYON unique identifier + folder_path = "/{}/{}".format( + tag_hierarchy_data["hierarchy"], + self.tag_data["asset_name"] + ) + self.tag_data["folderPath"] = folder_path if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: self.tag_data.update({"reviewTrack": None}) + # TODO: remove debug print log.debug("___ self.tag_data: {}".format( pformat(self.tag_data) )) @@ -889,7 +897,7 @@ class PublishClip: tag_hierarchy_data = hero_data # add data to return data dict - self.tag_data.update(tag_hierarchy_data) + return tag_hierarchy_data def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 65b8fed49c..590d7b7050 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -1,9 +1,12 @@ import pyblish + +from openpype import AYON_SERVER_ENABLED from openpype.pipeline.editorial import is_overlapping_otio_ranges + from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export -import hiero +import hiero # # developer reload modules from pprint import pformat @@ -80,25 +83,24 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["asset"] + asset, asset_name = self._get_asset_data(tag_data) + subset = tag_data["subset"] # insert family into families - family = tag_data["family"] families = [str(f) for f in tag_data["families"]] - families.insert(0, str(family)) # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({})".format(clip_name) label += " {}".format(subset) - label += " {}".format("[" + ", ".join(families) + "]") data.update({ "name": "{}_{}".format(asset, subset), "label": label, "asset": asset, + "asset_name": asset_name, "item": track_item, "families": families, "publish": tag_data["publish"], @@ -176,6 +178,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) def create_shot_instance(self, context, **data): + subset = "shotMain" master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") item = data.get("item") @@ -188,23 +191,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "shotMain" + asset_name = data["asset_name"] # insert family into families family = "shot" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": [] }) @@ -214,7 +215,33 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) + def _get_asset_data(self, data): + folder_path = data.pop("folderPath", None) + + if data.get("asset_name"): + asset_name = data["asset_name"] + else: + asset_name = data["asset"] + + # backward compatibility for clip tags + # which are missing folderPath key + # TODO remove this in future versions + if not folder_path: + hierarchy_path = data["hierarchy"] + folder_path = "/{}/{}".format( + hierarchy_path, + asset_name + ) + + if AYON_SERVER_ENABLED: + asset = folder_path + else: + asset = asset_name + + return asset, asset_name + def create_audio_instance(self, context, **data): + subset = "audioMain" master_layer = data.get("heroTrack") if not master_layer: @@ -229,23 +256,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "audioMain" + asset_name = data["asset_name"] # insert family into families family = "audio" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": ["clip"] }) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 1d6bdc0257..8abb0885c6 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -18,7 +18,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.491 def process(self, context): - asset_name = context.data["asset"] + asset = context.data["asset"] + asset_name = asset if AYON_SERVER_ENABLED: asset_name = asset_name.split("/")[-1] @@ -29,7 +30,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): # adding otio timeline to context otio_timeline = hiero_export.create_otio_timeline() - # get workfile thumnail paths + # get workfile thumbnail paths tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") thumbnail_name = "workfile_thumbnail.png" thumbnail_path = os.path.join(tmp_staging, thumbnail_name) @@ -51,8 +52,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } # get workfile paths - curent_file = project.path() - staging_dir, base_name = os.path.split(curent_file) + current_file = project.path() + staging_dir, base_name = os.path.split(current_file) # creating workfile representation workfile_representation = { @@ -63,10 +64,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } family = "workfile" instance_data = { + "label": "{} - {}Main".format( + asset, family), "name": "{}_{}".format(asset_name, family), "asset": context.data["asset"], # TODO use 'get_subset_name' - "subset": "{}{}".format(asset_name, family.capitalize()), + "subset": "{}{}Main".format(asset_name, family.capitalize()), "item": project, "family": family, "families": [], @@ -81,7 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "activeProject": project, "activeTimeline": active_timeline, "otioTimeline": otio_timeline, - "currentFile": curent_file, + "currentFile": current_file, "colorspace": self.get_colorspace(project), "fps": fps } diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index fe8cb40ad2..ef69369d67 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -191,15 +191,15 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): """ # filter only the active publishing instances - active_folder_names = set() + active_folder_paths = set() for instance in context: if instance.data.get("publish") is not False: - active_folder_names.add(instance.data.get("asset")) + active_folder_paths.add(instance.data.get("asset")) - active_folder_names.discard(None) + active_folder_paths.discard(None) - self.log.debug("Active folder names: {}".format(active_folder_names)) - if not active_folder_names: + self.log.debug("Active folder paths: {}".format(active_folder_paths)) + if not active_folder_paths: return None project_item = None @@ -230,12 +230,13 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): if not children_context: continue - for asset_name, asset_info in children_context.items(): + for asset, asset_info in children_context.items(): if ( - asset_name not in active_folder_names + asset not in active_folder_paths and not asset_info.get("childs") ): continue + asset_name = asset.split("/")[-1] item_id = uuid.uuid4().hex new_item = copy.deepcopy(asset_info) new_item["name"] = asset_name @@ -252,7 +253,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): items_by_id[item_id] = new_item parent_id_by_item_id[item_id] = parent_id - if asset_name in active_folder_names: + if asset in active_folder_paths: valid_ids.add(item_id) hierarchy_queue.append((item_id, new_children_context)) From ba697c85cc2c9e22bb5be6ac772a257fe174a4e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Nov 2023 18:45:24 +0100 Subject: [PATCH 291/298] added function to get AYON original settings values --- openpype/settings/ayon_settings.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 5171517232..6676e71a8e 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1591,3 +1591,18 @@ def get_ayon_system_settings(default_values): return convert_system_settings( ayon_settings, default_values, addon_versions ) + + +def get_ayon_settings(project_name=None): + """AYON studio settings. + + Raw AYON settings values. + + Args: + project_name (Optional[str]): Project name. + + Returns: + dict[str, Any]: AYON settings. + """ + + return _AyonSettingsCache.get_value_by_project(project_name) From cdf49c4d40de868732802f6157553eac1b6b0c28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Nov 2023 18:48:28 +0100 Subject: [PATCH 292/298] Added 'AYONAddon' class which is initialized with AYON settings --- openpype/modules/__init__.py | 2 + openpype/modules/base.py | 193 +++++++++++++++++++++++------------ 2 files changed, 131 insertions(+), 64 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 1f345feea9..3097805353 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -10,6 +10,7 @@ from .interfaces import ( ) from .base import ( + AYONAddon, OpenPypeModule, OpenPypeAddOn, @@ -35,6 +36,7 @@ __all__ = ( "ISettingsChangeListener", "IHostAddon", + "AYONAddon", "OpenPypeModule", "OpenPypeAddOn", diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 4636906cec..9d5a2608a5 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Base class for Pype Modules.""" +"""Base class for AYON addons.""" import copy import os import sys @@ -11,6 +11,7 @@ import platform import threading import collections import traceback + from uuid import uuid4 from abc import ABCMeta, abstractmethod @@ -29,9 +30,12 @@ from openpype.settings import ( from openpype.settings.lib import ( get_studio_system_settings_overrides, - load_json_file + load_json_file, +) +from openpype.settings.ayon_settings import ( + is_dev_mode_enabled, + get_ayon_settings, ) -from openpype.settings.ayon_settings import is_dev_mode_enabled from openpype.lib import ( Logger, @@ -47,11 +51,11 @@ from .interfaces import ( ITrayService ) -# Files that will be always ignored on modules import +# Files that will be always ignored on addons import IGNORED_FILENAMES = ( "__pycache__", ) -# Files ignored on modules import from "./openpype/modules" +# Files ignored on addons import from "./openpype/modules" IGNORED_DEFAULT_FILENAMES = ( "__init__.py", "base.py", @@ -59,8 +63,8 @@ IGNORED_DEFAULT_FILENAMES = ( "example_addons", "default_modules", ) -# Modules that won't be loaded in AYON mode from "./openpype/modules" -# - the same modules are ignored in "./server_addon/create_ayon_addons.py" +# Addons that won't be loaded in AYON mode from "./openpype/modules" +# - the same addons are ignored in "./server_addon/create_ayon_addons.py" IGNORED_FILENAMES_IN_AYON = { "ftrack", "shotgrid", @@ -466,7 +470,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): attr = getattr(mod, attr_name) if ( inspect.isclass(attr) - and issubclass(attr, OpenPypeModule) + and issubclass(attr, AYONAddon) ): imported_modules.append(mod) break @@ -633,26 +637,22 @@ def _load_modules(): @six.add_metaclass(ABCMeta) -class OpenPypeModule: - """Base class of pype module. +class AYONAddon(object): + """Base class of AYON addon. Attributes: - id (UUID): Module's id. - enabled (bool): Is module enabled. - name (str): Module name. - manager (ModulesManager): Manager that created the module. + id (UUID): Addon object id. + enabled (bool): Is addon enabled. + name (str): Addon name. + + Args: + manager (ModulesManager): Manager object who discovered addon. + settings (dict[str, Any]): AYON settings. """ - # Disable by default - enabled = False + enabled = True _id = None - @property - @abstractmethod - def name(self): - """Module's name.""" - pass - def __init__(self, manager, settings): self.manager = manager @@ -662,22 +662,45 @@ class OpenPypeModule: @property def id(self): + """Random id of addon object. + + Returns: + str: Object id. + """ + if self._id is None: self._id = uuid4() return self._id + @property @abstractmethod - def initialize(self, module_settings): - """Initialization of module attributes. + def name(self): + """Addon name. - It is not recommended to override __init__ that's why specific method - was implemented. + Returns: + str: Addon name. """ pass - def connect_with_modules(self, enabled_modules): - """Connect with other enabled modules.""" + def initialize(self, settings): + """Initialization of module attributes. + + It is not recommended to override __init__ that's why specific method + was implemented. + + Args: + settings (dict[str, Any]): Settings. + """ + + pass + + def connect_with_modules(self, enabled_addons): + """Connect with other enabled addons. + + Args: + enabled_addons (list[AYONAddon]): Addons that are enabled. + """ pass @@ -685,6 +708,9 @@ class OpenPypeModule: """Get global environments values of module. Environment variables that can be get only from system settings. + + Returns: + dict[str, str]: Environment variables. """ return {} @@ -697,7 +723,7 @@ class OpenPypeModule: Args: application (Application): Application that is launched. - env (dict): Current environment variables. + env (dict[str, str]): Current environment variables. """ pass @@ -713,7 +739,8 @@ class OpenPypeModule: to receive from 'host' object. Args: - host (ModuleType): Access to installed/registered host object. + host (Union[ModuleType, HostBase]): Access to installed/registered + host object. host_name (str): Name of host. project_name (str): Project name which is main part of host context. @@ -727,47 +754,64 @@ class OpenPypeModule: The best practise is to create click group for whole module which is used to separate commands. - class MyPlugin(OpenPypeModule): - ... - def cli(self, module_click_group): - module_click_group.add_command(cli_main) + Example: + class MyPlugin(AYONAddon): + ... + def cli(self, module_click_group): + module_click_group.add_command(cli_main) - @click.group(, help="") - def cli_main(): - pass + @click.group(, help="") + def cli_main(): + pass - @cli_main.command() - def mycommand(): - print("my_command") + @cli_main.command() + def mycommand(): + print("my_command") + + Args: + module_click_group (click.Group): Group to which can be added + commands. """ pass +class OpenPypeModule(AYONAddon): + """Base class of OpenPype module. + + Instead of 'AYONAddon' are passed in module settings. + + Args: + manager (ModulesManager): Manager object who discovered addon. + settings (dict[str, Any]): OpenPype settings. + """ + + # Disable by default + enabled = False + + class OpenPypeAddOn(OpenPypeModule): # Enable Addon by default enabled = True - def initialize(self, module_settings): - """Initialization is not be required for most of addons.""" - pass - class ModulesManager: """Manager of Pype modules helps to load and prepare them to work. Args: - modules_settings(dict): To be able create module manager with specified - data. For settings changes callbacks and testing purposes. + system_settings (Optional[dict[str, Any]]): OpenPype system settings. + ayon_settings (Optional[dict[str, Any]]): AYON studio settings. """ + # Helper attributes for report _report_total_key = "Total" - def __init__(self, _system_settings=None): + def __init__(self, system_settings=None, ayon_settings=None): self.log = logging.getLogger(self.__class__.__name__) - self._system_settings = _system_settings + self._system_settings = system_settings + self._ayon_settings = ayon_settings self.modules = [] self.modules_by_id = {} @@ -789,8 +833,9 @@ class ModulesManager: default (Any): Default output if module is not available. Returns: - Union[OpenPypeModule, None]: Module found by name or None. + Union[AYONAddon, None]: Module found by name or None. """ + return self.modules_by_name.get(module_name, default) def get_enabled_module(self, module_name, default=None): @@ -804,7 +849,7 @@ class ModulesManager: not enabled. Returns: - Union[OpenPypeModule, None]: Enabled module found by name or None. + Union[AYONAddon, None]: Enabled module found by name or None. """ module = self.get(module_name) @@ -821,9 +866,14 @@ class ModulesManager: self.log.debug("*** Pype modules initialization.") # Prepare settings for modules - system_settings = getattr(self, "_system_settings", None) + system_settings = self._system_settings if system_settings is None: system_settings = get_system_settings() + + ayon_settings = self._ayon_settings + if AYON_SERVER_ENABLED and ayon_settings is None: + ayon_settings = get_ayon_settings() + modules_settings = system_settings["modules"] report = {} @@ -836,12 +886,13 @@ class ModulesManager: for name in dir(module): modules_item = getattr(module, name, None) # Filter globals that are not classes which inherit from - # OpenPypeModule + # AYONAddon if ( not inspect.isclass(modules_item) + or modules_item is AYONAddon or modules_item is OpenPypeModule or modules_item is OpenPypeAddOn - or not issubclass(modules_item, OpenPypeModule) + or not issubclass(modules_item, AYONAddon) ): continue @@ -866,10 +917,14 @@ class ModulesManager: module_classes.append(modules_item) for modules_item in module_classes: + is_openpype_module = issubclass(modules_item, OpenPypeModule) + settings = ( + system_settings if is_openpype_module else ayon_settings + ) + name = modules_item.__name__ try: - name = modules_item.__name__ # Try initialize module - module = modules_item(self, modules_settings) + module = modules_item(self, settings) # Store initialized object self.modules.append(module) self.modules_by_id[module.id] = module @@ -924,8 +979,9 @@ class ModulesManager: """Enabled modules initialized by the manager. Returns: - list: Initialized and enabled modules. + list[AYONAddon]: Initialized and enabled modules. """ + return [ module for module in self.modules @@ -1108,7 +1164,7 @@ class ModulesManager: host_name (str): Host name for which is found host module. Returns: - OpenPypeModule: Found host module by name. + AYONAddon: Found host module by name. None: There was not found module inheriting IHostAddon which has host name set to passed 'host_name'. """ @@ -1129,12 +1185,11 @@ class ModulesManager: inheriting 'IHostAddon'. """ - host_names = { + return { module.host_name for module in self.get_enabled_modules() if isinstance(module, IHostAddon) } - return host_names def print_report(self): """Print out report of time spent on modules initialization parts. @@ -1290,6 +1345,10 @@ class TrayModulesManager(ModulesManager): callback can be defined with `doubleclick_callback` attribute. Missing feature how to define default callback. + + Args: + addon (AYONAddon): Addon object. + callback (FunctionType): Function callback. """ callback_name = "_".join([module.name, callback.__name__]) if callback_name not in self.doubleclick_callbacks: @@ -1310,11 +1369,17 @@ class TrayModulesManager(ModulesManager): self.tray_menu(tray_menu) def get_enabled_tray_modules(self): - output = [] - for module in self.modules: - if module.enabled and isinstance(module, ITrayModule): - output.append(module) - return output + """Enabled tray modules. + + Returns: + list[AYONAddon]: Enabled addons that inherit from tray interface. + """ + + return [ + module + for module in self.modules + if module.enabled and isinstance(module, ITrayModule) + ] def restart_tray(self): if self.tray_manager: From a31214d2c4f68ae5820139aa9857e711a8383577 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Nov 2023 11:05:55 +0100 Subject: [PATCH 293/298] use 'newAssetPublishing' instead of 'isEditorial' --- .../hosts/resolve/plugins/publish/precollect_instances.py | 1 - .../hosts/traypublisher/plugins/create/create_editorial.py | 1 - .../plugins/publish/collect_sequence_frame_data.py | 6 +++--- .../traypublisher/plugins/publish/validate_frame_ranges.py | 6 +++--- openpype/plugins/publish/collect_resources_path.py | 6 +++--- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 58c1c85276..bca6734848 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -79,7 +79,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "handleEnd": handle_end, "newAssetPublishing": True, "families": ["clip"], - "isEditorial": True }) # otio clip data diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 198e05d593..26cce35d55 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -714,7 +714,6 @@ or updating already created. Publishing will create OTIO file. "newAssetPublishing": True, "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, - "isEditorial": True, # creator_attributes "creator_attributes": creator_attributes diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index 92cedf6b5b..5e60a94927 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -28,9 +28,9 @@ class CollectSequenceFrameData( return # editorial would fail since they might not be in database yet - is_editorial = instance.data.get("isEditorial") - if is_editorial: - self.log.debug("Instance is Editorial. Skipping.") + new_asset_publishing = instance.data.get("newAssetPublishing") + if new_asset_publishing: + self.log.debug("Instance is creating new asset. Skipping.") return frame_data = self.get_frame_data_from_repre_sequence(instance) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index 4977a13374..95894848a4 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -31,9 +31,9 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, return # editorial would fail since they might not be in database yet - is_editorial = instance.data.get("isEditorial") - if is_editorial: - self.log.debug("Instance is Editorial. Skipping.") + new_asset_publishing = instance.data.get("newAssetPublishing") + if new_asset_publishing: + self.log.debug("Instance is creating new asset. Skipping.") return if (self.skip_timelines_check and diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 14c13310df..a7f12bdfdb 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -69,9 +69,9 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): def process(self, instance): # editorial would fail since they might not be in database yet - is_editorial = instance.data.get("isEditorial") - if is_editorial: - self.log.debug("Instance is Editorial. Skipping.") + new_asset_publishing = instance.data.get("newAssetPublishing") + if new_asset_publishing: + self.log.debug("Instance is creating new asset. Skipping.") return anatomy = instance.context.data["anatomy"] From f78f9c98f347031a7a6c52059fef94a9ada5848a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Nov 2023 11:07:10 +0100 Subject: [PATCH 294/298] added harmony and flame to skipped addons --- openpype/modules/base.py | 18 ++++++++++++++--- server_addon/create_ayon_addons.py | 32 +++++++++++++++++++----------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 4636906cec..eba7d54db0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -68,6 +68,10 @@ IGNORED_FILENAMES_IN_AYON = { "slack", "kitsu", } +IGNORED_HOSTS_IN_AYON = { + "flame", + "harmony", +} # Inherit from `object` for Python 2 hosts @@ -536,6 +540,11 @@ def _load_modules(): addons_dir = os.path.join(os.path.dirname(current_dir), "addons") module_dirs.append(addons_dir) + ignored_host_names = set(IGNORED_HOSTS_IN_AYON) + ignored_current_dir_filenames = set(IGNORED_DEFAULT_FILENAMES) + if AYON_SERVER_ENABLED: + ignored_current_dir_filenames |= IGNORED_FILENAMES_IN_AYON + processed_paths = set() for dirpath in frozenset(module_dirs): # Skip already processed paths @@ -551,9 +560,6 @@ def _load_modules(): is_in_current_dir = dirpath == current_dir is_in_host_dir = dirpath == hosts_dir - ignored_current_dir_filenames = set(IGNORED_DEFAULT_FILENAMES) - if AYON_SERVER_ENABLED: - ignored_current_dir_filenames |= IGNORED_FILENAMES_IN_AYON for filename in os.listdir(dirpath): # Ignore filenames @@ -566,6 +572,12 @@ def _load_modules(): ): continue + if ( + is_in_host_dir + and filename in ignored_host_names + ): + continue + fullpath = os.path.join(dirpath, filename) basename, ext = os.path.splitext(filename) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index fe8d278321..fc7a673dcc 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -33,6 +33,20 @@ IGNORE_FILE_PATTERNS: List[Pattern] = [ } ] +IGNORED_HOSTS = [ + "flame", + "harmony", +] + +IGNORED_MODULES = [ + "ftrack", + "shotgrid", + "sync_server", + "example_addons", + "slack", + "kitsu", +] + class ZipFileLongPaths(zipfile.ZipFile): """Allows longer paths in zip files. @@ -202,16 +216,6 @@ def create_openpype_package( str(pyproject_path), (private_dir / pyproject_path.name) ) - - ignored_hosts = [] - ignored_modules = [ - "ftrack", - "shotgrid", - "sync_server", - "example_addons", - "slack", - "kitsu", - ] # Subdirs that won't be added to output zip file ignored_subpaths = [ ["addons"], @@ -219,11 +223,11 @@ def create_openpype_package( ] ignored_subpaths.extend( ["hosts", host_name] - for host_name in ignored_hosts + for host_name in IGNORED_HOSTS ) ignored_subpaths.extend( ["modules", module_name] - for module_name in ignored_modules + for module_name in IGNORED_MODULES ) # Zip client @@ -297,6 +301,7 @@ def main( # Make sure output dir is created output_dir.mkdir(parents=True, exist_ok=True) + ignored_addons = set(IGNORED_HOSTS) | set(IGNORED_MODULES) for addon_dir in current_dir.iterdir(): if not addon_dir.is_dir(): continue @@ -304,6 +309,9 @@ def main( if addons and addon_dir.name not in addons: continue + if addon_dir.name in ignored_addons: + continue + server_dir = addon_dir / "server" if not server_dir.exists(): continue From 3f850f1b0ae5b91db9e2f2dee7f5f8183af6906d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Nov 2023 12:02:01 +0100 Subject: [PATCH 295/298] fix module settings usage --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9d5a2608a5..40a9db45d5 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -919,7 +919,7 @@ class ModulesManager: for modules_item in module_classes: is_openpype_module = issubclass(modules_item, OpenPypeModule) settings = ( - system_settings if is_openpype_module else ayon_settings + modules_settings if is_openpype_module else ayon_settings ) name = modules_item.__name__ try: From 8dddc31706bd2560366d3fe26cfe31dd998a93a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Nov 2023 12:06:38 +0100 Subject: [PATCH 296/298] change debug log message --- openpype/modules/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 40a9db45d5..8ed6b4b9c1 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -806,6 +806,8 @@ class ModulesManager: # Helper attributes for report _report_total_key = "Total" + _system_settings = None + _ayon_settings = None def __init__(self, system_settings=None, ayon_settings=None): self.log = logging.getLogger(self.__class__.__name__) @@ -864,7 +866,11 @@ class ModulesManager: import openpype_modules - self.log.debug("*** Pype modules initialization.") + self.log.debug("*** {} initialization.".format( + "AYON addons" + if AYON_SERVER_ENABLED + else "OpenPype modules" + )) # Prepare settings for modules system_settings = self._system_settings if system_settings is None: From a4225a2a35734df3f246361f3171b8675be0d5f9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 22 Nov 2023 03:25:56 +0000 Subject: [PATCH 297/298] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 9d8724f926..89067af269 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.7-nightly.1" +__version__ = "3.17.7-nightly.2" From d47128cb1f510845b9706c5ba59fcc56a1f0edd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Nov 2023 03:26:34 +0000 Subject: [PATCH 298/298] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f484016bfe..e2afcdaac7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.7-nightly.2 - 3.17.7-nightly.1 - 3.17.6 - 3.17.6-nightly.3 @@ -134,7 +135,6 @@ body: - 3.15.2-nightly.5 - 3.15.2-nightly.4 - 3.15.2-nightly.3 - - 3.15.2-nightly.2 validations: required: true - type: dropdown