From ea54b0dc2512eeb428b1f3ea702a10a55ac6ccf3 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 9 May 2022 10:28:09 +0200 Subject: [PATCH 01/56] refactor --- openpype/hosts/nuke/startup/menu.py | 31 +++++++++++++++++++ .../defaults/project_settings/nuke.json | 4 +++ .../projects_schema/schema_project_maya.json | 2 +- .../projects_schema/schema_project_nuke.json | 4 +++ ...riptsmenu.json => schema_scriptsmenu.json} | 0 5 files changed, 40 insertions(+), 1 deletion(-) rename openpype/settings/entities/schemas/projects_schema/schemas/{schema_maya_scriptsmenu.json => schema_scriptsmenu.json} (100%) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 9ed43b2110..dee1d9d868 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,5 +1,7 @@ import nuke +import os +import avalon.api from openpype.api import Logger from openpype.pipeline import install_host from openpype.hosts.nuke import api @@ -9,6 +11,7 @@ from openpype.hosts.nuke.api.lib import ( WorkfileSettings, dirmap_file_name_filter ) +from openpype.settings import get_project_settings log = Logger.get_logger(__name__) @@ -28,3 +31,31 @@ nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) nuke.addFilenameFilter(dirmap_file_name_filter) log.info('Automatic syncing of write file knob to script version') + + +def add_scripts_menu(): + try: + from scriptsmenu import launchfornuke + except ImportError: + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + return + + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + config = project_settings["nuke"]["scriptsmenu"]["definition"] + _menu = project_settings["nuke"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return + + # run the launcher for Maya menu + studio_menu = launchfornuke.main(title=_menu.title()) + + # apply configuration + studio_menu.build_from_configuration(studio_menu, config) + +add_scripts_menu() diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 128d440732..a9d284873c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -15,6 +15,10 @@ "destination-path": [] } }, + "scriptsmenu": { + "name": "OpenPype Tools", + "definition": [] + }, "create": { "CreateWriteRender": { "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index cc70516c72..0c7943447b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -49,7 +49,7 @@ }, { "type": "schema", - "name": "schema_maya_scriptsmenu" + "name": "schema_scriptsmenu" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index bc572cbdc8..1ae4efd8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -79,6 +79,10 @@ } ] }, + { + "type": "schema", + "name": "schema_scriptsmenu" + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json similarity index 100% rename from openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json rename to openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json From f7d4cbecfe36826865cb3aba3e33da10fd0aeb71 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 2 May 2022 17:17:29 +0200 Subject: [PATCH 02/56] add the scriptsmenu schema to nuke --- .../projects_schema/schema_project_nuke.json | 4 ++++ .../schemas/schema_nuke_scriptsmenu.json | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 1ae4efd8ea..1fc925557b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -290,6 +290,10 @@ } ] }, + { + "type": "schema", + "name": "schema_nuke_scriptsmenu" + }, { "type": "schema", "name": "schema_nuke_publish", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json new file mode 100644 index 0000000000..e841d6ba77 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json @@ -0,0 +1,22 @@ +{ + "type": "dict", + "collapsible": true, + "key": "scriptsmenu", + "label": "Scripts Menu Definition", + "children": [ + { + "type": "text", + "key": "name", + "label": "Menu Name" + }, + { + "type": "splitter" + }, + { + "type": "raw-json", + "key": "definition", + "label": "Menu definition", + "is_list": true + } + ] +} \ No newline at end of file From c90a949076a1c8d2237fcc6ec118549344a50343 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 12:41:37 +0200 Subject: [PATCH 03/56] call the launchfornuke module from the nuke menu.py to generate custom menu --- openpype/hosts/nuke/startup/menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index dee1d9d868..3a0bfdb28f 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -58,4 +58,5 @@ def add_scripts_menu(): # apply configuration studio_menu.build_from_configuration(studio_menu, config) + add_scripts_menu() From a18c4d3d16e5454f86cd1b4f539c6e3031bd7c9c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 17:27:32 +0200 Subject: [PATCH 04/56] add a function in the nuke menu.py module to also add gizmos --- openpype/hosts/nuke/startup/menu.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 3a0bfdb28f..bb81ee7fac 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,5 +1,6 @@ import nuke import os +import json import avalon.api from openpype.api import Logger @@ -59,4 +60,61 @@ def add_scripts_menu(): studio_menu.build_from_configuration(studio_menu, config) +def add_gizmos(): + """ Build a custom gizmo menu from a yaml description file. + """ + quad_plugin_path = os.environ.get("QUAD_PLUGIN_PATH") + gizmos_folder = os.path.join(quad_plugin_path, 'nuke/gizmos') + icons_folder = os.path.join(quad_plugin_path, 'nuke/icons') + json_file = os.path.join(quad_plugin_path, 'nuke/toolbar.json') + + if os.path.isdir(gizmos_folder): + for p in os.listdir(gizmos_folder): + if os.path.isdir(os.path.join(gizmos_folder, p)): + nuke.pluginAddPath(os.path.join(gizmos_folder, p)) + nuke.pluginAddPath(gizmos_folder) + + with open(json_file, 'rb') as fd: + try: + data = json.loads(fd.read()) + except Exception as e: + print(f"Problem occurs when reading toolbar file: {e}") + return + + if data is None or not isinstance(data, list): + # return early if the json file is empty or not well structured + return + + bar = nuke.menu("Nodes") + menu = bar.addMenu( + "FixStudio", + icon=os.path.join(icons_folder, 'fixstudio.png') + ) + + # populate the menu + for entry in data: + # make fail if the name or command key doesn't exists + name = entry['name'] + + command = entry.get('command', "") + + if command.find('{pipe_path}') > -1: + command = command.format(pipe_path=os.environ['QUAD_PLUGIN_PATH']) + + hotkey = entry.get('hotkey', "") + icon = entry.get('icon', "") + + parent_name = os.path.dirname(name) + + if 'separator' in name: + current = menu.findItem(parent_name) + if current: + current.addSeparator() + else: + menu.addCommand( + name, command=command, shortcut=hotkey, icon=icon, + ) + + +add_gizmos() add_scripts_menu() From f479dd8635b95c1463bab44d79c85d19fc72095e Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 17:54:30 +0200 Subject: [PATCH 05/56] Revert "add a function in the nuke menu.py module to also add gizmos" This reverts commit 2f3bafb2fb8cbf247c354a8904acbc78ae081731. --- openpype/hosts/nuke/startup/menu.py | 58 ----------------------------- 1 file changed, 58 deletions(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index bb81ee7fac..3a0bfdb28f 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,6 +1,5 @@ import nuke import os -import json import avalon.api from openpype.api import Logger @@ -60,61 +59,4 @@ def add_scripts_menu(): studio_menu.build_from_configuration(studio_menu, config) -def add_gizmos(): - """ Build a custom gizmo menu from a yaml description file. - """ - quad_plugin_path = os.environ.get("QUAD_PLUGIN_PATH") - gizmos_folder = os.path.join(quad_plugin_path, 'nuke/gizmos') - icons_folder = os.path.join(quad_plugin_path, 'nuke/icons') - json_file = os.path.join(quad_plugin_path, 'nuke/toolbar.json') - - if os.path.isdir(gizmos_folder): - for p in os.listdir(gizmos_folder): - if os.path.isdir(os.path.join(gizmos_folder, p)): - nuke.pluginAddPath(os.path.join(gizmos_folder, p)) - nuke.pluginAddPath(gizmos_folder) - - with open(json_file, 'rb') as fd: - try: - data = json.loads(fd.read()) - except Exception as e: - print(f"Problem occurs when reading toolbar file: {e}") - return - - if data is None or not isinstance(data, list): - # return early if the json file is empty or not well structured - return - - bar = nuke.menu("Nodes") - menu = bar.addMenu( - "FixStudio", - icon=os.path.join(icons_folder, 'fixstudio.png') - ) - - # populate the menu - for entry in data: - # make fail if the name or command key doesn't exists - name = entry['name'] - - command = entry.get('command', "") - - if command.find('{pipe_path}') > -1: - command = command.format(pipe_path=os.environ['QUAD_PLUGIN_PATH']) - - hotkey = entry.get('hotkey', "") - icon = entry.get('icon', "") - - parent_name = os.path.dirname(name) - - if 'separator' in name: - current = menu.findItem(parent_name) - if current: - current.addSeparator() - else: - menu.addCommand( - name, command=command, shortcut=hotkey, icon=icon, - ) - - -add_gizmos() add_scripts_menu() From 4e694a3f36f740fe9b5488cf75ba421e4b520d97 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:27:51 +0200 Subject: [PATCH 06/56] changes from comments --- .../entities/schemas/projects_schema/schema_project_nuke.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 1fc925557b..1ae4efd8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -290,10 +290,6 @@ } ] }, - { - "type": "schema", - "name": "schema_nuke_scriptsmenu" - }, { "type": "schema", "name": "schema_nuke_publish", From 2eea2522be0ecce65c8fc5876a628b15169f3189 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:31:05 +0200 Subject: [PATCH 07/56] delete schema_nuke_scriptsmenu.json since we use a schema_scriptsmenu.json for both maya and nuke --- .../schemas/schema_nuke_scriptsmenu.json | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json deleted file mode 100644 index e841d6ba77..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "dict", - "collapsible": true, - "key": "scriptsmenu", - "label": "Scripts Menu Definition", - "children": [ - { - "type": "text", - "key": "name", - "label": "Menu Name" - }, - { - "type": "splitter" - }, - { - "type": "raw-json", - "key": "definition", - "label": "Menu definition", - "is_list": true - } - ] -} \ No newline at end of file From f081fe3521158cba9425f86a866fd50fc8106f6f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:58:28 +0200 Subject: [PATCH 08/56] add nuke doc for custom menu --- website/docs/admin_hosts_nuke.md | 14 ++++++++++++++ website/docs/assets/nuke-admin_scriptsmenu.png | Bin 0 -> 21712 bytes website/sidebars.js | 1 + 3 files changed, 15 insertions(+) create mode 100644 website/docs/admin_hosts_nuke.md create mode 100644 website/docs/assets/nuke-admin_scriptsmenu.png diff --git a/website/docs/admin_hosts_nuke.md b/website/docs/admin_hosts_nuke.md new file mode 100644 index 0000000000..46f596a2dc --- /dev/null +++ b/website/docs/admin_hosts_nuke.md @@ -0,0 +1,14 @@ +--- +id: admin_hosts_nuke +title: Nuke +sidebar_label: Nuke +--- + +## Custom Menu +You can add your custom tools menu into Nuke by extending definitions in **Nuke -> Scripts Menu Definition**. +![Custom menu definition](assets/nuke-admin_scriptsmenu.png) + +:::note Work in progress +This is still work in progress. Menu definition will be handled more friendly with widgets and not +raw json. +::: diff --git a/website/docs/assets/nuke-admin_scriptsmenu.png b/website/docs/assets/nuke-admin_scriptsmenu.png new file mode 100644 index 0000000000000000000000000000000000000000..cad2a4411d058a35995863a57bdbcc4cd2793457 GIT binary patch literal 21712 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 literal 0 HcmV?d00001 diff --git a/website/sidebars.js b/website/sidebars.js index 105afc30eb..0e33bed949 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -88,6 +88,7 @@ module.exports = { items: [ "admin_hosts_blender", "admin_hosts_maya", + "admin_hosts_nuke", "admin_hosts_resolve", "admin_hosts_harmony", "admin_hosts_aftereffects", From 8f24a764fd3f5061573931757e0eb6817949e77f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 11 May 2022 18:02:22 +0200 Subject: [PATCH 09/56] remove avalon.api import --- openpype/hosts/nuke/startup/menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 3a0bfdb28f..49edb22a89 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,7 +1,6 @@ import nuke import os -import avalon.api from openpype.api import Logger from openpype.pipeline import install_host from openpype.hosts.nuke import api From 2a94ac1248446d8735bf34ebda62356af5658358 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 12 May 2022 10:10:57 +0200 Subject: [PATCH 10/56] Add doc to default value --- openpype/settings/defaults/project_settings/nuke.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a9d284873c..7f916424ed 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -17,7 +17,15 @@ }, "scriptsmenu": { "name": "OpenPype Tools", - "definition": [] + "definition": [ + { + "type": "action", + "sourcetype": "python", + "title": "OpenPype Docs", + "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", + "tooltip": "Open the OpenPype Nuke user doc page" + } + ] }, "create": { "CreateWriteRender": { From 3484d57a7633af9eea4f380b270f361e19eb075d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 16 May 2022 12:29:34 +0200 Subject: [PATCH 11/56] add support for PxrTexture --- .../maya/plugins/publish/collect_look.py | 27 ++++++++++++------- .../maya/plugins/publish/extract_look.py | 6 +++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index b6a76f1e21..70265a160f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -77,9 +77,14 @@ def node_uses_image_sequence(node): node_path = get_file_node_path(node).lower() # The following tokens imply a sequence - patterns = ["", "", "", "u_v", "", "", "", + "u_v", ""] + try: + extension = cmds.getAttr('%s.useFrameExtension' % node) + except ValueError: + extension = None - return (cmds.getAttr('%s.useFrameExtension' % node) or + return (extension or any(pattern in node_path for pattern in patterns)) @@ -165,7 +170,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) == 'aiImage': + if cmds.nodeType(node) in ['aiImage', 'PxrTexture']: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -326,7 +331,10 @@ class CollectLook(pyblish.api.InstancePlugin): "volumeShader", "displacementShader", "aiSurfaceShader", - "aiVolumeShader"] + "aiVolumeShader", + "rman__surface", + "rman__displacement" + ] if look_sets: materials = [] @@ -376,6 +384,7 @@ class CollectLook(pyblish.api.InstancePlugin): files = cmds.ls(history, type="file", long=True) files.extend(cmds.ls(history, type="aiImage", long=True)) + files.extend(cmds.ls(history, type="PxrTexture", long=True)) files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) self.log.info("Collected file nodes:\n{}".format(files)) @@ -510,23 +519,21 @@ class CollectLook(pyblish.api.InstancePlugin): Returns: dict """ - self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]: + if cmds.nodeType(node) not in [ + "file", "aiImage", "RedshiftNormalMap", "PxrTexture"]: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") + self.log.debug(" - got {}".format(cmds.nodeType(node))) if cmds.nodeType(node) == 'file': - self.log.debug(" - file node") attribute = "{}.fileTextureName".format(node) computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) == 'aiImage': - self.log.debug("aiImage node") + elif cmds.nodeType(node) in ['aiImage', 'PxrTexture']: attribute = "{}.filename".format(node) computed_attribute = attribute elif cmds.nodeType(node) == 'RedshiftNormalMap': - self.log.debug("RedshiftNormalMap node") attribute = "{}.tex0".format(node) computed_attribute = attribute diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 881705b92c..81d7c31ae7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -372,10 +372,12 @@ class ExtractLook(openpype.api.Extractor): if mode == COPY: transfers.append((source, destination)) - self.log.info('copying') + self.log.info('file will be copied {} -> {}'.format( + source, destination)) elif mode == HARDLINK: hardlinks.append((source, destination)) - self.log.info('hardlinking') + self.log.info('file will be hardlinked {} -> {}'.format( + source, destination)) # Store the hashes from hash to destination to include in the # database From 694c3654764034e1a3f84b4e082b58c0899c8c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 16 May 2022 18:26:45 +0200 Subject: [PATCH 12/56] list to sets and var name change --- openpype/hosts/maya/plugins/publish/collect_look.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 70265a160f..9697d0884f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -60,6 +60,7 @@ def get_look_attrs(node): def node_uses_image_sequence(node): + # type: (str) -> bool """Return whether file node uses an image sequence or single image. Determine if a node uses an image sequence or just a single image, @@ -80,11 +81,11 @@ def node_uses_image_sequence(node): patterns = ["", "", "", "u_v", ""] try: - extension = cmds.getAttr('%s.useFrameExtension' % node) + use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node) except ValueError: - extension = None + use_frame_extension = False - return (extension or + return (use_frame_extension or any(pattern in node_path for pattern in patterns)) @@ -170,7 +171,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) in ['aiImage', 'PxrTexture']: + if cmds.nodeType(node) in {'aiImage', 'PxrTexture'}: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -520,8 +521,8 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in [ - "file", "aiImage", "RedshiftNormalMap", "PxrTexture"]: + if cmds.nodeType(node) not in { + "file", "aiImage", "RedshiftNormalMap", "PxrTexture"}: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") From 43f7ad57ce3b3d9c289002f311855b031f1f7f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 18 May 2022 15:08:09 +0200 Subject: [PATCH 13/56] support for other renderman nodes --- .../maya/plugins/publish/collect_look.py | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 9697d0884f..692ecdcde1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -25,6 +25,32 @@ RENDERER_NODE_TYPES = [ SHAPE_ATTRS = set(SHAPE_ATTRS) +DEFAULT_FILE_NODES = frozenset( + ["file"] +) + +ARNOLD_FILE_NODES = frozenset( + ["aiImage"] +) + +REDSHIFT_FILE_NODES = frozenset( + ["RedshiftNormalMap"] +) + +RENDERMAN_FILE_NODES = frozenset( + [ + "PxrBump", + "PxrNormalMap", + # PxrMultiTexture (need to handle multiple filename0 attrs) + "PxrPtexture", + "PxrTexture", + ] +) + +NODES_WITH_FILENAME = frozenset().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES +) + def get_look_attrs(node): """Returns attributes of a node that are important for the look. @@ -171,7 +197,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) in {'aiImage', 'PxrTexture'}: + if cmds.nodeType(node) in NODES_WITH_FILENAME: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -383,10 +409,13 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - files = cmds.ls(history, type="file", long=True) - files.extend(cmds.ls(history, type="aiImage", long=True)) - files.extend(cmds.ls(history, type="PxrTexture", long=True)) - files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) + all_supported_nodes = set().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, + RENDERMAN_FILE_NODES + ) + files = [] + for node_type in all_supported_nodes: + files.extend(cmds.ls(history, type=node_type, long=True)) self.log.info("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found From 5ceba8cad4ee59c662fbf172041821d6f4d5fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 18 May 2022 17:52:59 +0200 Subject: [PATCH 14/56] fix supported nodes --- .../maya/plugins/publish/collect_look.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 692ecdcde1..123e2637cb 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -22,21 +22,16 @@ RENDERER_NODE_TYPES = [ # redshift "RedshiftMeshParameters" ] - SHAPE_ATTRS = set(SHAPE_ATTRS) - DEFAULT_FILE_NODES = frozenset( ["file"] ) - ARNOLD_FILE_NODES = frozenset( ["aiImage"] ) - REDSHIFT_FILE_NODES = frozenset( ["RedshiftNormalMap"] ) - RENDERMAN_FILE_NODES = frozenset( [ "PxrBump", @@ -46,9 +41,14 @@ RENDERMAN_FILE_NODES = frozenset( "PxrTexture", ] ) - +NODES_WITH_FILE = frozenset().union( + DEFAULT_FILE_NODES +) NODES_WITH_FILENAME = frozenset().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES + ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES +) +NODES_WITH_TEX = frozenset().union( + REDSHIFT_FILE_NODES ) @@ -550,20 +550,23 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in { - "file", "aiImage", "RedshiftNormalMap", "PxrTexture"}: + all_supported_nodes = set().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, + RENDERMAN_FILE_NODES + ) + if cmds.nodeType(node) not in all_supported_nodes: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") self.log.debug(" - got {}".format(cmds.nodeType(node))) - if cmds.nodeType(node) == 'file': + if cmds.nodeType(node) in NODES_WITH_FILE: attribute = "{}.fileTextureName".format(node) computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) in ['aiImage', 'PxrTexture']: + elif cmds.nodeType(node) in NODES_WITH_FILENAME: attribute = "{}.filename".format(node) computed_attribute = attribute - elif cmds.nodeType(node) == 'RedshiftNormalMap': + elif cmds.nodeType(node) in NODES_WITH_TEX: attribute = "{}.tex0".format(node) computed_attribute = attribute From d46c919281145a7d07ffb5aca1ef16f7407c6e46 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:46:55 +0200 Subject: [PATCH 15/56] general: calculation of duration should not exclude one frame From ddfee503677c3ad6e653c9346cd742520667d0d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:47:23 +0200 Subject: [PATCH 16/56] hiero: fitting new duration calculation From f4ebcdb27856888794c59c142c43806b863cb61b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 12:22:35 +0200 Subject: [PATCH 17/56] Hiero: small bugs - track name was not equal and was catching similar names too - publish action in timeline submenu was broken - parse_container was returning false data even it should not --- openpype/hosts/hiero/api/lib.py | 7 ++++--- openpype/hosts/hiero/api/pipeline.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 2a4cd03b76..be02c7c793 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -118,7 +118,7 @@ def get_current_track(sequence, name, audio=False): # get track by name track = None for _track in tracks: - if _track.name() in name: + if _track.name() == name: track = _track if not track: @@ -126,6 +126,7 @@ def get_current_track(sequence, name, audio=False): track = hiero.core.VideoTrack(name) else: track = hiero.core.AudioTrack(name) + sequence.addTrack(track) return track @@ -497,7 +498,7 @@ class PyblishSubmission(hiero.exporters.FnSubmission.Submission): from . import publish # Add submission to Hiero module for retrieval in plugins. hiero.submission = self - publish() + publish(hiero.ui.mainWindow()) def add_submission(): @@ -527,7 +528,7 @@ class PublishAction(QtWidgets.QAction): # from getting picked up when not using the "Export" dialog. if hasattr(hiero, "submission"): del hiero.submission - publish() + publish(hiero.ui.mainWindow()) def eventHandler(self, event): # Add the Menu to the right-click menu diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 8025ebff05..9b628ec70b 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -143,6 +143,11 @@ def parse_container(track_item, validate=True): """ # convert tag metadata to normal keys names data = lib.get_track_item_pype_data(track_item) + if ( + not data + or data.get("id") != "pyblish.avalon.container" + ): + return if validate and data and data.get("schema"): schema.validate(data) From c09038984190085b6182cf075455d503692e10ca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:46:34 +0200 Subject: [PATCH 18/56] Hiero: add new `get_timeline_selection` function --- openpype/hosts/hiero/api/__init__.py | 2 ++ openpype/hosts/hiero/api/lib.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index f3c32b268c..fc2d017f04 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -27,6 +27,7 @@ from .lib import ( get_track_items, get_current_project, get_current_sequence, + get_timeline_selection, get_current_track, get_track_item_pype_tag, set_track_item_pype_tag, @@ -80,6 +81,7 @@ __all__ = [ "get_track_items", "get_current_project", "get_current_sequence", + "get_timeline_selection", "get_current_track", "get_track_item_pype_tag", "set_track_item_pype_tag", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index be02c7c793..115a926d84 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -96,6 +96,12 @@ def get_current_sequence(name=None, new=False): return sequence +def get_timeline_selection(): + active_sequence = hiero.ui.activeSequence() + timeline_editor = hiero.ui.getTimelineEditor(active_sequence) + return list(timeline_editor.selection()) + + def get_current_track(sequence, name, audio=False): """ Get current track in context of active project. From 9dd13425c36511873aeefb46f88f721381d91cc6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:47:15 +0200 Subject: [PATCH 19/56] Hiero: removing event slowing down work with timeline --- openpype/hosts/hiero/api/events.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 7fab3edfc8..59fd278a81 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -109,8 +109,9 @@ def register_hiero_events(): # hiero.core.events.registerInterest("kShutdown", shutDown) # hiero.core.events.registerInterest("kStartup", startupCompleted) - hiero.core.events.registerInterest( - ("kSelectionChanged", "kTimeline"), selection_changed_timeline) + # INFO: was disabled because it was slowing down timeline operations + # hiero.core.events.registerInterest( + # ("kSelectionChanged", "kTimeline"), selection_changed_timeline) # workfiles try: From c70c6d99110dce5f7d880cc0d285e2a03dfbf583 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:48:12 +0200 Subject: [PATCH 20/56] Hiero: fixing one frame difference otio clip and media --- openpype/hosts/hiero/api/otio/hiero_export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 64fb81aed4..46e1204324 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -151,7 +151,7 @@ def create_otio_reference(clip): padding = media_source.filenamePadding() file_head = media_source.filenameHead() is_sequence = not media_source.singleFile() - frame_duration = media_source.duration() - 1 + frame_duration = media_source.duration() fps = utils.get_rate(clip) or self.project_fps extension = os.path.splitext(path)[-1] @@ -277,7 +277,7 @@ def create_otio_clip(track_item): # flip if speed is in minus source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() - duration = int(track_item.duration()) + duration = int(track_item.duration()) - 1 fps = utils.get_rate(track_item) or self.project_fps name = track_item.name() From cf6ea949acca41fcd04a0155134252c21816939a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:48:40 +0200 Subject: [PATCH 21/56] global: hierarchy fps should be taken from instance.data --- openpype/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index a96d444be6..8398a2815a 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -62,7 +62,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "frameEnd": instance.data["frameEnd"], "clipIn": instance.data["clipIn"], "clipOut": instance.data["clipOut"], - 'fps': instance.context.data["fps"], + "fps": instance.data["fps"], "resolutionWidth": instance.data["resolutionWidth"], "resolutionHeight": instance.data["resolutionHeight"], "pixelAspect": instance.data["pixelAspect"] From 191444167c025f0f1bf20b6d15dbd480eff1243e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:49:52 +0200 Subject: [PATCH 22/56] Hiero: moving order bit lower under core plugins --- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index b9f58c15f6..c9bfb86810 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -16,7 +16,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.491 def process(self, context): @@ -84,6 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "colorspace": self.get_colorspace(project), "fps": fps } + self.log.debug("__ context_data: {}".format(pformat(context_data))) context.data.update(context_data) self.log.info("Creating instance: {}".format(instance)) From b55bf81d352790c46ba748a7781bc75cda88afb1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:50:50 +0200 Subject: [PATCH 23/56] Hiero: adding timeline selected to precollector --- .../hosts/hiero/plugins/publish/precollect_instances.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 46f0b2440e..1ef7e5f538 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -19,9 +19,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): self.otio_timeline = context.data["otioTimeline"] - + timeline_selection = phiero.get_timeline_selection() selected_timeline_items = phiero.get_track_items( - selected=True, check_tagged=True, check_enabled=True) + selection=timeline_selection, + check_tagged=True, + check_enabled=True + ) # only return enabled track items if not selected_timeline_items: From 68439301dc5f24372ccd69202bcea3a3040ce733 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:51:21 +0200 Subject: [PATCH 24/56] Hiero: one frame diff fix, with code improvements --- .../hosts/hiero/plugins/publish/precollect_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 1ef7e5f538..e54d050f0d 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -295,9 +295,9 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for otio_clip in self.otio_timeline.each_clip(): track_name = otio_clip.parent().name parent_range = otio_clip.range_in_parent() - if ti_track_name not in track_name: + if ti_track_name != track_name: continue - if otio_clip.name not in track_item.name(): + if otio_clip.name != track_item.name(): continue self.log.debug("__ parent_range: {}".format(parent_range)) self.log.debug("__ timeline_range: {}".format(timeline_range)) @@ -317,7 +317,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): speed = track_item.playbackSpeed() timeline = phiero.get_current_sequence() frame_start = int(track_item.timelineIn()) - frame_duration = int(track_item.sourceDuration() / speed) + frame_duration = int((track_item.duration() - 1) / speed) fps = timeline.framerate().toFloat() return hiero_export.create_otio_time_range( From 2798469e36c3f915692cb0cfdea5e0cb5f16900a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:54:17 +0200 Subject: [PATCH 25/56] Hiero: refactory of get_track_items with better validation --- openpype/hosts/hiero/api/lib.py | 140 ++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 115a926d84..15142daa09 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1,7 +1,10 @@ """ Host specific functions where host api is connected """ + +import contextlib import os +from pprint import pformat import re import sys import platform @@ -139,7 +142,7 @@ def get_current_track(sequence, name, audio=False): def get_track_items( - selected=False, + selection=False, sequence_name=None, track_item_name=None, track_name=None, @@ -150,7 +153,7 @@ def get_track_items( """Get all available current timeline track items. Attribute: - selected (bool)[optional]: return only selected items on timeline + selection (list)[optional]: list of selected track items sequence_name (str)[optional]: return only clips from input sequence track_item_name (str)[optional]: return only item with input name track_name (str)[optional]: return only items from track name @@ -162,32 +165,33 @@ def get_track_items( Return: list or hiero.core.TrackItem: list of track items or single track item """ - return_list = list() - track_items = list() + track_type = track_type or "video" + selection = selection or [] + return_list = [] # get selected track items or all in active sequence - if selected: - try: - selected_items = list(hiero.selection) - for item in selected_items: - if track_name and track_name in item.parent().name(): - # filter only items fitting input track name - track_items.append(item) - elif not track_name: - # or add all if no track_name was defined - track_items.append(item) - except AttributeError: - pass + if selection: + with contextlib.suppress(AttributeError): + for track_item in selection: + log.info("___ track_item: {}".format(track_item)) + # make sure only trackitems are selected + if not isinstance(track_item, hiero.core.TrackItem): + continue + + if _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + log.info("___ valid trackitem: {}".format(track_item)) + return_list.append(track_item) - # check if any collected track items are - # `core.Hiero.Python.TrackItem` instance - if track_items: - any_track_item = track_items[0] - if not isinstance(any_track_item, hiero.core.TrackItem): - selected_items = [] # collect all available active sequence track items - if not track_items: + if not return_list: sequence = get_current_sequence(name=sequence_name) # get all available tracks from sequence tracks = list(sequence.audioTracks()) + list(sequence.videoTracks()) @@ -198,42 +202,76 @@ def get_track_items( if check_enabled and not track.isEnabled(): continue # and all items in track - for item in track.items(): - if check_tagged and not item.tags(): + for track_item in track.items(): + # make sure no subtrackitem is also track items + if not isinstance(track_item, hiero.core.TrackItem): continue - # check if track item is enabled - if check_enabled: - if not item.isEnabled(): - continue - if track_item_name: - if track_item_name in item.name(): - return item - # make sure only track items with correct track names are added - if track_name and track_name in track.name(): - # filter out only defined track_name items - track_items.append(item) - elif not track_name: - # or add all if no track_name is defined - track_items.append(item) + if not _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + return_list.append(track_item) - # filter out only track items with defined track_type - for track_item in track_items: - if track_type and track_type == "video" and isinstance( + return return_list + + +def _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged +): + def _validate_correct_name_track_item(): + if track_item_name and track_item_name in track_item.name(): + return True + elif not track_item_name: + return True + + def _validate_tagged_track_item(): + if check_tagged and track_item.tags(): + return True + elif not check_tagged: + return True + + def _validate_enabled_track_item(): + if check_enabled and track_item.isEnabled(): + return True + elif not check_enabled: + return True + + def _validate_parent_track_item(): + if track_name and track_name in track_item.parent().name(): + # filter only items fitting input track name + return True + elif not track_name: + # or add all if no track_name was defined + return True + + def _validate_type_track_item(): + if track_type == "video" and isinstance( track_item.parent(), hiero.core.VideoTrack): # only video track items are allowed - return_list.append(track_item) - elif track_type and track_type == "audio" and isinstance( + return True + elif track_type == "audio" and isinstance( track_item.parent(), hiero.core.AudioTrack): # only audio track items are allowed - return_list.append(track_item) - elif not track_type: - # add all if no track_type is defined - return_list.append(track_item) + return True - # return output list but make sure all items are TrackItems - return [_i for _i in return_list - if type(_i) == hiero.core.TrackItem] + # check if track item is enabled + return all([ + _validate_enabled_track_item(), + _validate_type_track_item(), + _validate_tagged_track_item(), + _validate_parent_track_item(), + _validate_correct_name_track_item() + ]) def get_track_item_pype_tag(track_item): From 13599837176687e49a57762414aad97e56e9125a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:55:16 +0200 Subject: [PATCH 26/56] Hiero: fixing events --- openpype/hosts/hiero/api/lib.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 15142daa09..d3e6441705 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1027,7 +1027,7 @@ def sync_clip_name_to_data_asset(track_items_list): print("asset was changed in clip: {}".format(ti_name)) -def check_inventory_versions(): +def check_inventory_versions(track_items=None): """ Actual version color idetifier of Loaded containers @@ -1038,14 +1038,15 @@ def check_inventory_versions(): """ from . import parse_container + track_item = track_items or get_track_items() # presets clip_color_last = "green" clip_color = "red" # get all track items from current timeline - for track_item in get_track_items(): + for track_item in track_item: container = parse_container(track_item) - + log.info("___> container: {}".format(pformat(container))) if container: # get representation from io representation = legacy_io.find_one({ @@ -1083,29 +1084,31 @@ def selection_changed_timeline(event): timeline_editor = event.sender selection = timeline_editor.selection() - selection = [ti for ti in selection - if isinstance(ti, hiero.core.TrackItem)] + track_items = get_track_items( + selection=selection, + track_type="video", + check_enabled=True, + check_locked=True, + check_tagged=True + ) # run checking function - sync_clip_name_to_data_asset(selection) - - # also mark old versions of loaded containers - check_inventory_versions() + sync_clip_name_to_data_asset(track_items) def before_project_save(event): track_items = get_track_items( - selected=False, track_type="video", check_enabled=True, check_locked=True, - check_tagged=True) + check_tagged=True + ) # run checking function sync_clip_name_to_data_asset(track_items) # also mark old versions of loaded containers - check_inventory_versions() + check_inventory_versions(track_items) def get_main_window(): From 733ad125b5798d01fe9dac9d3d24e6256d614386 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:56:15 +0200 Subject: [PATCH 27/56] Hiero: one frame diff during loading --- openpype/hosts/hiero/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 54e66bf99a..35e9d54810 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -500,7 +500,7 @@ class ClipLoader: track_item.setSource(clip) track_item.setSourceIn(self.handle_start) track_item.setTimelineIn(self.timeline_in) - track_item.setSourceOut(self.media_duration - self.handle_end) + track_item.setSourceOut((self.media_duration + 1) - self.handle_end) track_item.setTimelineOut(self.timeline_out) track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) From 91af28612e367d412e70a20ece03481ed4b976e9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 11:21:16 +0200 Subject: [PATCH 28/56] Hiero: poping found clip --- openpype/hosts/hiero/plugins/load/load_clip.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index da4326c8c1..a3365253b3 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -3,10 +3,6 @@ from openpype.pipeline import ( get_representation_path, ) import openpype.hosts.hiero.api as phiero -# from openpype.hosts.hiero.api import plugin, lib -# reload(lib) -# reload(plugin) -# reload(phiero) class LoadClip(phiero.SequenceLoader): @@ -106,7 +102,7 @@ class LoadClip(phiero.SequenceLoader): name = container['name'] namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() version = legacy_io.find_one({ "type": "version", "_id": representation["parent"] @@ -157,7 +153,7 @@ class LoadClip(phiero.SequenceLoader): # load clip to timeline and get main variables namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() track = track_item.parent() # remove track item from track From 053287bc616f1370602383f2aa4891fe80358599 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:00:01 +0200 Subject: [PATCH 29/56] Hiero: better logging and improving code --- openpype/hosts/hiero/api/plugin.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 35e9d54810..174a25102f 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -1,4 +1,5 @@ import os +from pprint import pformat import re from copy import deepcopy @@ -400,7 +401,8 @@ class ClipLoader: # inject asset data to representation dict self._get_asset_data() - log.debug("__init__ self.data: `{}`".format(self.data)) + log.info("__init__ self.data: `{}`".format(pformat(self.data))) + log.info("__init__ options: `{}`".format(pformat(options))) # add active components to class if self.new_sequence: @@ -482,7 +484,9 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = openpype.get_asset(asset_name)["data"] + asset_doc = openpype.get_asset(asset_name) + log.debug("__ asset_doc: {}".format(pformat(asset_doc))) + self.data["assetData"] = asset_doc["data"] def _make_track_item(self, source_bin_item, audio=False): """ Create track item with """ @@ -527,7 +531,8 @@ class ClipLoader: if self.sequencial_load: last_track_item = lib.get_track_items( sequence_name=self.active_sequence.name(), - track_name=self.active_track.name()) + track_name=self.active_track.name() + ) if len(last_track_item) == 0: last_timeline_out = 0 else: @@ -541,6 +546,8 @@ class ClipLoader: self.timeline_in = int(self.data["assetData"]["clipIn"]) self.timeline_out = int(self.data["assetData"]["clipOut"]) + log.debug("__ self.timeline_in: {}".format(self.timeline_in)) + log.debug("__ self.timeline_out: {}".format(self.timeline_out)) # check if slate is included # either in version data families or by calculating frame diff slate_on = next( @@ -553,6 +560,7 @@ class ClipLoader: (self.timeline_out - self.timeline_in + 1) + self.handle_start + self.handle_end) < self.media_duration) + log.debug("__ slate_on: {}".format(slate_on)) # if slate is on then remove the slate frame from beginning if slate_on: self.media_duration -= 1 @@ -599,8 +607,8 @@ class Creator(LegacyCreator): rename_index = None def __init__(self, *args, **kwargs): - import openpype.hosts.hiero.api as phiero super(Creator, self).__init__(*args, **kwargs) + import openpype.hosts.hiero.api as phiero self.presets = openpype.get_current_project_settings()[ "hiero"]["create"].get(self.__class__.__name__, {}) @@ -609,7 +617,10 @@ class Creator(LegacyCreator): self.sequence = phiero.get_current_sequence() if (self.options or {}).get("useSelection"): - self.selected = phiero.get_track_items(selected=True) + timeline_selection = phiero.get_timeline_selection() + self.selected = phiero.get_track_items( + selection=timeline_selection + ) else: self.selected = phiero.get_track_items() @@ -716,6 +727,10 @@ class PublishClip: else: self.tag_data.update({"reviewTrack": None}) + log.debug("___ self.tag_data: {}".format( + pformat(self.tag_data) + )) + # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) From d27448e7e36d751f16b458bde328065706378092 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:42:29 +0200 Subject: [PATCH 30/56] Hiero: handles and slate detection - handles should be int - slate only if in families on version --- openpype/hosts/hiero/api/plugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 174a25102f..e3ec6f3cf1 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -524,9 +524,12 @@ class ClipLoader: self.handle_start = self.data["versionData"].get("handleStart") self.handle_end = self.data["versionData"].get("handleEnd") if self.handle_start is None: - self.handle_start = int(self.data["assetData"]["handleStart"]) + self.handle_start = self.data["assetData"]["handleStart"] if self.handle_end is None: - self.handle_end = int(self.data["assetData"]["handleEnd"]) + self.handle_end = self.data["assetData"]["handleEnd"] + + self.handle_start = int(self.handle_start) + self.handle_end = int(self.handle_end) if self.sequencial_load: last_track_item = lib.get_track_items( @@ -556,9 +559,7 @@ class ClipLoader: if "slate" in f), # if nothing was found then use default None # so other bool could be used - None) or bool(int( - (self.timeline_out - self.timeline_in + 1) - + self.handle_start + self.handle_end) < self.media_duration) + None) log.debug("__ slate_on: {}".format(slate_on)) # if slate is on then remove the slate frame from beginning From cac79e69031f5c9a0e082ad0b4117ce3b5339a78 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:42:53 +0200 Subject: [PATCH 31/56] Hiero: fix timewarp lookup to be list --- openpype/lib/editorial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 1ee21deedc..5fe498bf6a 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -218,6 +218,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "name": name } tw_node.update(metadata) + tw_node["lookup"] = list(lookup) # get first and last frame offsets offset_in += lookup[0] From ffe1bff6599e0de5c0e07b30b4878714bd26e91a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:06:11 +0200 Subject: [PATCH 32/56] Hiero: fix by reversing validation --- openpype/hosts/hiero/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index d3e6441705..4dc8d26c2a 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -207,7 +207,7 @@ def get_track_items( if not isinstance(track_item, hiero.core.TrackItem): continue - if not _validate_all_atrributes( + if _validate_all_atrributes( track_item, track_item_name, track_name, @@ -1046,7 +1046,6 @@ def check_inventory_versions(track_items=None): # get all track items from current timeline for track_item in track_item: container = parse_container(track_item) - log.info("___> container: {}".format(pformat(container))) if container: # get representation from io representation = legacy_io.find_one({ From 15726cfef92bb465a794d7ccd3acb8cdc3e828bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:06:42 +0200 Subject: [PATCH 33/56] Hiero: simplify code for slate detection --- openpype/hosts/hiero/api/plugin.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index e3ec6f3cf1..8c61baa04b 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -551,17 +551,11 @@ class ClipLoader: log.debug("__ self.timeline_in: {}".format(self.timeline_in)) log.debug("__ self.timeline_out: {}".format(self.timeline_out)) - # check if slate is included - # either in version data families or by calculating frame diff - slate_on = next( - # check iterate if slate is in families - (f for f in self.context["version"]["data"]["families"] - if "slate" in f), - # if nothing was found then use default None - # so other bool could be used - None) + # check if slate is included + slate_on = "slate" in self.context["version"]["data"]["families"] log.debug("__ slate_on: {}".format(slate_on)) + # if slate is on then remove the slate frame from beginning if slate_on: self.media_duration -= 1 @@ -581,7 +575,7 @@ class ClipLoader: # there were some cases were hiero was not creating it source_bin_item = None for item in self.active_bin.items(): - if self.data["clip_name"] in item.name(): + if self.data["clip_name"] == item.name(): source_bin_item = item if not source_bin_item: log.warning("Problem with created Source clip: `{}`".format( From 38d4c3fa67effbc25847ee8706c7a1714cfa54a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:17:14 +0200 Subject: [PATCH 34/56] Hiero: lib code refactory --- openpype/hosts/hiero/api/lib.py | 37 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 4dc8d26c2a..758df43968 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -4,7 +4,6 @@ Host specific functions where host api is connected import contextlib import os -from pprint import pformat import re import sys import platform @@ -92,7 +91,7 @@ def get_current_sequence(name=None, new=False): if not sequence: # if nothing found create new with input name sequence = get_current_sequence(name, True) - elif not name and not new: + else: # if name is none and new is False then return current open sequence sequence = hiero.ui.activeSequence() @@ -189,7 +188,6 @@ def get_track_items( log.info("___ valid trackitem: {}".format(track_item)) return_list.append(track_item) - # collect all available active sequence track items if not return_list: sequence = get_current_sequence(name=sequence_name) @@ -311,7 +309,7 @@ def set_track_item_pype_tag(track_item, data=None): "editable": "0", "note": "OpenPype data container", "icon": "openpype_icon.png", - "metadata": {k: v for k, v in data.items()} + "metadata": dict(data.items()) } # get available pype tag if any _tag = get_track_item_pype_tag(track_item) @@ -369,7 +367,7 @@ def get_track_item_pype_data(track_item): log.warning(msg) value = v - data.update({key: value}) + data[key] = value return data @@ -938,32 +936,32 @@ def apply_colorspace_clips(): def is_overlapping(ti_test, ti_original, strict=False): - covering_exp = bool( + covering_exp = ( (ti_test.timelineIn() <= ti_original.timelineIn()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - inside_exp = bool( + inside_exp = ( (ti_test.timelineIn() >= ti_original.timelineIn()) and (ti_test.timelineOut() <= ti_original.timelineOut()) ) - overlaying_right_exp = bool( + overlaying_right_exp = ( (ti_test.timelineIn() < ti_original.timelineOut()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - overlaying_left_exp = bool( + overlaying_left_exp = ( (ti_test.timelineOut() > ti_original.timelineIn()) and (ti_test.timelineIn() <= ti_original.timelineIn()) ) - if not strict: + if strict: + return covering_exp + else: return any(( covering_exp, inside_exp, overlaying_right_exp, overlaying_left_exp )) - else: - return covering_exp def get_sequence_pattern_and_padding(file): @@ -981,17 +979,12 @@ def get_sequence_pattern_and_padding(file): """ foundall = re.findall( r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) - if foundall: - found = sorted(list(set(foundall[0])))[-1] - - if "%" in found: - padding = int(re.findall(r"\d+", found)[-1]) - else: - padding = len(found) - - return found, padding - else: + if not foundall: return None, None + found = sorted(list(set(foundall[0])))[-1] + + padding = int(re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + return found, padding def sync_clip_name_to_data_asset(track_items_list): From 90948931b08dba9e4698ffc45f6a3be307996853 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:49:08 +0200 Subject: [PATCH 35/56] Hiero: removing old code From 61be6857603c8061283bcbed938f07c5bd525eee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 12:35:46 +0200 Subject: [PATCH 36/56] replaced persistent editors with added ability of any edit trigger --- .../tools/project_manager/project_manager/view.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 25174232bc..6f5b9dc3f7 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -139,6 +139,7 @@ class HierarchyView(QtWidgets.QTreeView): self.setAlternatingRowColors(True) self.setSelectionMode(HierarchyView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setEditTriggers(HierarchyView.AllEditTriggers) column_delegates = {} column_key_to_index = {} @@ -301,16 +302,6 @@ class HierarchyView(QtWidgets.QTreeView): def rowsInserted(self, parent_index, start, end): super(HierarchyView, self).rowsInserted(parent_index, start, end) - for row in range(start, end + 1): - for key, column in self._column_key_to_index.items(): - if key not in self.persistent_columns: - continue - col_index = self._source_model.index(row, column, parent_index) - if bool( - self._source_model.flags(col_index) - & QtCore.Qt.ItemIsEditable - ): - self.openPersistentEditor(col_index) # Expand parent on insert if not self.isExpanded(parent_index): From 7c3fdcc633f80b73084b0fbf4ba6f51d4c8c8a71 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 12:59:38 +0200 Subject: [PATCH 37/56] Hiero: tag should use deepcopy for metadata --- openpype/hosts/hiero/api/lib.py | 7 ++++--- openpype/hosts/hiero/api/tags.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 758df43968..5b2f6c814d 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -3,6 +3,7 @@ Host specific functions where host api is connected """ import contextlib +from copy import deepcopy import os import re import sys @@ -288,7 +289,7 @@ def get_track_item_pype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if tag.name() in self.pype_tag_name: + if tag.name() == self.pype_tag_name: return tag @@ -344,9 +345,9 @@ def get_track_item_pype_data(track_item): return None # get tag metadata attribute - tag_data = tag.metadata() + tag_data = deepcopy(dict(tag.metadata())) # convert tag metadata to normal keys names and values to correct types - for k, v in dict(tag_data).items(): + for k, v in tag_data.items(): key = k.replace("tag.", "") try: diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index 8877b92b9d..8c6ff2a77b 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -86,7 +86,7 @@ def update_tag(tag, data): # due to hiero bug we have to make sure keys which are not existent in # data are cleared of value by `None` - for _mk in mtd.keys(): + for _mk in mtd.dict().keys(): if _mk.replace("tag.", "") not in data_mtd.keys(): mtd.setValue(_mk, str(None)) From be9f6cf5c5ddd7e833f5dd5c31e690706767e493 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 15:08:56 +0200 Subject: [PATCH 38/56] Hiero: frame difference issue --- openpype/hosts/hiero/api/otio/hiero_export.py | 2 +- openpype/hosts/hiero/api/plugin.py | 2 +- openpype/lib/editorial.py | 2 +- openpype/plugins/publish/collect_otio_frame_ranges.py | 8 ++++---- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 46e1204324..1e4088d9c0 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -277,7 +277,7 @@ def create_otio_clip(track_item): # flip if speed is in minus source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() - duration = int(track_item.duration()) - 1 + duration = int(track_item.duration()) fps = utils.get_rate(track_item) or self.project_fps name = track_item.name() diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 8c61baa04b..add416d04e 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -504,7 +504,7 @@ class ClipLoader: track_item.setSource(clip) track_item.setSourceIn(self.handle_start) track_item.setTimelineIn(self.timeline_in) - track_item.setSourceOut((self.media_duration + 1) - self.handle_end) + track_item.setSourceOut((self.media_duration) - self.handle_end) track_item.setTimelineOut(self.timeline_out) track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 5fe498bf6a..2c877b9d0d 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -255,7 +255,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in + source_in + offset_in) media_out_trimmed = ( media_in + source_in + ( - (source_range.duration.value * abs( + ((source_range.duration.value - 1) * abs( time_scalar)) + offset_out)) # calculate available handles diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index ee7b7957ad..8eaf9d6f29 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -55,13 +55,13 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): "frameStart": frame_start, "frameEnd": frame_end, "clipIn": tl_start, - "clipOut": tl_end, + "clipOut": tl_end - 1, "clipInH": tl_start_h, - "clipOutH": tl_end_h, + "clipOutH": tl_end_h - 1, "sourceStart": src_starting_from + src_start, - "sourceEnd": src_starting_from + src_end, + "sourceEnd": src_starting_from + src_end - 1, "sourceStartH": src_starting_from + src_start_h, - "sourceEndH": src_starting_from + src_end_h, + "sourceEndH": src_starting_from + src_end_h - 1, } instance.data.update(data) self.log.debug( diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 7c11462ef0..b89a076a44 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -66,7 +66,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # create trimmed otio time range trimmed_media_range_h = editorial.range_from_frames( - a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), + a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1, media_fps ) trimmed_duration = trimmed_media_range_h.duration.value From 4c4c824d8084247c2b0ed550579849fe9b503247 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 May 2022 15:22:23 +0200 Subject: [PATCH 39/56] :recycle: refactored work with attributes --- .../maya/plugins/publish/collect_look.py | 153 +++++++++--------- 1 file changed, 81 insertions(+), 72 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 123e2637cb..fb2ce04cad 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -23,33 +23,43 @@ RENDERER_NODE_TYPES = [ "RedshiftMeshParameters" ] SHAPE_ATTRS = set(SHAPE_ATTRS) -DEFAULT_FILE_NODES = frozenset( - ["file"] -) -ARNOLD_FILE_NODES = frozenset( - ["aiImage"] -) -REDSHIFT_FILE_NODES = frozenset( - ["RedshiftNormalMap"] -) -RENDERMAN_FILE_NODES = frozenset( - [ - "PxrBump", - "PxrNormalMap", - # PxrMultiTexture (need to handle multiple filename0 attrs) - "PxrPtexture", - "PxrTexture", - ] -) -NODES_WITH_FILE = frozenset().union( - DEFAULT_FILE_NODES -) -NODES_WITH_FILENAME = frozenset().union( - ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES -) -NODES_WITH_TEX = frozenset().union( - REDSHIFT_FILE_NODES -) + + +def get_pxr_multitexture_file_attrs(node): + attrs = [] + for i in range(9): + if cmds.attributeQuery("filename{}".format(i), node): + file = cmds.getAttr("{}.filename{}".format(node, i)) + if file: + attrs.append("filename{}".format(i)) + return attrs + + +FILE_NODES = { + "file": "fileTextureName", + + "aiImage": "filename", + + "RedshiftNormalMap": "text0", + + "PxrBump": "filename", + "PxrNormalMap": "filename", + "PxrMultiTexture": get_pxr_multitexture_file_attrs, + "PxrPtexture": "filename", + "PxrTexture": "filename" +} + + +def get_attributes(dictionary, attr): + # type: (dict, str) -> list + if callable(dictionary[attr]): + val = dictionary[attr]() + else: + val = dictionary.get(attr, []) + + if not isinstance(val, list): + return [val] + return val def get_look_attrs(node): @@ -77,15 +87,13 @@ def get_look_attrs(node): if cmds.objectType(node, isAType="shape"): attrs = cmds.listAttr(node, changedSinceFileOpen=True) or [] for attr in attrs: - if attr in SHAPE_ATTRS: + if attr in SHAPE_ATTRS or \ + attr not in SHAPE_ATTRS and attr.startswith('ai'): result.append(attr) - elif attr.startswith('ai'): - result.append(attr) - return result -def node_uses_image_sequence(node): +def node_uses_image_sequence(node, node_path): # type: (str) -> bool """Return whether file node uses an image sequence or single image. @@ -101,8 +109,6 @@ def node_uses_image_sequence(node): """ # useFrameExtension indicates an explicit image sequence - node_path = get_file_node_path(node).lower() - # The following tokens imply a sequence patterns = ["", "", "", "u_v", ""] @@ -169,14 +175,15 @@ def seq_to_glob(path): return path -def get_file_node_path(node): +def get_file_node_paths(node): + # type: (str) -> list """Get the file path used by a Maya file node. Args: node (str): Name of the Maya file node Returns: - str: the file path in use + list: the file paths in use """ # if the path appears to be sequence, use computedFileTextureNamePattern, @@ -195,15 +202,19 @@ def get_file_node_path(node): ""] lower = texture_pattern.lower() if any(pattern in lower for pattern in patterns): - return texture_pattern + return [texture_pattern] - if cmds.nodeType(node) in NODES_WITH_FILENAME: - return cmds.getAttr('{0}.filename'.format(node)) - if cmds.nodeType(node) == 'RedshiftNormalMap': - return cmds.getAttr('{}.tex0'.format(node)) + try: + file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node)) + except AttributeError: + file_attributes = "fileTextureName" - # otherwise use fileTextureName - return cmds.getAttr('{0}.fileTextureName'.format(node)) + files = [] + for file_attr in file_attributes: + if cmds.attributeQuery(file_attr, node=node, exists=True): + files.append(cmds.getAttr("{}.{}".format(node, file_attr))) + + return files def get_file_node_files(node): @@ -217,16 +228,21 @@ def get_file_node_files(node): list: List of full file paths. """ + paths = get_file_node_paths(node) + sequences = [] + replaces = [] + for index, path in enumerate(paths): + if node_uses_image_sequence(node, path): + glob_pattern = seq_to_glob(path) + sequences.extend(glob.glob(glob_pattern)) + replaces.append(index) - path = get_file_node_path(node) - path = cmds.workspace(expandName=path) - if node_uses_image_sequence(node): - glob_pattern = seq_to_glob(path) - return glob.glob(glob_pattern) - elif os.path.exists(path): - return [path] - else: - return [] + for index in replaces: + paths.pop(index) + + paths.extend(sequences) + + return [p for p in paths if os.path.exists(p)] class CollectLook(pyblish.api.InstancePlugin): @@ -270,13 +286,13 @@ class CollectLook(pyblish.api.InstancePlugin): "for %s" % instance.data['name']) # Discover related object sets - self.log.info("Gathering sets..") + self.log.info("Gathering sets ...") sets = self.collect_sets(instance) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) - self.log.info("Gathering set relations..") + self.log.info("Gathering set relations ...") # Ensure iteration happen in a list so we can remove keys from the # dict within the loop @@ -409,10 +425,7 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - all_supported_nodes = set().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, - RENDERMAN_FILE_NODES - ) + all_supported_nodes = FILE_NODES.keys() files = [] for node_type in all_supported_nodes: files.extend(cmds.ls(history, type=node_type, long=True)) @@ -550,27 +563,23 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - all_supported_nodes = set().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, - RENDERMAN_FILE_NODES - ) + all_supported_nodes = FILE_NODES.keys() if cmds.nodeType(node) not in all_supported_nodes: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") self.log.debug(" - got {}".format(cmds.nodeType(node))) - if cmds.nodeType(node) in NODES_WITH_FILE: - attribute = "{}.fileTextureName".format(node) - computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) in NODES_WITH_FILENAME: - attribute = "{}.filename".format(node) - computed_attribute = attribute - elif cmds.nodeType(node) in NODES_WITH_TEX: - attribute = "{}.tex0".format(node) - computed_attribute = attribute - source = cmds.getAttr(attribute) + attribute = FILE_NODES.get(cmds.nodeType(node)) + source = cmds.getAttr("{}.{}".format( + node, + attribute + )) + computed_attribute = "{}.{}".format(node, attribute) + if attribute == "fileTextureName": + computed_attribute = node + ".computedFileTextureNamePattern" + self.log.info(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: From f35079aaed7eb74ccca74645c800d20d8f225801 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 16:18:39 +0200 Subject: [PATCH 40/56] global: remove exclude family for `clip` --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf13a4050e..353314fff2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -113,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" From 87182f5a3e66cbae791f49c2f8cc0692f8dff3bf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 17:00:48 +0200 Subject: [PATCH 41/56] global: otio duration is one frame longer --- openpype/plugins/publish/extract_otio_trimming_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 30b57e2c69..e8e2994f36 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -80,7 +80,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): video_path = input_file_path frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate - frame_duration = (otio_range.duration.value + 1) + frame_duration = otio_range.duration.value - 1 sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) From 5d6ce5592dfcb02444e9d7f86e9feab6eaebf53d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 May 2022 17:47:54 +0200 Subject: [PATCH 42/56] Hiero: rolled back py3 compatible code make it py27 working --- openpype/hosts/hiero/api/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 5b2f6c814d..ae0aef9e9b 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -171,7 +171,7 @@ def get_track_items( # get selected track items or all in active sequence if selection: - with contextlib.suppress(AttributeError): + try: for track_item in selection: log.info("___ track_item: {}".format(track_item)) # make sure only trackitems are selected @@ -188,6 +188,8 @@ def get_track_items( ): log.info("___ valid trackitem: {}".format(track_item)) return_list.append(track_item) + except AttributeError: + pass # collect all available active sequence track items if not return_list: From 2e5acf6221e5063517870c93046979e79b243e49 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 May 2022 17:50:09 +0200 Subject: [PATCH 43/56] hound --- openpype/hosts/hiero/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index ae0aef9e9b..d19cefd2da 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -2,7 +2,6 @@ Host specific functions where host api is connected """ -import contextlib from copy import deepcopy import os import re @@ -986,7 +985,8 @@ def get_sequence_pattern_and_padding(file): return None, None found = sorted(list(set(foundall[0])))[-1] - padding = int(re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + padding = int( + re.findall(r"\d+", found)[-1]) if "%" in found else len(found) return found, padding From d4b6d6552caf6ebaef179a1f1519731746dad28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 23 May 2022 18:34:03 +0200 Subject: [PATCH 44/56] :bug: get resolution from overrides --- .../hosts/maya/plugins/publish/collect_render.py | 12 +++++++++--- .../hosts/maya/plugins/publish/collect_vrayscene.py | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e66983780e..b19572ab37 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -339,9 +339,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "source": filepath, "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer + ), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index afdb570cbc..6a0c2332fe 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -124,9 +124,15 @@ class CollectVrayScene(pyblish.api.InstancePlugin): # Add source to allow tracing back to the scene from # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer + ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( "vraySceneMultipleFiles") From f57c22e4203b9d6441a05edabeda19742e4eda91 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 24 May 2022 22:20:00 +0200 Subject: [PATCH 45/56] :bug: fix layer names --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 +++--- openpype/hosts/maya/plugins/publish/collect_vrayscene.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b19572ab37..fbd2e81279 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -340,13 +340,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer + "defaultResolution.height", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer + "defaultResolution.width", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer + "defaultResolution.pixelAspect", layer=layer_name ), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index 6a0c2332fe..0bae9656f3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -125,13 +125,13 @@ class CollectVrayScene(pyblish.api.InstancePlugin): # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer + "defaultResolution.height", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer + "defaultResolution.width", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer + "defaultResolution.pixelAspect", layer=layer_name ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( From ac79f31a279fcbd50d04a571b2b3eb8270b761d7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 24 May 2022 22:39:50 +0200 Subject: [PATCH 46/56] :bug: filter out display types without file output don't process renderman display type that are not producing any file output (like `d_it`) --- openpype/hosts/maya/api/lib_renderproducts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index ff04fa7aa2..2d3bda5245 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1093,6 +1093,11 @@ class RenderProductsRenderman(ARenderProducts): if not enabled: continue + # Skip display types not producing any file output. + # Is there a better way to do it? + if not display_types.get(display["driverNode"]["type"]): + continue + aov_name = name if aov_name == "rmanDefaultDisplay": aov_name = "beauty" From c9b0bb0e54cbd3390f06ee8ea3135ffd5e9413e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 May 2022 13:32:45 +0200 Subject: [PATCH 47/56] make sure chunk size is at least 1 --- openpype/modules/ftrack/lib/avalon_sync.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 124787e467..e4ba651bfd 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -143,14 +143,17 @@ def create_chunks(iterable, chunk_size=None): list: Chunked items. """ chunks = [] - if not iterable: - return chunks tupled_iterable = tuple(iterable) + if not tupled_iterable: + return chunks iterable_size = len(tupled_iterable) if chunk_size is None: chunk_size = 200 + if chunk_size < 1: + chunk_size = 1 + for idx in range(0, iterable_size, chunk_size): chunks.append(tupled_iterable[idx:idx + chunk_size]) return chunks From 93c4e3403aa0a26717ab47815940cb269e279c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 25 May 2022 14:06:25 +0200 Subject: [PATCH 48/56] :bug: fix node attribute name --- .../hosts/maya/plugins/publish/collect_look.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index fb2ce04cad..d295492f9a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -616,11 +616,15 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.info(" - color space: {}".format(color_space)) # Define the resource - return {"node": node, - "attribute": attribute, - "source": source, # required for resources - "files": files, - "color_space": color_space} # required for resources + return { + "node": node, + # here we are passing not only attribute, but with node again + # this should be simplified and changed extractor. + "attribute": "{}.{}".format(node, attribute), + "source": source, # required for resources + "files": files, + "color_space": color_space + } # required for resources class CollectModelRenderSets(CollectLook): From ce882641e7baec3c7edca957e2024331943ab9af Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:27:40 +0200 Subject: [PATCH 49/56] Vendor: updating scriptmenu to 1.5.2 --- openpype/vendor/python/common/scriptsmenu/action.py | 3 ++- openpype/vendor/python/common/scriptsmenu/launchfornuke.py | 7 ++----- openpype/vendor/python/common/scriptsmenu/scriptsmenu.py | 3 +-- openpype/vendor/python/common/scriptsmenu/version.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/vendor/python/common/scriptsmenu/action.py b/openpype/vendor/python/common/scriptsmenu/action.py index dc4d775f6a..5e68628406 100644 --- a/openpype/vendor/python/common/scriptsmenu/action.py +++ b/openpype/vendor/python/common/scriptsmenu/action.py @@ -119,7 +119,8 @@ module.{module_name}()""" """ # get the current application and its linked keyboard modifiers - modifiers = QtWidgets.QApplication.keyboardModifiers() + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. diff --git a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py index 23e4ed1b4d..72302a79a6 100644 --- a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py +++ b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py @@ -8,7 +8,7 @@ def _nuke_main_window(): if (obj.inherits('QMainWindow') and obj.metaObject().className() == 'Foundry::UI::DockMainWindow'): return obj - raise RuntimeError('Could not find Nuke MainWindow instance') + raise RuntimeError('Could not find Nuke MainWindow instance') def _nuke_main_menubar(): @@ -22,9 +22,6 @@ def _nuke_main_menubar(): def main(title="Scripts"): - # Register control + shift callback to add to shelf (Nuke behavior) - # modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - # menu.register_callback(modifiers, to_shelf) nuke_main_bar = _nuke_main_menubar() for nuke_bar in nuke_main_bar.children(): if isinstance(nuke_bar, scriptsmenu.ScriptsMenu): @@ -33,4 +30,4 @@ def main(title="Scripts"): return menu menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar) - return menu \ No newline at end of file + return menu diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py index e2b7ff96c7..9e7c094902 100644 --- a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -264,8 +264,7 @@ class ScriptsMenu(QtWidgets.QMenu): action.setVisible(True) else: for action in self._script_actions: - if not action.has_tag(search.lower()): - action.setVisible(False) + action.setVisible(action.has_tag(search.lower())) # Set visibility for all submenus for action in self.actions(): diff --git a/openpype/vendor/python/common/scriptsmenu/version.py b/openpype/vendor/python/common/scriptsmenu/version.py index 73f9426c2d..52ec49c845 100644 --- a/openpype/vendor/python/common/scriptsmenu/version.py +++ b/openpype/vendor/python/common/scriptsmenu/version.py @@ -1,6 +1,6 @@ VERSION_MAJOR = 1 VERSION_MINOR = 5 -VERSION_PATCH = 1 +VERSION_PATCH = 2 version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) From d9a9981fefacc42a9e9466f6af6769f1605d4396 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 May 2022 19:37:14 +0200 Subject: [PATCH 50/56] Fix - Harmony 21.1 messed up Javascript Qt API QDataStream is missing, different way to get codec used. QApplication.activeWindow() also returned null, replaced by topLevelWidgets --- openpype/hosts/harmony/api/TB_sceneOpened.js | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 6a403fa65e..29473bcc93 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -279,19 +279,13 @@ function Client() { }; self._send = function(message) { - var data = new QByteArray(); - var outstr = new QDataStream(data, QIODevice.WriteOnly); - outstr.writeInt(0); - data.append('UTF-8'); - outstr.device().seek(0); - outstr.writeInt(data.size() - 4); - var codec = QTextCodec.codecForUtfText(data); - var msg = codec.fromUnicode(message); - var l = msg.size(); - var coded = new QByteArray('AH').append(self.pack(l)); - coded = coded.append(msg); - self.socket.write(new QByteArray(coded)); - self.logDebug('Sent.'); + var codec_name = new QByteArray().append("ISO-8859-1"); + var codec = QTextCodec.codecForName(codec_name); + var msg = codec.fromUnicode(message); + var l = msg.size(); + var coded = new QByteArray().append('AH').append(self.pack(l)).append(msg); + self.socket.write(new QByteArray(coded)); + self.logDebug('Sent.'); }; self.waitForLock = function() { @@ -343,6 +337,7 @@ function start() { var host = '127.0.0.1'; /** port of the server */ var port = parseInt(System.getenv('AVALON_HARMONY_PORT')); + MessageLog.trace("port " + port.toString()); // Attach the client to the QApplication to preserve. var app = QCoreApplication.instance(); @@ -351,7 +346,15 @@ function start() { app.avalonClient = new Client(); app.avalonClient.socket.connectToHost(host, port); } - var menuBar = QApplication.activeWindow().menuBar(); + var mainWindow = null; + var widgets = QApplication.topLevelWidgets(); + for (var i = 0 ; i < widgets.length; i++) { + if (widgets[i] instanceof QMainWindow){ + MessageLog.trace('(DEBUG): START Main window '); + mainWindow = widgets[i]; + } + } + var menuBar = mainWindow.menuBar(); var actions = menuBar.actions(); app.avalonMenu = null; From f213a33f130d336ca8345ab64bbc6d1105c3a379 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 May 2022 19:40:40 +0200 Subject: [PATCH 51/56] Fix - Harmony 21.1 messed up Javascript Qt API Removed missed logging --- openpype/hosts/harmony/api/TB_sceneOpened.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 29473bcc93..610b0a73bb 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -337,7 +337,6 @@ function start() { var host = '127.0.0.1'; /** port of the server */ var port = parseInt(System.getenv('AVALON_HARMONY_PORT')); - MessageLog.trace("port " + port.toString()); // Attach the client to the QApplication to preserve. var app = QCoreApplication.instance(); @@ -350,7 +349,6 @@ function start() { var widgets = QApplication.topLevelWidgets(); for (var i = 0 ; i < widgets.length; i++) { if (widgets[i] instanceof QMainWindow){ - MessageLog.trace('(DEBUG): START Main window '); mainWindow = widgets[i]; } } From d3179847d266be44a96ab08e9b9b7fdffe5b3173 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 14:59:08 +0200 Subject: [PATCH 52/56] General: editorial otio_range in collection was one frame longer --- openpype/lib/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 2c877b9d0d..7b2d22f738 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata): first, last = otio_range_to_frame_range(otio_range) collection = clique.Collection( head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, (last + 1))]) + collection.indexes.update([i for i in range(first, last)]) return dir_path, collection From a2289429a850061484300e81eaed99413a87ef38 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 15:57:52 +0200 Subject: [PATCH 53/56] :sparkles: added collector for FBX camera export --- .../plugins/publish/collect_fbx_camera.py | 20 +++++++++++++++++++ .../defaults/project_settings/maya.json | 3 +++ .../schemas/schema_maya_publish.json | 14 +++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/collect_fbx_camera.py diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py new file mode 100644 index 0000000000..bfa5bccbb9 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api + + +class CollectFbxCamera(pyblish.api.InstancePlugin): + """Collect Camera for FBX export.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Camera for FBX export" + families = ["camera"] + + def process(self, instance): + if not instance.data.get("families"): + instance.data["families"] = [] + + if "fbx" not in instance.data["families"]: + instance.data["families"].append("fbx") + + instance.data["cameras"] = True diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4cdfe1ca5d..e03bdcecc3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -165,6 +165,9 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "CollectFbxCamera": { + "enabled": false + }, "ValidateInstanceInContext": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 2e5bc64e1c..9877b5ff0d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -21,6 +21,20 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectFbxCamera", + "label": "Collect Camera for FBX export", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "splitter" }, From 24c289a0d5088c708d9e6754dd8ebbc033e0e491 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:18:00 +0200 Subject: [PATCH 54/56] nuke: use framerange used as list but it is bool --- openpype/hosts/nuke/api/lib.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ba8aa7a8db..f40425eefc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -373,7 +373,7 @@ def add_write_node_legacy(name, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -391,10 +391,10 @@ def add_write_node_legacy(name, **kwarg): log.debug(e) continue - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w @@ -409,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -420,10 +420,10 @@ def add_write_node(name, file_path, knobs, **kwarg): # finally add knob overrides set_node_knobs_from_settings(w, knobs, **kwarg) - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w From a4c32639d7bcd1aae3735f763fadd7a334968df9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:51:25 +0200 Subject: [PATCH 55/56] nuke: adding frame range to plugin --- openpype/hosts/nuke/plugins/create/create_write_prerender.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 32ee1fd86f..fec97167fb 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -27,6 +27,10 @@ class CreateWritePrerender(plugin.AbstractWriteRender): # add fpath_template write_data["fpath_template"] = self.fpath_template write_data["use_range_limit"] = self.use_range_limit + write_data["frame_range"] = ( + nuke.root()["first_frame"].value(), + nuke.root()["last_frame"].value() + ) if not self.is_legacy(): return create_write_node( From b8cade1009bf4863b12c467fa0cbdef7b8d864e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 18:43:00 +0200 Subject: [PATCH 56/56] Fix - Harmony message length Harmony 21.1 doesn't have QDataStream anymore. This means we aren't able to write bytes into QByteArray so we had modify how content lenght is sent do the server. Content lenght is sent as string of 8 char convertible into integer (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) --- openpype/hosts/harmony/api/TB_sceneOpened.js | 21 ++++++++++++++++---- openpype/hosts/harmony/api/server.py | 12 +++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 610b0a73bb..e7cd555332 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -35,7 +35,11 @@ function Client() { self.pack = function(num) { var ascii=''; for (var i = 3; i >= 0; i--) { - ascii += String.fromCharCode((num >> (8 * i)) & 255); + var hex = ((num >> (8 * i)) & 255).toString(16); + if (hex.length < 2){ + ascii += "0"; + } + ascii += hex; } return ascii; }; @@ -279,12 +283,21 @@ function Client() { }; self._send = function(message) { - var codec_name = new QByteArray().append("ISO-8859-1"); + /** Harmony 21.1 doesn't have QDataStream anymore. + + This means we aren't able to write bytes into QByteArray so we had + modify how content lenght is sent do the server. + Content lenght is sent as string of 8 char convertible into integer + (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */ + var codec_name = new QByteArray().append("UTF-8"); + var codec = QTextCodec.codecForName(codec_name); var msg = codec.fromUnicode(message); var l = msg.size(); - var coded = new QByteArray().append('AH').append(self.pack(l)).append(msg); - self.socket.write(new QByteArray(coded)); + var header = new QByteArray().append('AH').append(self.pack(l)); + var coded = msg.prepend(header); + + self.socket.write(coded); self.logDebug('Sent.'); }; diff --git a/openpype/hosts/harmony/api/server.py b/openpype/hosts/harmony/api/server.py index 88cfe54521..0de359ec61 100644 --- a/openpype/hosts/harmony/api/server.py +++ b/openpype/hosts/harmony/api/server.py @@ -88,21 +88,25 @@ class Server(threading.Thread): """ current_time = time.time() while True: - + self.log.info("wait ttt") # Receive the data in small chunks and retransmit it request = None - header = self.connection.recv(6) + header = self.connection.recv(10) if len(header) == 0: # null data received, socket is closing. self.log.info(f"[{self.timestamp()}] Connection closing.") break + if header[0:2] != b"AH": self.log.error("INVALID HEADER") - length = struct.unpack(">I", header[2:])[0] + content_length_str = header[2:].decode() + + length = int(content_length_str, 16) data = self.connection.recv(length) while (len(data) < length): # we didn't received everything in first try, lets wait for # all data. + self.log.info("loop") time.sleep(0.1) if self.connection is None: self.log.error(f"[{self.timestamp()}] " @@ -113,7 +117,7 @@ class Server(threading.Thread): break data += self.connection.recv(length - len(data)) - + self.log.debug("data:: {} {}".format(data, type(data))) self.received += data.decode("utf-8") pretty = self._pretty(self.received) self.log.debug(