diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 249da3da0e..7a1fe9d83e 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.5-nightly.3 - 3.17.5-nightly.2 - 3.17.5-nightly.1 - 3.17.4 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.5 - 3.15.1-nightly.4 - 3.15.1-nightly.3 - - 3.15.1-nightly.2 validations: required: true - type: dropdown diff --git a/openpype/cli.py b/openpype/cli.py index 7422f32f13..f0fe550a1f 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -282,6 +282,9 @@ def run(script): "--app_variant", help="Provide specific app variant for test, empty for latest", default=None) +@click.option("--app_group", + help="Provide specific app group for test, empty for default", + default=None) @click.option("-t", "--timeout", help="Provide specific timeout value for test case", @@ -294,11 +297,11 @@ def run(script): help="MongoDB for testing.", default=None) def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant, - timeout, setup_only, mongo_url): + timeout, setup_only, mongo_url, app_group): """Run all automatic tests after proper initialization via start.py""" PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url) + mongo_url, app_group) @main.command(help="DEPRECATED - run sync server") diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 8fc7a70dd8..e059f7c272 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", application_launch) @@ -186,11 +181,6 @@ def application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active AfterEffects document. diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 25d6568889..3d6b634916 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader): empties = [obj for obj in objects if obj.type == 'EMPTY'] for empty in empties: - if empty.get(AVALON_PROPERTY): + if empty.get(AVALON_PROPERTY) and empty.parent is None: return empty return None @@ -90,6 +90,7 @@ 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 @@ -100,8 +101,11 @@ class BlendLoader(plugin.AssetLoader): # Link all the container children to the collection for obj in container.children_recursive: + 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) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 1f9fef7417..14662dc419 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -173,6 +173,7 @@ def install(): os.remove(filepath) icon = get_openpype_icon_filepath() + tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins @@ -197,14 +198,14 @@ def install(): if not network_categories: continue - key = "openpype_create.{}".format(identifier) + key = "ayon_create.{}".format(identifier) log.debug(f"Registering {key}") script = CREATE_SCRIPT.format(identifier=identifier) data = { "script": script, "language": hou.scriptLanguage.Python, "icon": icon, - "help": "Create OpenPype publish instance for {}".format( + "help": "Create Ayon publish instance for {}".format( creator.label ), "help_url": None, @@ -213,7 +214,7 @@ def install(): "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, - "locations": ["OpenPype"] + "locations": [tab_menu_label] } label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac375c56d6..8b058e605d 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -569,9 +569,9 @@ def get_template_from_value(key, value): return parm -def get_frame_data(node, handle_start=0, handle_end=0, log=None): - """Get the frame data: start frame, end frame, steps, - start frame with start handle and end frame with end handle. +def get_frame_data(node, log=None): + """Get the frame data: `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. This function uses Houdini node's `trange`, `t1, `t2` and `t3` parameters as the source of truth for the full inclusive frame @@ -579,20 +579,17 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): range including the handles. The non-inclusive frame start and frame end without handles - are computed by subtracting the handles from the inclusive + can be computed by subtracting the handles from the inclusive frame range. Args: node (hou.Node): ROP node to retrieve frame range from, the frame range is assumed to be the frame range *including* the start and end handles. - handle_start (int): Start handles. - handle_end (int): End handles. - log (logging.Logger): Logger to log to. Returns: - dict: frame data for start, end, steps, - start with handle and end with handle + dict: frame data for `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. """ @@ -623,11 +620,6 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") - data["handleStart"] = handle_start - data["handleEnd"] = handle_end - data["frameStart"] = data["frameStartHandle"] + data["handleStart"] - data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] - return data @@ -1018,7 +1010,7 @@ def self_publish(): def add_self_publish_button(node): """Adds a self publish button to the rop node.""" - label = os.environ.get("AVALON_LABEL") or "OpenPype" + label = os.environ.get("AVALON_LABEL") or "AYON" button_parm = hou.ButtonParmTemplate( "ayon_self_publish", diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f8db45c56b..11135e20b2 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -3,7 +3,6 @@ import os import sys import logging -import contextlib import hou # noqa @@ -66,10 +65,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_event_callback("open", on_open) register_event_callback("new", on_new) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) - self._has_been_setup = True # add houdini vendor packages hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor") @@ -406,54 +401,3 @@ def _set_context_settings(): lib.reset_framerange() lib.update_houdini_vars_context() - - -def on_pyblish_instance_toggled(instance, new_value, old_value): - """Toggle saver tool passthrough states on instance toggles.""" - @contextlib.contextmanager - def main_take(no_update=True): - """Enter root take during context""" - original_take = hou.takes.currentTake() - original_update_mode = hou.updateModeSetting() - root = hou.takes.rootTake() - has_changed = False - try: - if original_take != root: - has_changed = True - if no_update: - hou.setUpdateMode(hou.updateMode.Manual) - hou.takes.setCurrentTake(root) - yield - finally: - if has_changed: - if no_update: - hou.setUpdateMode(original_update_mode) - hou.takes.setCurrentTake(original_take) - - if not instance.data.get("_allowToggleBypass", True): - return - - nodes = instance[:] - if not nodes: - return - - # Assume instance node is first node - instance_node = nodes[0] - - if not hasattr(instance_node, "isBypassed"): - # Likely not a node that can actually be bypassed - log.debug("Can't bypass node: %s", instance_node.path()) - return - - if instance_node.isBypassed() != (not old_value): - print("%s old bypass state didn't match old instance state, " - "updating anyway.." % instance_node.path()) - - try: - # Go into the main take, because when in another take changing - # the bypass state of a note cannot be done due to it being locked - # by default. - with main_take(no_update=True): - instance_node.bypass(not new_value) - except hou.PermissionError as exc: - log.warning("%s - %s", instance_node.path(), exc) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 5df45a1f72..5093a90988 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -24,29 +24,33 @@ def generate_shelves(): # load configuration of houdini shelves project_name = get_current_project_name() project_settings = get_project_settings(project_name) - shelves_set_config = project_settings["houdini"]["shelves"] + shelves_configs = project_settings["houdini"]["shelves"] - if not shelves_set_config: + if not shelves_configs: log.debug("No custom shelves found in project settings.") return # Get Template data template_data = get_current_context_template_data_with_asset_data() - for shelf_set_config in shelves_set_config: - shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') - shelf_set_os_filepath = shelf_set_filepath[current_os] - if shelf_set_os_filepath: - shelf_set_os_filepath = get_path_using_template_data( - shelf_set_os_filepath, template_data - ) - if not os.path.isfile(shelf_set_os_filepath): - log.error("Shelf path doesn't exist - " - "{}".format(shelf_set_os_filepath)) - continue + for config in shelves_configs: + selected_option = config["options"] + shelf_set_config = config[selected_option] - hou.shelves.newShelfSet(file_path=shelf_set_os_filepath) - continue + shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') + if shelf_set_filepath: + shelf_set_os_filepath = shelf_set_filepath[current_os] + if shelf_set_os_filepath: + shelf_set_os_filepath = get_path_using_template_data( + shelf_set_os_filepath, template_data + ) + if not os.path.isfile(shelf_set_os_filepath): + log.error("Shelf path doesn't exist - " + "{}".format(shelf_set_os_filepath)) + continue + + hou.shelves.loadFile(shelf_set_os_filepath) + continue shelf_set_name = shelf_set_config.get('shelf_set_name') if not shelf_set_name: diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index a3f31e7e94..0f629cf9c9 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -3,6 +3,7 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError from openpype.lib import EnumDef +import hou class CreateBGEO(plugin.HoudiniCreator): @@ -13,7 +14,6 @@ class CreateBGEO(plugin.HoudiniCreator): icon = "gears" def create(self, subset_name, instance_data, pre_create_data): - import hou instance_data.pop("active", None) @@ -90,3 +90,9 @@ class CreateBGEO(plugin.HoudiniCreator): return attrs + [ EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), ] + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py index 9d4f7969bb..52ea6fa054 100644 --- a/openpype/hosts/houdini/plugins/create/create_composite.py +++ b/openpype/hosts/houdini/plugins/create/create_composite.py @@ -45,6 +45,11 @@ class CreateCompositeSequence(plugin.HoudiniCreator): instance_node.setParms(parms) + # Manually set f1 & f2 to $FSTART and $FEND respectively + # to match other Houdini nodes default. + instance_node.parm("f1").setExpression("$FSTART") + instance_node.parm("f2").setExpression("$FEND") + # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index c4093bfbc6..ac075d2072 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -5,6 +5,7 @@ from openpype.client import ( get_subsets, ) from openpype.hosts.houdini.api import plugin +import hou class CreateHDA(plugin.HoudiniCreator): @@ -35,7 +36,6 @@ class CreateHDA(plugin.HoudiniCreator): def create_instance_node( self, node_name, parent, node_type="geometry"): - import hou parent_node = hou.node("/obj") if self.selected_nodes: @@ -81,3 +81,8 @@ class CreateHDA(plugin.HoudiniCreator): pre_create_data) # type: plugin.CreatedInstance return instance + + def get_network_categories(self): + return [ + hou.objNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py index b814dd9d57..3a4ab7008b 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating Redshift proxies.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +import hou class CreateRedshiftProxy(plugin.HoudiniCreator): @@ -12,7 +12,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): icon = "magic" def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa + # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) @@ -28,7 +28,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): instance = super(CreateRedshiftProxy, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) instance_node = hou.node(instance.get("instance_node")) @@ -44,3 +44,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): # Lock some Avalon attributes to_lock = ["family", "id", "prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py index ea0b36f03f..d0985198bd 100644 --- a/openpype/hosts/houdini/plugins/create/create_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py @@ -54,6 +54,7 @@ class CreateStaticMesh(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 9c96e48e3a..69418f9575 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -40,6 +40,7 @@ class CreateVDBCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index 663a93e48b..cff2b74e52 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -119,7 +119,8 @@ class ImageLoader(load.LoaderPlugin): if not parent.children(): parent.destroy() - def _get_file_sequence(self, root): + def _get_file_sequence(self, file_path): + root = os.path.dirname(file_path) files = sorted(os.listdir(root)) first_fname = files[0] diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index b489f83b29..d95f763826 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -21,8 +21,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): label = "Arnold ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py new file mode 100644 index 0000000000..67a281639d --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Collector plugin for frames data on ROP instances.""" +import hou # noqa +import pyblish.api +from openpype.lib import BoolDef +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectAssetHandles(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Apply asset handles. + + If instance does not have: + - frameStart + - frameEnd + - handleStart + - handleEnd + But it does have: + - frameStartHandle + - frameEndHandle + + Then we will retrieve the asset's handles to compute + the exclusive frame range and actual handle ranges. + """ + + hosts = ["houdini"] + + # This specific order value is used so that + # this plugin runs after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.499 + + label = "Collect Asset Handles" + use_asset_handles = True + + def process(self, instance): + # Only process instances without already existing handles data + # but that do have frameStartHandle and frameEndHandle defined + # like the data collected from CollectRopFrameRange + if "frameStartHandle" not in instance.data: + return + if "frameEndHandle" not in instance.data: + return + + has_existing_data = { + "handleStart", + "handleEnd", + "frameStart", + "frameEnd" + }.issubset(instance.data) + if has_existing_data: + return + + attr_values = self.get_attr_values_from_data(instance.data) + if attr_values.get("use_handles", self.use_asset_handles): + asset_data = instance.data["assetEntity"]["data"] + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + else: + handle_start = 0 + handle_end = 0 + + frame_start = instance.data["frameStartHandle"] + handle_start + frame_end = instance.data["frameEndHandle"] - handle_end + + instance.data.update({ + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end + }) + + # Log debug message about the collected frame range + if attr_values.get("use_handles", self.use_asset_handles): + self.log.debug( + "Full Frame range with Handles " + "[{frame_start_handle} - {frame_end_handle}]" + .format( + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] + ) + ) + else: + self.log.debug( + "Use handles is deactivated for this instance, " + "start and end handles are set to 0." + ) + + # Log collected frame range to the user + message = "Frame range [{frame_start} - {frame_end}]".format( + frame_start=frame_start, + frame_end=frame_end + ) + if handle_start or handle_end: + message += " with handles [{handle_start}]-[{handle_end}]".format( + handle_start=handle_start, + handle_end=handle_end + ) + self.log.info(message) + + if instance.data.get("byFrameStep", 1.0) != 1.0: + self.log.info( + "Frame steps {}".format(instance.data["byFrameStep"])) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{label} [{frame_start_handle} - {frame_end_handle}]" + .format( + label=label, + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] + ) + ) + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("use_handles", + tooltip="Disable this if you want the publisher to" + " ignore start and end handles specified in the" + " asset data for this publish instance", + default=cls.use_asset_handles, + label="Use asset handles") + ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 01df809d4c..cdef642174 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -11,7 +11,9 @@ from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" - order = pyblish.api.CollectorOrder + 0.01 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "bgeo"] @@ -20,8 +22,8 @@ class CollectFrames(pyblish.api.InstancePlugin): ropnode = hou.node(instance.data["instance_node"]) - start_frame = instance.data.get("frameStart", None) - end_frame = instance.data.get("frameEnd", None) + start_frame = instance.data.get("frameStartHandle", None) + end_frame = instance.data.get("frameEndHandle", None) output_parm = lib.get_output_parameter(ropnode) if start_frame is not None: diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py index 0600730d00..d154cdc7c0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py @@ -122,10 +122,6 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): instance.data.update(save_data) instance.data["usdLayer"] = layer - # Don't allow the Pyblish `instanceToggled` we have installed - # to set this node to bypass. - instance.data["_allowToggleBypass"] = False - instances.append(instance) # Store the collected ROP node dependencies diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index fe0b8711fc..dac350a6ef 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -25,8 +25,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): label = "Karma ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index cc412f30a1..a3e7927807 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -25,8 +25,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): label = "Mantra ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index deb9eac971..0acddab011 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -25,8 +25,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): label = "Redshift ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index 3efb75e66c..9671945b9a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -6,6 +6,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): """Collect Review Data.""" label = "Collect Review Data" + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 hosts = ["houdini"] families = ["review"] @@ -41,8 +43,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): return if focal_length_parm.isTimeDependent(): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + 1 + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + 1 focal_length = [ focal_length_parm.evalAsFloatAtFrame(t) for t in range(int(start), int(end)) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 186244fedd..1e6bc3b16e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -2,22 +2,15 @@ """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api -from openpype.lib import BoolDef from openpype.hosts.houdini.api import lib -from openpype.pipeline import OpenPypePyblishPluginMixin -class CollectRopFrameRange(pyblish.api.InstancePlugin, - OpenPypePyblishPluginMixin): - +class CollectRopFrameRange(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" hosts = ["houdini"] - # This specific order value is used so that - # this plugin runs after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.499 + order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" - use_asset_handles = True def process(self, instance): @@ -30,78 +23,16 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return ropnode = hou.node(node_path) - - attr_values = self.get_attr_values_from_data(instance.data) - - if attr_values.get("use_handles", self.use_asset_handles): - asset_data = instance.data["assetEntity"]["data"] - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) - else: - handle_start = 0 - handle_end = 0 - frame_data = lib.get_frame_data( - ropnode, handle_start, handle_end, self.log + ropnode, self.log ) if not frame_data: return # Log debug message about the collected frame range - frame_start = frame_data["frameStart"] - frame_end = frame_data["frameEnd"] - - if attr_values.get("use_handles", self.use_asset_handles): - self.log.debug( - "Full Frame range with Handles " - "[{frame_start_handle} - {frame_end_handle}]" - .format( - frame_start_handle=frame_data["frameStartHandle"], - frame_end_handle=frame_data["frameEndHandle"] - ) - ) - else: - self.log.debug( - "Use handles is deactivated for this instance, " - "start and end handles are set to 0." - ) - - # Log collected frame range to the user - message = "Frame range [{frame_start} - {frame_end}]".format( - frame_start=frame_start, - frame_end=frame_end + self.log.debug( + "Collected frame_data: {}".format(frame_data) ) - if handle_start or handle_end: - message += " with handles [{handle_start}]-[{handle_end}]".format( - handle_start=handle_start, - handle_end=handle_end - ) - self.log.info(message) - - if frame_data.get("byFrameStep", 1.0) != 1.0: - self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) instance.data.update(frame_data) - - # Add frame range to label if the instance has a frame range. - label = instance.data.get("label", instance.data["name"]) - instance.data["label"] = ( - "{label} [{frame_start} - {frame_end}]" - .format( - label=label, - frame_start=frame_start, - frame_end=frame_end - ) - ) - - @classmethod - def get_attribute_defs(cls): - return [ - BoolDef("use_handles", - tooltip="Disable this if you want the publisher to" - " ignore start and end handles specified in the" - " asset data for this publish instance", - default=cls.use_asset_handles, - label="Use asset handles") - ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 53072aebc6..64de2079cd 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -25,8 +25,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): label = "VRay ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["vray_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py index 0d246625ba..be60217055 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_ass.py +++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py @@ -56,7 +56,7 @@ class ExtractAss(publish.Extractor): 'ext': ext, "files": files, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index c9625ec880..d13141b426 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -47,7 +47,7 @@ class ExtractBGEO(publish.Extractor): "ext": ext.lstrip("."), "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"] + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"] } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py index 7a1ab36b93..11cf83a46d 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_composite.py +++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py @@ -41,8 +41,8 @@ class ExtractComposite(publish.Extractor): "ext": ext, "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } from pprint import pformat diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 7993b3352f..7dc193c6a9 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -40,9 +40,9 @@ class ExtractFBX(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] # set value type for 'representations' key to list if "representations" not in instance.data: diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index 6c36dec5f5..38808089ac 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -39,8 +39,8 @@ class ExtractOpenGL(publish.Extractor): "ext": instance.data["imageFormat"], "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": instance.data.get("review_camera") diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 1d99ac665c..ef5991924f 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -44,8 +44,8 @@ class ExtractRedshiftProxy(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py index 4bca758f08..89af8e1756 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py +++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py @@ -40,7 +40,7 @@ class ExtractVDBCache(publish.Extractor): "ext": "vdb", "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 6a66f3de9f..1b12fa7096 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -57,7 +57,17 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): return rop_node = hou.node(instance.data["instance_node"]) - if instance.data["frameStart"] > instance.data["frameEnd"]: + frame_start = instance.data.get("frameStart") + frame_end = instance.data.get("frameEnd") + + if frame_start is None or frame_end is None: + cls.log.debug( + "Skipping frame range validation for " + "instance without frame data: {}".format(rop_node.path()) + ) + return + + if frame_start > frame_end: cls.log.info( "The ROP node render range is set to " "{0[frameStartHandle]} - {0[frameEndHandle]} " @@ -89,7 +99,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): .format(instance)) return - created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa + created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa create_context.save_changes() cls.log.debug("use asset handles is turned off for '{}'" diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index b2e32a70f9..0903aef7bc 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -4,7 +4,7 @@ - + - + - + 1 - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] if is_sequence: representation = self._representation_with_hash_in_frame( @@ -299,9 +297,6 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(representation) - - read_node["name"].setValue(read_name) read_node["file"].setValue(filepath) # to avoid multiple undo steps for rest of process @@ -356,7 +351,7 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) def remove(self, container): - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index cacc00854e..cc048372d4 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -62,11 +62,12 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -159,7 +160,7 @@ class LoadEffects(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -175,12 +176,13 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -212,7 +214,7 @@ class LoadEffects(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -346,6 +348,6 @@ class LoadEffects(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index bdf3cd6965..cdfdfef3b8 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -63,11 +63,12 @@ class LoadEffectsInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -98,7 +99,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -164,28 +165,26 @@ class LoadEffectsInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") - name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = container['namespace'] colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -217,7 +216,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -251,11 +250,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): output = nuke.createNode("Output") output.setInput(0, pre_node) - # # try to place it under Viewer1 - # if not self.connect_active_viewer(GN): - # nuke.delete(GN) - # return - # get all versions in list last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] @@ -365,6 +359,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 23cf4d7741..19b5cca74e 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -64,11 +64,12 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -111,7 +112,7 @@ class LoadGizmo(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -126,12 +127,13 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -175,6 +177,6 @@ class LoadGizmo(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index ce0a1615f1..5b4877678a 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -66,11 +66,12 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -118,7 +119,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -133,12 +134,13 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -256,6 +258,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 6bffb97e6f..411a61d77b 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -146,8 +146,6 @@ class LoadImage(load.LoaderPlugin): data_imprint.update( {k: context["version"]['data'].get(k, str(None))}) - data_imprint.update({"objectName": read_name}) - r["tile_color"].setValue(int("0x4ecd25ff", 16)) return containerise(r, @@ -168,7 +166,7 @@ class LoadImage(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container["objectName"]) + node = container["node"] frame_number = node["first"].value() assert node.Class() == "Read", "Must be Read" @@ -237,7 +235,7 @@ class LoadImage(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] assert node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index b9b8a0f4c0..3fe92b74d0 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -46,10 +46,11 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -114,9 +115,9 @@ class AlembicModelLoader(load.LoaderPlugin): # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + # get corresponding node - model_node = nuke.toNode(object_name) + model_node = container["node"] # get main variables version_data = version_doc.get("data", {}) @@ -129,11 +130,12 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -142,7 +144,6 @@ class AlembicModelLoader(load.LoaderPlugin): file = get_representation_path(representation).replace("\\", "/") with maintained_selection(): - model_node = nuke.toNode(object_name) model_node['selected'].setValue(True) # collect input output dependencies @@ -163,8 +164,10 @@ class AlembicModelLoader(load.LoaderPlugin): ypos = model_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(model_node) + + # paste the node back and set the position nuke.nodePaste("%clipboard%") - model_node = nuke.toNode(object_name) + model_node = nuke.selectedNode() model_node.setXYpos(xpos, ypos) # link to original input nodes diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 18c8cdba35..c0f8235253 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -55,7 +55,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): """ namespace = namespace or context['asset']['name'] suffix = secrets.token_hex(nbytes=4) - object_name = "{}_{}_{}".format( + node_name = "{}_{}_{}".format( name, namespace, suffix) # getting file path @@ -64,7 +64,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, json_f["data"]) + filepath, json_f["data"]) + # renaming group node + group_node["name"].setValue(node_name) self._node_version_color(context["version"], group_node) @@ -76,17 +78,14 @@ class LoadOcioLookNodes(load.LoaderPlugin): name=name, namespace=namespace, context=context, - loader=self.__class__.__name__, - data={ - "objectName": object_name, - } + loader=self.__class__.__name__ ) def _create_group_node( self, - object_name, filepath, - data + data, + group_node=None ): """Creates group node with all the nodes inside. @@ -94,9 +93,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): in between - in case those are needed. Arguments: - object_name (str): name of the group node filepath (str): path to json file data (dict): data from json file + group_node (Optional[nuke.Node]): group node or None Returns: nuke.Node: group node with all the nodes inside @@ -117,7 +116,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): input_node = None output_node = None - group_node = nuke.toNode(object_name) if group_node: # remove all nodes between Input and Output nodes for node in group_node.nodes(): @@ -130,7 +128,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): else: group_node = nuke.createNode( "Group", - "name {}_1".format(object_name), inpanel=False ) @@ -227,16 +224,16 @@ class LoadOcioLookNodes(load.LoaderPlugin): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + group_node = container["node"] filepath = get_representation_path(representation) json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, - json_f["data"] + json_f["data"], + group_node ) self._node_version_color(version_doc, group_node) diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index d5f9d24765..cbe19d217b 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -46,8 +46,6 @@ class LinkAsGroup(load.LoaderPlugin): file = self.filepath_from_context(context).replace("\\", "/") self.log.info("file: {}\n".format(file)) - precomp_name = context["representation"]["context"]["subset"] - self.log.info("versionData: {}\n".format(context["version"]["data"])) # add additional metadata from the version to imprint to Avalon knob @@ -62,7 +60,6 @@ class LinkAsGroup(load.LoaderPlugin): } for k in add_keys: data_imprint.update({k: context["version"]['data'][k]}) - data_imprint.update({"objectName": precomp_name}) # group context is set to precomp, so back up one level. nuke.endGroup() @@ -118,7 +115,7 @@ class LinkAsGroup(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container['objectName']) + node = container["node"] root = get_representation_path(representation).replace("\\", "/") @@ -159,6 +156,6 @@ class LinkAsGroup(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 56ae2a4c25..4e0dbcad06 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -48,11 +48,6 @@ class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", on_application_launch) @@ -177,11 +172,6 @@ def on_application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active Photoshop document diff --git a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt similarity index 89% rename from openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt rename to openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt index 98597a12cb..7d1d6edf61 100644 --- a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt +++ b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt @@ -1,4 +1,4 @@ -Updated as of 9 May 2022 +Updated as of 26 May 2023 ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. @@ -19,7 +19,7 @@ DaVinci Resolve scripting requires one of the following to be installed (for all Lua 5.1 Python 2.7 64-bit - Python 3.6 64-bit + Python >= 3.6 64-bit Using a script @@ -171,6 +171,10 @@ Project GetRenderResolutions(format, codec) --> [{Resolution}] # Returns list of resolutions applicable for the given render format (string) and render codec (string). Returns full list of resolutions if no argument is provided. Each element in the list is a dictionary with 2 keys "Width" and "Height". RefreshLUTList() --> Bool # Refreshes LUT List GetUniqueId() --> string # Returns a unique ID for the project item + InsertAudioToCurrentTrackAtPlayhead(mediaPath, --> Bool # Inserts the media specified by mediaPath (string) with startOffsetInSamples (int) and durationInSamples (int) at the playhead on a selected track on the Fairlight page. Returns True if successful, otherwise False. + startOffsetInSamples, durationInSamples) + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for project when supplied presetName (string). Returns true if successful. + ExportCurrentFrameAsStill(filePath) --> Bool # Exports current frame as still to supplied filePath. filePath must end in valid export file format. Returns True if succssful, False otherwise. MediaStorage GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage. @@ -179,6 +183,7 @@ MediaStorage RevealInStorage(path) --> Bool # Expands and displays given file/folder path in Resolve’s Media Storage. AddItemListToMediaPool(item1, item2, ...) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is one or more file/folder paths. Returns a list of the MediaPoolItems created. AddItemListToMediaPool([items...]) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is an array of file/folder paths. Returns a list of the MediaPoolItems created. + AddItemListToMediaPool([{itemInfo}, ...]) --> [clips...] # Adds list of itemInfos specified as dict of "media", "startFrame" (int), "endFrame" (int) from Media Storage into current Media Pool folder. Returns a list of the MediaPoolItems created. AddClipMattesToMediaPool(MediaPoolItem, [paths], stereoEye) --> Bool # Adds specified media files as mattes for the specified MediaPoolItem. StereoEye is an optional argument for specifying which eye to add the matte to for stereo clips ("left" or "right"). Returns True if successful. AddTimelineMattesToMediaPool([paths]) --> [MediaPoolItems] # Adds specified media files as timeline mattes in current media pool folder. Returns a list of created MediaPoolItems. @@ -189,20 +194,22 @@ MediaPool CreateEmptyTimeline(name) --> Timeline # Adds new timeline with given name. AppendToTimeline(clip1, clip2, ...) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. AppendToTimeline([clips]) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. - AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only). Returns the list of appended timelineItems. + AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only), "trackIndex" (int) and "recordFrame" (int). Returns the list of appended timelineItems. CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [clips]) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. - CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int). - ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file and optional importOptions dict, with support for the keys: - # "timelineName": string, specifies the name of the timeline to be created - # "importSourceClips": Bool, specifies whether source clips should be imported, True by default + CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), "recordFrame" (int). + ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys: + # "timelineName": string, specifies the name of the timeline to be created. Not valid for DRT import + # "importSourceClips": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import # "sourceClipsPath": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if "importSourceClips" is True - # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False + # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False. Not valid for DRT import # "interlaceProcessing": Bool, specifies whether to enable interlace processing on the imported timeline being created. valid only for AAF import DeleteTimelines([timeline]) --> Bool # Deletes specified timelines in the media pool. GetCurrentFolder() --> Folder # Returns currently selected Folder. SetCurrentFolder(Folder) --> Bool # Sets current folder by given Folder. DeleteClips([clips]) --> Bool # Deletes specified clips or timeline mattes in the media pool + ImportFolderFromFile(filePath, sourceClipsPath="") --> Bool # Returns true if import from given DRB filePath is successful, false otherwise + # sourceClipsPath is a string that specifies a filesystem path to search for source clips if the media is inaccessible in their original path, empty by default DeleteFolders([subfolders]) --> Bool # Deletes specified subfolders in the media pool MoveClips([clips], targetFolder) --> Bool # Moves specified clips to target folder. MoveFolders([folders], targetFolder) --> Bool # Moves specified folders to target folder. @@ -225,6 +232,7 @@ Folder GetSubFolderList() --> [folders...] # Returns a list of subfolders in the folder. GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise GetUniqueId() --> string # Returns a unique ID for the media pool folder + Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise MediaPoolItem GetName() --> string # Returns the clip name. @@ -257,6 +265,8 @@ MediaPoolItem UnlinkProxyMedia() --> Bool # Unlinks any proxy media associated with clip. ReplaceClip(filePath) --> Bool # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path. GetUniqueId() --> string # Returns a unique ID for the media pool item + TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise + ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise. Timeline GetName() --> string # Returns the timeline name. @@ -266,6 +276,23 @@ Timeline SetStartTimecode(timecode) --> Bool # Set the start timecode of the timeline to the string 'timecode'. Returns true when the change is successful, false otherwise. GetStartTimecode() --> string # Returns the start timecode for the timeline. GetTrackCount(trackType) --> int # Returns the number of tracks for the given track type ("audio", "video" or "subtitle"). + AddTrack(trackType, optionalSubTrackType) --> Bool # Adds track of trackType ("video", "subtitle", "audio"). Second argument optionalSubTrackType is required for "audio" + # optionalSubTrackType can be one of {"mono", "stereo", "5.1", "5.1film", "7.1", "7.1film", "adaptive1", ... , "adaptive24"} + DeleteTrack(trackType, trackIndex) --> Bool # Deletes track of trackType ("video", "subtitle", "audio") and given trackIndex. 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackEnable(trackType, trackIndex, Bool) --> Bool # Enables/Disables track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackEnabled(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is enabled and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackLock(trackType, trackIndex, Bool) --> Bool # Locks/Unlocks track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackLocked(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is locked and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + DeleteClips([timelineItems], Bool) --> Bool # Deletes specified TimelineItems from the timeline, performing ripple delete if the second argument is True. Second argument is optional (The default for this is False) + SetClipsLinked([timelineItems], Bool) --> Bool # Links or unlinks the specified TimelineItems depending on second argument. GetItemListInTrack(trackType, index) --> [items...] # Returns a list of timeline items on that track (based on trackType and index). 1 <= index <= GetTrackCount(trackType). AddMarker(frameId, color, name, note, duration, --> Bool # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker. customData) @@ -301,7 +328,7 @@ Timeline # "sourceClipsFolders": string, list of Media Pool folder objects to search for source clips if the media is not present in current folder Export(fileName, exportType, exportSubtype) --> Bool # Exports timeline to 'fileName' as per input exportType & exportSubtype format. - # Refer to section "Looking up timeline exports properties" for information on the parameters. + # Refer to section "Looking up timeline export properties" for information on the parameters. GetSetting(settingName) --> string # Returns value of timeline setting (indicated by settingName : string). Check the section below for more information. SetSetting(settingName, settingValue) --> Bool # Sets timeline setting (indicated by settingName : string) to the value (settingValue : string). Check the section below for more information. InsertGeneratorIntoTimeline(generatorName) --> TimelineItem # Inserts a generator (indicated by generatorName : string) into the timeline. @@ -313,6 +340,8 @@ Timeline GrabStill() --> galleryStill # Grabs still from the current video clip. Returns a GalleryStill object. GrabAllStills(stillFrameSource) --> [galleryStill] # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects. GetUniqueId() --> string # Returns a unique ID for the timeline + CreateSubtitlesFromAudio() --> Bool # Creates subtitles from audio for the timeline. Returns True on success, False otherwise. + DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. TimelineItem GetName() --> string # Returns the item name. @@ -362,6 +391,7 @@ TimelineItem GetStereoLeftFloatingWindowParams() --> {keyframes...} # For the LEFT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetStereoRightFloatingWindowParams() --> {keyframes...} # For the RIGHT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item + ApplyArriCdlLut() --> Bool # Applies ARRI CDL and LUT. Returns True if successful, False otherwise. SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes. # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). @@ -376,8 +406,16 @@ TimelineItem SelectTakeByIndex(idx) --> Bool # Selects a take by index, 1 <= idx <= number of takes. FinalizeTake() --> Bool # Finalizes take selection. CopyGrades([tgtTimelineItems]) --> Bool # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occurred. + SetClipEnabled(Bool) --> Bool # Sets clip enabled based on argument. + GetClipEnabled() --> Bool # Gets clip enabled status. UpdateSidecar() --> Bool # Updates sidecar file for BRAW clips or RMD file for R3D clips. GetUniqueId() --> string # Returns a unique ID for the timeline item + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for clip when supplied presetName (string). Returns true if successful. + GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. + CreateMagicMask(mode) --> Bool # Returns True if magic mask was created successfully, False otherwise. mode can "F" (forward), "B" (backward), or "BI" (bidirection) + RegenerateMagicMask() --> Bool # Returns True if magic mask was regenerated successfully, False otherwise. + Stabilize() --> Bool # Returns True if stabilization was successful, False otherwise + SmartReframe() --> Bool # Performs Smart Reframe. Returns True if successful, False otherwise. Gallery GetAlbumName(galleryStillAlbum) --> string # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'. @@ -422,9 +460,11 @@ Invoke "Project:SetSetting", "Timeline:SetSetting" or "MediaPoolItem:SetClipProp ensure the success of the operation. You can troubleshoot the validity of keys and values by setting the desired result from the UI and checking property snapshots before and after the change. The following Project properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 0 and 3 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"superScale" - the property value is an enumerated integer between 0 and 4 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = Project:GetSetting('superScale') and Project:SetSetting('superScale', x) +• for '2x Enhanced' --> Project:SetSetting('superScale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] "timelineFrameRate" - the property value is one of the frame rates available to the user in project settings under "Timeline frame rate" option. Drop Frame can be configured for supported frame rates by appending the frame rate with "DF", e.g. "29.97 DF" will enable drop frame and "29.97" will disable drop frame @@ -432,9 +472,11 @@ Affects: • x = Project:GetSetting('timelineFrameRate') and Project:SetSetting('timelineFrameRate', x) The following Clip properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 1 and 3 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"Super Scale" - the property value is an enumerated integer between 1 and 4 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x) +• for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] Looking up Render Settings @@ -478,11 +520,6 @@ exportType can be one of the following constants: - resolve.EXPORT_DRT - resolve.EXPORT_EDL - resolve.EXPORT_FCP_7_XML - - resolve.EXPORT_FCPXML_1_3 - - resolve.EXPORT_FCPXML_1_4 - - resolve.EXPORT_FCPXML_1_5 - - resolve.EXPORT_FCPXML_1_6 - - resolve.EXPORT_FCPXML_1_7 - resolve.EXPORT_FCPXML_1_8 - resolve.EXPORT_FCPXML_1_9 - resolve.EXPORT_FCPXML_1_10 @@ -492,6 +529,8 @@ exportType can be one of the following constants: - resolve.EXPORT_TEXT_TAB - resolve.EXPORT_DOLBY_VISION_VER_2_9 - resolve.EXPORT_DOLBY_VISION_VER_4_0 + - resolve.EXPORT_DOLBY_VISION_VER_5_1 + - resolve.EXPORT_OTIO exportSubtype can be one of the following enums: - resolve.EXPORT_NONE - resolve.EXPORT_AAF_NEW @@ -504,6 +543,16 @@ When exportType is resolve.EXPORT_AAF, valid exportSubtype values are resolve.EX When exportType is resolve.EXPORT_EDL, valid exportSubtype values are resolve.EXPORT_CDL, resolve.EXPORT_SDL, resolve.EXPORT_MISSING_CLIPS and resolve.EXPORT_NONE. Note: Replace 'resolve.' when using the constants above, if a different Resolve class instance name is used. +Unsupported exportType types +--------------------------------- +Starting with DaVinci Resolve 18.1, the following export types are not supported: + - resolve.EXPORT_FCPXML_1_3 + - resolve.EXPORT_FCPXML_1_4 + - resolve.EXPORT_FCPXML_1_5 + - resolve.EXPORT_FCPXML_1_6 + - resolve.EXPORT_FCPXML_1_7 + + Looking up Timeline item properties ----------------------------------- This section covers additional notes for the function "TimelineItem:SetProperty" and "TimelineItem:GetProperty". These functions are used to get and set properties mentioned. diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 37410c9727..aef9caca78 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -6,7 +6,10 @@ import contextlib from opentimelineio import opentime from openpype.lib import Logger -from openpype.pipeline.editorial import is_overlapping_otio_ranges +from openpype.pipeline.editorial import ( + is_overlapping_otio_ranges, + frames_to_timecode +) from ..otio import davinci_export as otio_export @@ -246,18 +249,22 @@ def get_media_pool_item(filepath, root: object = None) -> object: return None -def create_timeline_item(media_pool_item: object, - timeline: object = None, - source_start: int = None, - source_end: int = None) -> object: +def create_timeline_item( + media_pool_item: object, + timeline: object = None, + timeline_in: int = None, + source_start: int = None, + source_end: int = None, +) -> object: """ Add media pool item to current or defined timeline. Args: media_pool_item (resolve.MediaPoolItem): resolve's object - timeline (resolve.Timeline)[optional]: resolve's object - source_start (int)[optional]: media source input frame (sequence frame) - source_end (int)[optional]: media source output frame (sequence frame) + timeline (Optional[resolve.Timeline]): resolve's object + timeline_in (Optional[int]): timeline input frame (sequence frame) + source_start (Optional[int]): media source input frame (sequence frame) + source_end (Optional[int]): media source output frame (sequence frame) Returns: object: resolve.TimelineItem @@ -269,16 +276,29 @@ def create_timeline_item(media_pool_item: object, clip_name = _clip_property("File Name") timeline = timeline or get_current_timeline() + # timing variables + if all([timeline_in, source_start, source_end]): + fps = timeline.GetSetting("timelineFrameRate") + duration = source_end - source_start + timecode_in = frames_to_timecode(timeline_in, fps) + timecode_out = frames_to_timecode(timeline_in + duration, fps) + else: + timecode_in = None + timecode_out = None + # if timeline was used then switch it to current timeline with maintain_current_timeline(timeline): # Add input mediaPoolItem to clip data - clip_data = {"mediaPoolItem": media_pool_item} + clip_data = { + "mediaPoolItem": media_pool_item, + } - # add source time range if input was given - if source_start is not None: - clip_data.update({"startFrame": source_start}) - if source_end is not None: - clip_data.update({"endFrame": source_end}) + if source_start: + clip_data["startFrame"] = source_start + if source_end: + clip_data["endFrame"] = source_end + if timecode_in: + clip_data["recordFrame"] = timecode_in # add to timeline media_pool.AppendToTimeline([clip_data]) @@ -286,10 +306,15 @@ def create_timeline_item(media_pool_item: object, output_timeline_item = get_timeline_item( media_pool_item, timeline) - assert output_timeline_item, AssertionError( - "Track Item with name `{}` doesn't exist on the timeline: `{}`".format( - clip_name, timeline.GetName() - )) + assert output_timeline_item, AssertionError(( + "Clip name '{}' was't created on the timeline: '{}' \n\n" + "Please check if correct track position is activated, \n" + "or if a clip is not already at the timeline in \n" + "position: '{}' out: '{}'. \n\n" + "Clip data: {}" + ).format( + clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data + )) return output_timeline_item @@ -490,7 +515,7 @@ def imprint(timeline_item, data=None): Arguments: timeline_item (hiero.core.TrackItem): hiero track item object - data (dict): Any data which needst to be imprinted + data (dict): Any data which needs to be imprinted Examples: data = { diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index a3d533d3d7..49c3b484d2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -307,11 +307,18 @@ class ClipLoader: self.active_project = lib.get_current_project() # try to get value from options or evaluate key value for `handles` - self.with_handles = options.get("handles") or bool( - options.get("handles") is True) + self.with_handles = options.get("handles") is True + # try to get value from options or evaluate key value for `load_to` - self.new_timeline = options.get("newTimeline") or bool( - "New timeline" in options.get("load_to", "")) + self.new_timeline = ( + options.get("newTimeline") or + options.get("load_to") == "New timeline" + ) + # try to get value from options or evaluate key value for `load_how` + self.sequential_load = ( + options.get("sequentially") or + options.get("load_how") == "Sequentially in order" + ) assert self._populate_data(), str( "Cannot Load selected data, look into database " @@ -392,30 +399,70 @@ class ClipLoader: # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - handle_start = self.data["versionData"].get("handleStart") or 0 - handle_end = self.data["versionData"].get("handleEnd") or 0 - + # create mediaItem in active project bin + # create clip media media_pool_item = lib.create_media_pool_item( files, self.active_bin ) _clip_property = media_pool_item.GetClipProperty + # get handles + handle_start = self.data["versionData"].get("handleStart") + handle_end = self.data["versionData"].get("handleEnd") + if handle_start is None: + handle_start = int(self.data["assetData"]["handleStart"]) + if handle_end is None: + handle_end = int(self.data["assetData"]["handleEnd"]) + + # check frame duration from versionData or assetData + frame_start = self.data["versionData"].get("frameStart") + if frame_start is None: + frame_start = self.data["assetData"]["frameStart"] + + # check frame duration from versionData or assetData + frame_end = self.data["versionData"].get("frameEnd") + if frame_end is None: + frame_end = self.data["assetData"]["frameEnd"] + + db_frame_duration = int(frame_end) - int(frame_start) + 1 + + # get timeline in + timeline_start = self.active_timeline.GetStartFrame() + if self.sequential_load: + # set timeline start frame + timeline_in = int(timeline_start) + else: + # set timeline start frame + original clip in frame + timeline_in = int( + timeline_start + self.data["assetData"]["clipIn"]) + source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) + source_duration = int(_clip_property("Frames")) - if _clip_property("Type") == "Video": + # check if source duration is shorter than db frame duration + source_with_handles = True + if source_duration < db_frame_duration: + source_with_handles = False + + # only exclude handles if source has no handles or + # if user wants to load without handles + if ( + not self.with_handles + or not source_with_handles + ): source_in += handle_start source_out -= handle_end - # include handles - if self.with_handles: - source_in -= handle_start - source_out += handle_end - # make track item from source in bin as item timeline_item = lib.create_timeline_item( - media_pool_item, self.active_timeline, source_in, source_out) + media_pool_item, + self.active_timeline, + timeline_in, + source_in, + source_out, + ) print("Loading clips: `{}`".format(self.data["clip_name"])) return timeline_item @@ -456,7 +503,7 @@ class TimelineItemLoader(LoaderPlugin): """ options = [ - qargparse.Toggle( + qargparse.Boolean( "handles", label="Include handles", default=0, @@ -471,6 +518,16 @@ class TimelineItemLoader(LoaderPlugin): ], default=0, help="Where do you want clips to be loaded?" + ), + qargparse.Choice( + "load_how", + label="How to load clips", + items=[ + "Original timing", + "Sequentially in order" + ], + default="Original timing", + help="Would you like to place it at original timing?" ) ] diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index d67ef8f798..2c4d8160a6 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -21,6 +21,7 @@ from aiohttp_json_rpc.protocol import ( ) from aiohttp_json_rpc.exceptions import RpcError +from openpype import AYON_SERVER_ENABLED from openpype.lib import emit_event from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path @@ -834,8 +835,12 @@ class BaseCommunicator: class QtCommunicator(BaseCommunicator): + label = os.getenv("AVALON_LABEL") + if not label: + label = "AYON" if AYON_SERVER_ENABLED else "OpenPype" + title = "{} Tools".format(label) menu_definitions = { - "title": "OpenPype Tools", + "title": title, "menu_items": [ { "callback": "workfiles_tool", diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 58fbd09545..c125da1533 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -7,7 +7,7 @@ import requests import pyblish.api -from openpype.client import get_project, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.hosts.tvpaint import TVPAINT_ROOT_DIR from openpype.settings import get_current_project_settings @@ -84,10 +84,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(load_dir) register_creator_plugin_path(create_dir) - registered_callbacks = ( - pyblish.api.registered_callbacks().get("instanceToggled") or [] - ) - register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 95a5cd77bd..56b51c812a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -69,7 +69,6 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "asset_name": context.data["asset"], "task_name": context.data["task"] } - context.data["previous_context"] = current_context self.log.debug("Current context is: {}".format(current_context)) # Collect context from workfile metadata diff --git a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py index 8ef333effd..2ac02f233e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py @@ -1,3 +1,6 @@ +import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -73,8 +76,21 @@ class FirstVersionStatus(BaseEvent): if not self.task_status_map: return - entities_info = self.filter_event_ents(event) - if not entities_info: + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return + + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) + + def process_by_project(self, session, event, project_id, entities_info): + project_name = self.get_project_name_from_event( + session, event, project_id + ) + if get_project(project_name) is None: + self.log.debug( + f"Project '{project_name}' not found in OpenPype. Skipping" + ) return entity_ids = [] @@ -154,18 +170,18 @@ class FirstVersionStatus(BaseEvent): exc_info=True ) - def filter_event_ents(self, event): - filtered_ents = [] - for entity in event["data"].get("entities", []): + def filter_entities_info(self, event): + filtered_entities_info = collections.defaultdict(list) + for entity_info in event["data"].get("entities", []): # Care only about add actions - if entity.get("action") != "add": + if entity_info.get("action") != "add": continue # Filter AssetVersions - if entity["entityType"] != "assetversion": + if entity_info["entityType"] != "assetversion": continue - entity_changes = entity.get("changes") or {} + entity_changes = entity_info.get("changes") or {} # Check if version of Asset Version is `1` version_num = entity_changes.get("version", {}).get("new") @@ -177,9 +193,18 @@ class FirstVersionStatus(BaseEvent): if not task_id: continue - filtered_ents.append(entity) + project_id = None + for parent_item in reversed(entity_info["parents"]): + if parent_item["entityType"] == "show": + project_id = parent_item["entityId"] + break - return filtered_ents + if project_id is None: + continue + + filtered_entities_info[project_id].append(entity_info) + + return filtered_entities_info def register(session): 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 a100c34f67..07a8ff433e 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 @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -99,6 +101,10 @@ class NextTaskUpdate(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index ed630ad59d..65c3c1a69a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -3,6 +3,8 @@ import copy from typing import Any import ftrack_api + +from openpype.client import get_project from openpype_modules.ftrack.lib import ( BaseEvent, query_custom_attributes, @@ -139,6 +141,10 @@ class PushHierValuesToNonHierEvent(BaseEvent): project_name: str = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return set(), set() + # Load settings project_settings: dict[str, Any] = ( self.get_project_settings_from_event(event, project_name) diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py index 25fa3b0535..d2b395a1a3 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -60,6 +62,10 @@ class TaskStatusToParent(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py index b77849c678..91ee2410d7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -102,6 +104,10 @@ class TaskToVersionStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py index 64673f792c..318e69f414 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py +++ b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -22,6 +24,10 @@ class ThumbnailEvents(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index fb40fd6417..fbe44bcba7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -1,3 +1,4 @@ +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -50,6 +51,10 @@ class VersionToTaskStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name 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/pype_commands.py b/openpype/pype_commands.py index 071ecfffd2..b5828d3dfe 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -214,7 +214,7 @@ class PypeCommands: def run_tests(self, folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url): + mongo_url, app_group): """ Runs tests from 'folder' @@ -260,6 +260,9 @@ class PypeCommands: if persist: args.extend(["--persist", persist]) + if app_group: + args.extend(["--app_group", app_group]) + if app_variant: args.extend(["--app_variant", app_variant]) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..a31c8a04e0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -639,6 +639,15 @@ def _convert_3dsmax_project_settings(ayon_settings, output): for item in point_cloud_attribute } ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + # --- Publish (START) --- + ayon_publish = ayon_max["publish"] + try: + attributes = json.loads( + ayon_publish["ValidateAttributes"]["attributes"] + ) + except ValueError: + attributes = {} + ayon_publish["ValidateAttributes"]["attributes"] = attributes output["max"] = ayon_max diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 87983620ec..0dd8443e44 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -137,7 +137,7 @@ } }, "publish": { - "CollectRopFrameRange": { + "CollectAssetHandles": { "use_asset_handles": true }, "ValidateContainers": { diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..24a87020bb 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -36,6 +36,10 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateAttributes": { + "enabled": false, + "attributes": {} } } } diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 6a0ddb398e..a5283751e9 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -344,13 +344,30 @@ }, "environment": {} }, + "11-0": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke11.0v4\\Nuke11.0.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, "__dynamic_keys_labels__": { "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", - "11-2": "11.2" + "11-2": "11.2", + "11-0": "11.0" } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index 0de9f21c9f..324cfd8d58 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -11,8 +11,8 @@ { "type": "dict", "collapsible": true, - "key": "CollectRopFrameRange", - "label": "Collect Rop Frame Range", + "key": "CollectAssetHandles", + "label": "Collect Asset Handles", "children": [ { "type": "label", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index 35d768843d..cee04b73e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -5,70 +5,99 @@ "is_group": true, "use_label_wrap": true, "object_type": { - "type": "dict", - "children": [ + "type": "dict-conditional", + "enum_key": "options", + "enum_label": "Options", + "enum_children": [ { - "type": "text", - "key": "shelf_set_name", - "label": "Shelf Set Name" - }, - { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path (optional)", - "multipath": false, - "multiplatform": true - }, - { - "type": "list", - "key": "shelf_definition", - "label": "Shelves", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "shelf_name", - "label": "Shelf Name" - }, - { - "type": "list", - "key": "tools_list", - "label": "Tools", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Name and Script Path are mandatory." - }, - { - "type": "text", - "key": "label", - "label": "Name" - }, - { - "type": "path", - "key": "script", - "label": "Script" - }, - { - "type": "path", - "key": "icon", - "label": "Icon" - }, - { - "type": "text", - "key": "help", - "label": "Help" - } - ] + + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "dict", + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true } - } - ] - } + ] + } + ] + }, + { + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "children": [ + { + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_set_name", + "label": "Shelf Set Name" + }, + { + "type": "list", + "key": "shelf_definition", + "label": "Shelves Definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_name", + "label": "Shelf Name" + }, + { + "type": "list", + "key": "tools_list", + "label": "Tools", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, + { + "type": "text", + "key": "label", + "label": "Name" + }, + { + "type": "path", + "key": "script", + "label": "Script" + }, + { + "type": "path", + "key": "icon", + "label": "Icon" + }, + { + "type": "text", + "key": "help", + "label": "Help" + } + ] + } + } + ] + } + } + ] + } + ] } ] } 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..c3b56bae5e 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,25 @@ "label": "Active" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateAttributes", + "label": "ValidateAttributes", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "raw-json", + "key": "attributes", + "label": "Attributes" + } + ] } ] } diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 7dbaec5147..38c7f62bd5 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets, QtCore from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils import PlaceholderLineEdit, RefreshButton from openpype.tools.ayon_utils.widgets import ( - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -95,7 +95,7 @@ class ProjectsWidget(QtWidgets.QWidget): projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(projects_view) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) @@ -133,9 +133,14 @@ class ProjectsWidget(QtWidgets.QWidget): return self._projects_model.has_content() def _on_view_clicked(self, index): - if index.isValid(): - project_name = index.data(QtCore.Qt.DisplayRole) - self._controller.set_selected_project(project_name) + if not index.isValid(): + return + model = index.model() + flags = model.flags(index) + if not flags & QtCore.Qt.ItemIsEnabled: + return + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) def _on_project_filter_change(self, text): self._projects_proxy_model.setFilterFixedString(text) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py index 53351f76d9..eaaf7ca617 100644 --- a/openpype/tools/ayon_loader/ui/folders_widget.py +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -8,7 +8,7 @@ from openpype.tools.utils import ( from openpype.style import get_objected_colors from openpype.tools.ayon_utils.widgets import ( - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE @@ -182,7 +182,7 @@ class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): painter.restore() -class LoaderFoldersModel(FoldersModel): +class LoaderFoldersModel(FoldersQtModel): def __init__(self, *args, **kwargs): super(LoaderFoldersModel, self).__init__(*args, **kwargs) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 4ad53fbbfa..36d53edc24 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -87,7 +87,7 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): - self._projects_cache = CacheItem(default_factory=dict) + self._projects_cache = CacheItem(default_factory=list) self._project_items_by_name = {} self._projects_by_name = {} @@ -103,8 +103,18 @@ class ProjectsModel(object): self._refresh_projects_cache() def get_project_items(self, sender): + """ + + Args: + sender (str): Name of sender who asked for items. + + Returns: + Union[list[ProjectItem], None]: List of project items, or None + if model is refreshing. + """ + if not self._projects_cache.is_valid: - self._refresh_projects_cache(sender) + return self._refresh_projects_cache(sender) return self._projects_cache.get_data() def get_project_entity(self, project_name): @@ -136,11 +146,12 @@ class ProjectsModel(object): def _refresh_projects_cache(self, sender=None): if self._is_refreshing: - return + return None with self._project_refresh_event_manager(sender): project_items = self._query_projects() self._projects_cache.update_data(project_items) + return self._projects_cache.get_data() def _query_projects(self): projects = ayon_api.get_projects(fields=["name", "active", "library"]) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 432a249a73..f58de17c4a 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -1,19 +1,19 @@ from .projects_widget import ( # ProjectsWidget, ProjectsCombobox, - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) from .folders_widget import ( FoldersWidget, - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from .tasks_widget import ( TasksWidget, - TasksModel, + TasksQtModel, TASKS_MODEL_SENDER_NAME, ) from .utils import ( @@ -25,15 +25,15 @@ from .utils import ( __all__ = ( # "ProjectsWidget", "ProjectsCombobox", - "ProjectsModel", + "ProjectsQtModel", "ProjectSortFilterProxy", "FoldersWidget", - "FoldersModel", + "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "TasksWidget", - "TasksModel", + "TasksQtModel", "TASKS_MODEL_SENDER_NAME", "get_qt_icon", diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 322553c51c..44323a192c 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -16,7 +16,7 @@ FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3 FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class FoldersModel(QtGui.QStandardItemModel): +class FoldersQtModel(QtGui.QStandardItemModel): """Folders model which cares about refresh of folders. Args: @@ -26,7 +26,7 @@ class FoldersModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersModel, self).__init__() + super(FoldersQtModel, self).__init__() self._controller = controller self._items_by_id = {} @@ -104,8 +104,8 @@ class FoldersModel(QtGui.QStandardItemModel): if not project_name: self._last_project_name = project_name - self._current_refresh_thread = None self._fill_items({}) + self._current_refresh_thread = None return self._is_refreshing = True @@ -152,6 +152,7 @@ class FoldersModel(QtGui.QStandardItemModel): return self._fill_items(thread.get_result()) + self._current_refresh_thread = None def _fill_item_data(self, item, folder_item): """ @@ -281,7 +282,7 @@ class FoldersWidget(QtWidgets.QWidget): folders_view = TreeView(self) folders_view.setHeaderHidden(True) - folders_model = FoldersModel(controller) + folders_model = FoldersQtModel(controller) folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index be18cfe3ed..f98bfcdf8a 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -10,11 +10,11 @@ PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 -class ProjectsModel(QtGui.QStandardItemModel): +class ProjectsQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(ProjectsModel, self).__init__() + super(ProjectsQtModel, self).__init__() self._controller = controller self._project_items = {} @@ -35,12 +35,11 @@ class ProjectsModel(QtGui.QStandardItemModel): self._selected_project = None - self._is_refreshing = False self._refresh_thread = None @property def is_refreshing(self): - return self._is_refreshing + return self._refresh_thread is not None def refresh(self): self._refresh() @@ -169,28 +168,33 @@ class ProjectsModel(QtGui.QStandardItemModel): return self._select_item def _refresh(self): - if self._is_refreshing: + if self._refresh_thread is not None: return - self._is_refreshing = True + refresh_thread = RefreshThread( "projects", self._query_project_items ) refresh_thread.refresh_finished.connect(self._refresh_finished) - refresh_thread.start() + self._refresh_thread = refresh_thread + refresh_thread.start() def _query_project_items(self): - return self._controller.get_project_items() + return self._controller.get_project_items( + sender=PROJECTS_MODEL_SENDER + ) def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() + if result is not None: + self._fill_items(result) + self._refresh_thread = None - - self._fill_items(result) - - self._is_refreshing = False - self.refreshed.emit() + if result is None: + self._refresh() + else: + self.refreshed.emit() def _fill_items(self, project_items): new_project_names = { @@ -403,7 +407,7 @@ class ProjectsCombobox(QtWidgets.QWidget): projects_combobox = QtWidgets.QComboBox(self) combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) projects_combobox.setItemDelegate(combobox_delegate) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) projects_combobox.setModel(projects_proxy_model) diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index d01b3a7917..f27711acdd 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -12,7 +12,7 @@ ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class TasksModel(QtGui.QStandardItemModel): +class TasksQtModel(QtGui.QStandardItemModel): """Tasks model which cares about refresh of tasks by folder id. Args: @@ -22,7 +22,7 @@ class TasksModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(TasksModel, self).__init__() + super(TasksQtModel, self).__init__() self._controller = controller @@ -185,28 +185,7 @@ class TasksModel(QtGui.QStandardItemModel): 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. - """ - - # Make sure to remove thread from '_refresh_threads' dict - thread = self._refresh_threads.pop(thread_id) - if ( - self._current_refresh_thread is None - or thread_id != self._current_refresh_thread.id - ): - return - + def _fill_data_from_thread(self, thread): task_items = thread.get_result() # Task items are refreshed if task_items is None: @@ -247,7 +226,33 @@ class TasksModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + 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. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_data_from_thread(thread) + + root_item = self.invisibleRootItem() self._has_content = root_item.rowCount() > 0 + self._current_refresh_thread = None self._is_refreshing = False self.refreshed.emit() @@ -280,7 +285,7 @@ class TasksModel(QtGui.QStandardItemModel): if section == 0: return "Tasks" - return super(TasksModel, self).headerData( + return super(TasksQtModel, self).headerData( section, orientation, role ) @@ -305,7 +310,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) - tasks_model = TasksModel(controller) + tasks_model = TasksQtModel(controller) tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openpype/tools/ayon_utils/widgets/utils.py b/openpype/tools/ayon_utils/widgets/utils.py index 8bc3b1ea9b..2817b5efc0 100644 --- a/openpype/tools/ayon_utils/widgets/utils.py +++ b/openpype/tools/ayon_utils/widgets/utils.py @@ -54,6 +54,8 @@ class _IconsCache: @classmethod def get_icon(cls, icon_def): + if not icon_def: + return None icon_type = icon_def["type"] cache_key = cls._get_cache_key(icon_def) cache = cls._cache.get(cache_key) diff --git a/openpype/version.py b/openpype/version.py index 4865fcfb31..c6ebd65e9c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5-nightly.2" +__version__ = "3.17.5-nightly.3" diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index 69a759465e..93d8db964d 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -21,7 +21,7 @@ class ValidateBaseModel(BaseSettingsModel): class CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = Field( - True, title="Collect Anatomy Instance Data" + True, title="Follow workfile version" ) diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index d7c7b367b7..0dd9d396ae 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -489,7 +489,7 @@ DEFAULT_TOOLS_VALUES = { "template_name": "publish_online" }, { - "families": [ + "product_types": [ "tycache" ], "hosts": [ diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d48695a9c..54b7ff57c1 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -267,7 +267,7 @@ class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): title="Reviewable products filter", ) - @validator("aov_filter", "skip_integration_repre_list") + @validator("aov_filter") def validate_unique_names(cls, value): ensure_unique_names(value) return value diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 6615e34ca5..342bf957c1 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -3,7 +3,7 @@ from ayon_server.settings import BaseSettingsModel # Publish Plugins -class CollectRopFrameRangeModel(BaseSettingsModel): +class CollectAssetHandlesModel(BaseSettingsModel): """Collect Frame Range Disable this if you want the publisher to ignore start and end handles specified in the diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 8d0512bdeb..133c18f77c 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -22,16 +22,46 @@ class ShelfDefinitionModel(BaseSettingsModel): ) -class ShelvesModel(BaseSettingsModel): - _layout = "expanded" - shelf_set_name: str = Field("", title="Shelfs set name") - +class AddShelfFileModel(BaseSettingsModel): shelf_set_source_path: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, - title="Shelf Set Path (optional)" + title="Shelf Set Path" ) + +class AddSetAndDefinitionsModel(BaseSettingsModel): + shelf_set_name: str = Field("", title="Shelf Set Name") shelf_definition: list[ShelfDefinitionModel] = Field( default_factory=list, - title="Shelf Definitions" + title="Shelves Definitions" + ) + + +def shelves_enum_options(): + return [ + { + "value": "add_shelf_file", + "label": "Add a .shelf file" + }, + { + "value": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions" + } + ] + + +class ShelvesModel(BaseSettingsModel): + options: str = Field( + title="Options", + description="Switch between shelves manager options", + enum_resolver=shelves_enum_options, + conditionalEnum=True + ) + add_shelf_file: AddShelfFileModel = Field( + title="Add a .shelf file", + default_factory=AddShelfFileModel + ) + add_set_and_definitions: AddSetAndDefinitionsModel = Field( + title="Add Shelf Set Name and Shelves Definitions", + default_factory=AddSetAndDefinitionsModel ) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 01ef12070d..c49a95c357 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.6" +__version__ = "0.2.8" diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a695b85e89..df8412391a 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,6 +1,30 @@ -from pydantic import Field +import json +from pydantic import Field, validator from ayon_server.settings import BaseSettingsModel +from ayon_server.exceptions import BadRequestException + + +class ValidateAttributesModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateAttributes") + attributes: str = Field( + "{}", title="Attributes", widget="textarea") + + @validator("attributes") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The attibutes can't be parsed as json object" + ) + return value class BasicValidateModel(BaseSettingsModel): @@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel): title="Validate Frame Range", section="Validators" ) + ValidateAttributes: ValidateAttributesModel = Field( + default_factory=ValidateAttributesModel, + title="Validate Attributes" + ) DEFAULT_PUBLISH_SETTINGS = { @@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = { "enabled": True, "optional": True, "active": True - } + }, + "ValidateAttributes": { + "enabled": False, + "attributes": "{}" + }, } 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" diff --git a/tests/conftest.py b/tests/conftest.py index 6e82c9917d..a862030fff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,11 @@ def pytest_addoption(parser): help="True - keep test_db, test_openpype, outputted test files" ) + parser.addoption( + "--app_group", action="store", default=None, + help="Keep empty to use default application or explicit" + ) + parser.addoption( "--app_variant", action="store", default=None, help="Keep empty to locate latest installed variant or explicit" @@ -45,6 +50,11 @@ def persist(request): return request.config.getoption("--persist") +@pytest.fixture(scope="module") +def app_group(request): + return request.config.getoption("--app_group") + + @pytest.fixture(scope="module") def app_variant(request): return request.config.getoption("--app_variant") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 277b332e19..e8e338e434 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -248,19 +248,22 @@ class PublishTest(ModuleUnitTest): SETUP_ONLY = False @pytest.fixture(scope="module") - def app_name(self, app_variant): + def app_name(self, app_variant, app_group): """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" from openpype.lib import ApplicationManager app_variant = app_variant or self.APP_VARIANT + app_group = app_group or self.APP_GROUP application_manager = ApplicationManager() if not app_variant: variant = ( application_manager.find_latest_available_variant_for_group( - self.APP_GROUP)) + app_group + ) + ) app_variant = variant.name - yield "{}/{}".format(self.APP_GROUP, app_variant) + yield "{}/{}".format(app_group, app_variant) @pytest.fixture(scope="module") def app_args(self, download_test_data): diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index fffab8ca5d..6f23a19103 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -118,4 +118,28 @@ Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Ca This part of documentation is still work in progress. ::: +## Validators + +Current Openpype integration supports different validators such as Frame Range and Attributes. +Some validators are mandatory while some are optional and user can choose to enable them in the setting. + +**Validate Frame Range**: Optional Validator for checking Frame Range + +**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid + in MaxWrapper Class. +:::note + Users can write the properties' attributes they want to check in dict format in the setting + before validation. The attributes are then to be converted into Maxscript and do a check. + E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine``` + User can put the attributes in the dict format below + ``` + { + "renderer.current":{ + "separateAovFiles" : True + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" + } + } + ``` + ![Validate Attribute Setting](assets/3dsmax_validate_attributes.png) +::: ## ...to be added diff --git a/website/docs/assets/3dsmax_validate_attributes.png b/website/docs/assets/3dsmax_validate_attributes.png new file mode 100644 index 0000000000..5af8236188 Binary files /dev/null and b/website/docs/assets/3dsmax_validate_attributes.png differ