From 0ac27ab609a1198255e2fdad846f7be698e0e725 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 12 May 2022 13:19:29 +0200 Subject: [PATCH 1/9] start of integration --- openpype/hosts/3dsmax/__init__.py | 0 openpype/hosts/3dsmax/api/__init__.py | 0 openpype/hosts/3dsmax/plugins/__init__.py | 0 openpype/hosts/3dsmax/startup/startup.ms | 8 ++++ openpype/hosts/3dsmax/startup/startup.py | 2 + openpype/resources/app_icons/3dsmax.png | Bin 0 -> 12804 bytes .../system_settings/applications.json | 29 +++++++++++++ openpype/settings/entities/enum_entity.py | 1 + .../host_settings/schema_3dsmax.json | 39 ++++++++++++++++++ .../system_schema/schema_applications.json | 4 ++ 10 files changed, 83 insertions(+) create mode 100644 openpype/hosts/3dsmax/__init__.py create mode 100644 openpype/hosts/3dsmax/api/__init__.py create mode 100644 openpype/hosts/3dsmax/plugins/__init__.py create mode 100644 openpype/hosts/3dsmax/startup/startup.ms create mode 100644 openpype/hosts/3dsmax/startup/startup.py create mode 100644 openpype/resources/app_icons/3dsmax.png create mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_3dsmax.json diff --git a/openpype/hosts/3dsmax/__init__.py b/openpype/hosts/3dsmax/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/3dsmax/api/__init__.py b/openpype/hosts/3dsmax/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/3dsmax/plugins/__init__.py b/openpype/hosts/3dsmax/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/3dsmax/startup/startup.ms b/openpype/hosts/3dsmax/startup/startup.ms new file mode 100644 index 0000000000..94318afb01 --- /dev/null +++ b/openpype/hosts/3dsmax/startup/startup.ms @@ -0,0 +1,8 @@ +-- OpenPype Init Script +( + local sysPath = dotNetClass "System.IO.Path" + local sysDir = dotNetClass "System.IO.Directory" + local startup = sysPath.Combine (sysPath.GetDirectoryName getSourceFile) "startup.py" + + python.ExecuteFile startup +) \ No newline at end of file diff --git a/openpype/hosts/3dsmax/startup/startup.py b/openpype/hosts/3dsmax/startup/startup.py new file mode 100644 index 0000000000..dd8c08a6b9 --- /dev/null +++ b/openpype/hosts/3dsmax/startup/startup.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +print("inside python startup") \ No newline at end of file diff --git a/openpype/resources/app_icons/3dsmax.png b/openpype/resources/app_icons/3dsmax.png new file mode 100644 index 0000000000000000000000000000000000000000..9ebdf6099f6ac279ebaeccdf21a658a34da5e7b6 GIT binary patch literal 12804 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

{SJ~!Y9>I3PqWedKP*SW{qUGfR^((-q<{G5uF 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 literal 0 HcmV?d00001 diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 0fb99a2608..aaecef3494 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1232,6 +1232,35 @@ } } }, + "3dsmax": { + "enabled": true, + "label": "3ds max", + "icon": "{}/app_icons/3dsmax.png", + "host_name": "3dsmax", + "environment": { + + }, + "variants": { + "2023": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\3ds Max 2023\\3dsmax.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": { + "3DSMAX_VERSION": "2023" + } + } + } + }, "djvview": { "enabled": true, "label": "DJV View", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 92a397afba..b6004a3feb 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -154,6 +154,7 @@ class HostsEnumEntity(BaseEnumEntity): """ schema_types = ["hosts-enum"] all_host_names = [ + "3dsmax", "aftereffects", "blender", "celaction", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_3dsmax.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_3dsmax.json new file mode 100644 index 0000000000..f7c57298af --- /dev/null +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_3dsmax.json @@ -0,0 +1,39 @@ +{ + "type": "dict", + "key": "3dsmax", + "label": "Autodesk 3ds Max", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables" + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json" + }, + { + "type": "dict-modifiable", + "key": "variants", + "collapsible_key": true, + "use_label_wrap": false, + "object_type": { + "type": "dict", + "collapsible": true, + "children": [ + { + "type": "schema_template", + "name": "template_host_variant_items" + } + ] + } + } + ] +} diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index 20be33320d..36c5811496 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -9,6 +9,10 @@ "type": "schema", "name": "schema_maya" }, + { + "type": "schema", + "name": "schema_3dsmax" + }, { "type": "schema", "name": "schema_flame" From decc8df4aef6eb1aef8e55152c3eea0760d1fad2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Nov 2022 17:29:58 +0100 Subject: [PATCH 2/9] :construction: 3dsmax addon basics --- openpype/hosts/3dsmax/api/__init__.py | 0 openpype/hosts/3dsmax/plugins/__init__.py | 0 openpype/hosts/3dsmax/startup/startup.py | 2 - openpype/hosts/max/__init__.py | 10 ++ openpype/hosts/max/addon.py | 16 ++ openpype/hosts/max/api/__init__.py | 13 ++ openpype/hosts/max/api/lib.py | 2 + openpype/hosts/max/api/menu.py | 80 +++++++++ openpype/hosts/max/api/pipeline.py | 153 ++++++++++++++++++ openpype/hosts/max/hooks/set_paths.py | 17 ++ .../hosts/{3dsmax => max/plugins}/__init__.py | 0 .../hosts/{3dsmax => max}/startup/startup.ms | 0 openpype/hosts/max/startup/startup.py | 7 + openpype/settings/entities/enum_entity.py | 2 +- 14 files changed, 299 insertions(+), 3 deletions(-) delete mode 100644 openpype/hosts/3dsmax/api/__init__.py delete mode 100644 openpype/hosts/3dsmax/plugins/__init__.py delete mode 100644 openpype/hosts/3dsmax/startup/startup.py create mode 100644 openpype/hosts/max/__init__.py create mode 100644 openpype/hosts/max/addon.py create mode 100644 openpype/hosts/max/api/__init__.py create mode 100644 openpype/hosts/max/api/lib.py create mode 100644 openpype/hosts/max/api/menu.py create mode 100644 openpype/hosts/max/api/pipeline.py create mode 100644 openpype/hosts/max/hooks/set_paths.py rename openpype/hosts/{3dsmax => max/plugins}/__init__.py (100%) rename openpype/hosts/{3dsmax => max}/startup/startup.ms (100%) create mode 100644 openpype/hosts/max/startup/startup.py diff --git a/openpype/hosts/3dsmax/api/__init__.py b/openpype/hosts/3dsmax/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/3dsmax/plugins/__init__.py b/openpype/hosts/3dsmax/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/3dsmax/startup/startup.py b/openpype/hosts/3dsmax/startup/startup.py deleted file mode 100644 index dd8c08a6b9..0000000000 --- a/openpype/hosts/3dsmax/startup/startup.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -print("inside python startup") \ No newline at end of file diff --git a/openpype/hosts/max/__init__.py b/openpype/hosts/max/__init__.py new file mode 100644 index 0000000000..8da0e0ee42 --- /dev/null +++ b/openpype/hosts/max/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + MaxAddon, + MAX_HOST_DIR, +) + + +__all__ = ( + "MaxAddon", + "MAX_HOST_DIR", +) \ No newline at end of file diff --git a/openpype/hosts/max/addon.py b/openpype/hosts/max/addon.py new file mode 100644 index 0000000000..734b87dd21 --- /dev/null +++ b/openpype/hosts/max/addon.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import os +from openpype.modules import OpenPypeModule, IHostAddon + +MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class MaxAddon(OpenPypeModule, IHostAddon): + name = "max" + host_name = "max" + + def initialize(self, module_settings): + self.enabled = True + + def get_workfile_extensions(self): + return [".max"] diff --git a/openpype/hosts/max/api/__init__.py b/openpype/hosts/max/api/__init__.py new file mode 100644 index 0000000000..b6998df862 --- /dev/null +++ b/openpype/hosts/max/api/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +"""Public API for 3dsmax""" + +from .pipeline import ( + MaxHost +) +from .menu import OpenPypeMenu + + +__all__ = [ + "MaxHost", + "OpenPypeMenu" +] diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py new file mode 100644 index 0000000000..e50de85f68 --- /dev/null +++ b/openpype/hosts/max/api/lib.py @@ -0,0 +1,2 @@ +def imprint(attr, data): + ... diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py new file mode 100644 index 0000000000..13ca503b4d --- /dev/null +++ b/openpype/hosts/max/api/menu.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""3dsmax menu definition of OpenPype.""" +from abc import ABCMeta, abstractmethod +import six +from Qt import QtWidgets, QtCore +from pymxs import runtime as rt + +from openpype.tools.utils import host_tools + + +@six.add_metaclass(ABCMeta) +class OpenPypeMenu(object): + + def __init__(self): + self.main_widget = self.get_main_widget() + + @staticmethod + def get_main_widget(): + """Get 3dsmax main window.""" + return QtWidgets.QWidget.find(rt.windows.getMAXHWND()) + + def get_main_menubar(self): + """Get main Menubar by 3dsmax main window.""" + return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0] + + def get_or_create_openpype_menu(self, name="&OpenPype", before="&Help"): + menu_bar = self.get_main_menubar() + menu_items = menu_bar.findChildren( + QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly) + help_action = None + for item in menu_items: + if name in item.title(): + # we already have OpenPype menu + return item + + if before in item.title(): + help_action = item.menuAction() + + op_menu = QtWidgets.QMenu("&OpenPype") + menu_bar.insertMenu(before, op_menu) + return op_menu + + def build_openpype_menu(self): + openpype_menu = self.get_or_create_openpype_menu() + load_action = QtWidgets.QAction("Load...", openpype_menu) + load_action.triggered.connect(self.load_callback) + openpype_menu.addAction(load_action) + + publish_action = QtWidgets.QAction("Publish...", openpype_menu) + publish_action.triggered.connect(self.publish_callback) + openpype_menu.addAction(publish_action) + + manage_action = QtWidgets.QAction("Manage...", openpype_menu) + manage_action.triggered.connect(self.manage_callback) + openpype_menu.addAction(manage_action) + + library_action = QtWidgets.QAction("Library...", openpype_menu) + library_action.triggered.connect(self.library_callback) + openpype_menu.addAction(library_action) + + openpype_menu.addSeparator() + + workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) + workfiles_action.triggered.connect(self.workfiles_callback) + openpype_menu.addAction(workfiles_action) + + def load_callback(self): + host_tools.show_loader(parent=self.main_widget) + + def publish_callback(self): + host_tools.show_publisher(parent=self.main_widget) + + def manage_callback(self): + host_tools.show_subset_manager(parent=self.main_widget) + + def library_callback(self): + host_tools.show_library_loader(parent=self.main_widget) + + def workfiles_callback(self): + host_tools.show_workfiles(parent=self.main_widget) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py new file mode 100644 index 0000000000..2ee5989871 --- /dev/null +++ b/openpype/hosts/max/api/pipeline.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +"""Pipeline tools for OpenPype Houdini integration.""" +import os +import sys +import logging +import contextlib + +from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher +import pyblish.api +from openpype.pipeline import ( + register_creator_plugin_path, + register_loader_plugin_path, + AVALON_CONTAINER_ID, +) +from openpype.hosts.max.api import OpenPypeMenu +from openpype.hosts.max.api import lib +from openpype.hosts.max import MAX_HOST_DIR +from openpype.pipeline.load import any_outdated_containers +from openpype.lib import ( + register_event_callback, + emit_event, +) +from pymxs import runtime as rt # noqa + +log = logging.getLogger("openpype.hosts.max") + +PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + + +class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): + name = "max" + menu = None + + def __init__(self): + super(MaxHost, self).__init__() + self._op_events = {} + self._has_been_setup = False + + def install(self): + pyblish.api.register_host("max") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + log.info("Building menu ...") + + self.menu = OpenPypeMenu() + + log.info("Installing callbacks ... ") + # register_event_callback("init", on_init) + self._register_callbacks() + + # register_event_callback("before.save", before_save) + # register_event_callback("save", on_save) + # register_event_callback("open", on_open) + # register_event_callback("new", on_new) + + # pyblish.api.register_callback( + # "instanceToggled", on_pyblish_instance_toggled + # ) + + self._has_been_setup = True + + def has_unsaved_changes(self): + # TODO: how to get it from 3dsmax? + return True + + def get_workfile_extensions(self): + return [".hip", ".hiplc", ".hipnc"] + + def save_workfile(self, dst_path=None): + rt.saveMaxFile(dst_path) + return dst_path + + def open_workfile(self, filepath): + rt.checkForSave() + rt.loadMaxFile(filepath) + return filepath + + def get_current_workfile(self): + return os.path.join(rt.maxFilePath, rt.maxFileName) + + def get_containers(self): + return ls() + + def _register_callbacks(self): + for event in self._op_events.copy().values(): + if event is None: + continue + + try: + rt.callbacks.removeScript(id=rt.name(event.name)) + except RuntimeError as e: + log.info(e) + + rt.callbacks.addScript( + event.name, event.callback, id=rt.Name('OpenPype')) + + @staticmethod + def create_context_node(): + """Helper for creating context holding node.""" + + root_scene = rt.rootScene + + create_attr_script = (""" +attributes "OpenPypeContext" +( + parameters main rollout:params + ( + context type: #string + ) + + rollout params "OpenPype Parameters" + ( + editText editTextContext "Context" type: #string + ) +) + """) + + attr = rt.execute(create_attr_script) + rt.custAttributes.add(root_scene, attr) + + return root_scene.OpenPypeContext.context + + def update_context_data(self, data, changes): + try: + context = rt.rootScene.OpenPypeContext.context + except AttributeError: + # context node doesn't exists + context = self.create_context_node() + + lib.imprint(context, data) + + def get_context_data(self): + try: + context = rt.rootScene.OpenPypeContext.context + except AttributeError: + # context node doesn't exists + context = self.create_context_node() + return lib.read(context) + + def save_file(self, dst_path=None): + # Force forwards slashes to avoid segfault + dst_path = dst_path.replace("\\", "/") + rt.saveMaxFile(dst_path) + + +def ls(): + ... \ No newline at end of file diff --git a/openpype/hosts/max/hooks/set_paths.py b/openpype/hosts/max/hooks/set_paths.py new file mode 100644 index 0000000000..3db5306344 --- /dev/null +++ b/openpype/hosts/max/hooks/set_paths.py @@ -0,0 +1,17 @@ +from openpype.lib import PreLaunchHook + + +class SetPath(PreLaunchHook): + """Set current dir to workdir. + + Hook `GlobalHostDataHook` must be executed before this hook. + """ + app_groups = ["max"] + + def execute(self): + workdir = self.launch_context.env.get("AVALON_WORKDIR", "") + if not workdir: + self.log.warning("BUG: Workdir is not filled.") + return + + self.launch_context.kwargs["cwd"] = workdir diff --git a/openpype/hosts/3dsmax/__init__.py b/openpype/hosts/max/plugins/__init__.py similarity index 100% rename from openpype/hosts/3dsmax/__init__.py rename to openpype/hosts/max/plugins/__init__.py diff --git a/openpype/hosts/3dsmax/startup/startup.ms b/openpype/hosts/max/startup/startup.ms similarity index 100% rename from openpype/hosts/3dsmax/startup/startup.ms rename to openpype/hosts/max/startup/startup.ms diff --git a/openpype/hosts/max/startup/startup.py b/openpype/hosts/max/startup/startup.py new file mode 100644 index 0000000000..afcbd2d132 --- /dev/null +++ b/openpype/hosts/max/startup/startup.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from openpype.hosts.max.api import MaxHost +from openpype.pipeline import install_host + +host = MaxHost() +install_host(host) + diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index c07350ba07..c0c103ea10 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -152,7 +152,7 @@ class HostsEnumEntity(BaseEnumEntity): schema_types = ["hosts-enum"] all_host_names = [ - "3dsmax", + "max", "aftereffects", "blender", "celaction", From 8b71066d9c33d782ca2520bce251fe733e4d8ad5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Dec 2022 16:53:09 +0100 Subject: [PATCH 3/9] :art: add menu and basic publishing support --- openpype/hosts/max/api/__init__.py | 2 - openpype/hosts/max/api/lib.py | 66 ++++++++++- openpype/hosts/max/api/menu.py | 64 +++++++++-- openpype/hosts/max/api/pipeline.py | 63 +++++----- openpype/hosts/max/api/plugin.py | 108 ++++++++++++++++++ .../max/plugins/create/create_pointcache.py | 21 ++++ openpype/hosts/max/startup/startup.ms | 3 +- 7 files changed, 284 insertions(+), 43 deletions(-) create mode 100644 openpype/hosts/max/api/plugin.py create mode 100644 openpype/hosts/max/plugins/create/create_pointcache.py diff --git a/openpype/hosts/max/api/__init__.py b/openpype/hosts/max/api/__init__.py index b6998df862..503afade73 100644 --- a/openpype/hosts/max/api/__init__.py +++ b/openpype/hosts/max/api/__init__.py @@ -4,10 +4,8 @@ from .pipeline import ( MaxHost ) -from .menu import OpenPypeMenu __all__ = [ "MaxHost", - "OpenPypeMenu" ] diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index e50de85f68..8a57bb1bf6 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,2 +1,64 @@ -def imprint(attr, data): - ... +# -*- coding: utf-8 -*- +"""Library of functions useful for 3dsmax pipeline.""" +from pymxs import runtime as rt +from typing import Union + + +def imprint(node_name: str, data: dict) -> bool: + node = rt.getNodeByName(node_name) + if not node: + return False + + for k, v in data.items(): + rt.setUserProp(node, k, v) + + return True + + +def lsattr( + attr: str, + value: Union[str, None] = None, + root: Union[str, None] = None) -> list: + """List nodes having attribute with specified value. + + Args: + attr (str): Attribute name to match. + value (str, Optional): Value to match, of omitted, all nodes + with specified attribute are returned no matter of value. + root (str, Optional): Root node name. If omitted, scene root is used. + + Returns: + list of nodes. + """ + root = rt.rootnode if root is None else rt.getNodeByName(root) + + def output_node(node, nodes): + nodes.append(node) + for child in node.Children: + output_node(child, nodes) + + nodes = [] + output_node(root, nodes) + if not value: + return [n for n in nodes if rt.getUserProp(n, attr)] + + return [n for n in nodes if rt.getUserProp(n, attr) == value] + + +def read(container) -> dict: + data = {} + props = rt.getUserPropBuffer(container) + # this shouldn't happen but let's guard against it anyway + if not props: + return data + + for line in props.split("\r\n"): + key, value = line.split("=") + # if the line cannot be split we can't really parse it + if not key: + continue + data[key.strip()] = value.strip() + + data["instance_node"] = container + + return data diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 13ca503b4d..d1913c51e0 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -1,29 +1,70 @@ # -*- coding: utf-8 -*- """3dsmax menu definition of OpenPype.""" -from abc import ABCMeta, abstractmethod -import six from Qt import QtWidgets, QtCore from pymxs import runtime as rt from openpype.tools.utils import host_tools -@six.add_metaclass(ABCMeta) class OpenPypeMenu(object): + """Object representing OpenPype menu. + + This is using "hack" to inject itself before "Help" menu of 3dsmax. + For some reason `postLoadingMenus` event doesn't fire, and main menu + if probably re-initialized by menu templates, se we wait for at least + 1 event Qt event loop before trying to insert. + + """ def __init__(self): + super().__init__() self.main_widget = self.get_main_widget() + self.menu = None + + timer = QtCore.QTimer() + # set number of event loops to wait. + timer.setInterval(1) + timer.timeout.connect(self._on_timer) + timer.start() + + self._timer = timer + self._counter = 0 + + def _on_timer(self): + if self._counter < 1: + self._counter += 1 + return + + self._counter = 0 + self._timer.stop() + self.build_openpype_menu() @staticmethod def get_main_widget(): """Get 3dsmax main window.""" return QtWidgets.QWidget.find(rt.windows.getMAXHWND()) - def get_main_menubar(self): + def get_main_menubar(self) -> QtWidgets.QMenuBar: """Get main Menubar by 3dsmax main window.""" return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0] - def get_or_create_openpype_menu(self, name="&OpenPype", before="&Help"): + def get_or_create_openpype_menu( + self, name: str = "&OpenPype", + before: str = "&Help") -> QtWidgets.QAction: + """Create OpenPype menu. + + Args: + name (str, Optional): OpenPypep menu name. + before (str, Optional): Name of the 3dsmax main menu item to + add OpenPype menu before. + + Returns: + QtWidgets.QAction: OpenPype menu action. + + """ + if self.menu is not None: + return self.menu + menu_bar = self.get_main_menubar() menu_items = menu_bar.findChildren( QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly) @@ -37,10 +78,13 @@ class OpenPypeMenu(object): help_action = item.menuAction() op_menu = QtWidgets.QMenu("&OpenPype") - menu_bar.insertMenu(before, op_menu) + menu_bar.insertMenu(help_action, op_menu) + + self.menu = op_menu return op_menu - def build_openpype_menu(self): + def build_openpype_menu(self) -> QtWidgets.QAction: + """Build items in OpenPype menu.""" openpype_menu = self.get_or_create_openpype_menu() load_action = QtWidgets.QAction("Load...", openpype_menu) load_action.triggered.connect(self.load_callback) @@ -63,18 +107,24 @@ class OpenPypeMenu(object): workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) workfiles_action.triggered.connect(self.workfiles_callback) openpype_menu.addAction(workfiles_action) + return openpype_menu def load_callback(self): + """Callback to show Loader tool.""" host_tools.show_loader(parent=self.main_widget) def publish_callback(self): + """Callback to show Publisher tool.""" host_tools.show_publisher(parent=self.main_widget) def manage_callback(self): + """Callback to show Scene Manager/Inventory tool.""" host_tools.show_subset_manager(parent=self.main_widget) def library_callback(self): + """Callback to show Library Loader tool.""" host_tools.show_library_loader(parent=self.main_widget) def workfiles_callback(self): + """Callback to show Workfiles tool.""" host_tools.show_workfiles(parent=self.main_widget) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 2ee5989871..cef45193c4 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -5,6 +5,8 @@ import sys import logging import contextlib +import json + from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher import pyblish.api from openpype.pipeline import ( @@ -12,7 +14,7 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) -from openpype.hosts.max.api import OpenPypeMenu +from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max import MAX_HOST_DIR from openpype.pipeline.load import any_outdated_containers @@ -32,6 +34,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): + name = "max" menu = None @@ -46,23 +49,10 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info("Building menu ...") + # self._register_callbacks() self.menu = OpenPypeMenu() - log.info("Installing callbacks ... ") - # register_event_callback("init", on_init) - self._register_callbacks() - - # register_event_callback("before.save", before_save) - # register_event_callback("save", on_save) - # register_event_callback("open", on_open) - # register_event_callback("new", on_new) - - # pyblish.api.register_callback( - # "instanceToggled", on_pyblish_instance_toggled - # ) - self._has_been_setup = True def has_unsaved_changes(self): @@ -70,7 +60,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): return True def get_workfile_extensions(self): - return [".hip", ".hiplc", ".hipnc"] + return [".max"] def save_workfile(self, dst_path=None): rt.saveMaxFile(dst_path) @@ -88,17 +78,15 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): return ls() def _register_callbacks(self): - for event in self._op_events.copy().values(): - if event is None: - continue + rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks")) - try: - rt.callbacks.removeScript(id=rt.name(event.name)) - except RuntimeError as e: - log.info(e) + rt.callbacks.addScript( + rt.Name("postLoadingMenus"), + self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks')) - rt.callbacks.addScript( - event.name, event.callback, id=rt.Name('OpenPype')) + def _deferred_menu_creation(self): + self.log.info("Building menu ...") + self.menu = OpenPypeMenu() @staticmethod def create_context_node(): @@ -128,12 +116,12 @@ attributes "OpenPypeContext" def update_context_data(self, data, changes): try: - context = rt.rootScene.OpenPypeContext.context + _ = rt.rootScene.OpenPypeContext.context except AttributeError: # context node doesn't exists - context = self.create_context_node() + self.create_context_node() - lib.imprint(context, data) + rt.rootScene.OpenPypeContext.context = json.dumps(data) def get_context_data(self): try: @@ -141,7 +129,9 @@ attributes "OpenPypeContext" except AttributeError: # context node doesn't exists context = self.create_context_node() - return lib.read(context) + if not context: + context = "{}" + return json.loads(context) def save_file(self, dst_path=None): # Force forwards slashes to avoid segfault @@ -149,5 +139,16 @@ attributes "OpenPypeContext" rt.saveMaxFile(dst_path) -def ls(): - ... \ No newline at end of file +def ls() -> list: + """Get all OpenPype instances.""" + objs = rt.objects + containers = [ + obj for obj in objs + if rt.getUserProp(obj, "id") == AVALON_CONTAINER_ID + ] + + for container in sorted(containers, key=lambda name: container.name): + yield lib.read(container) + + + diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py new file mode 100644 index 0000000000..0f01c94ce1 --- /dev/null +++ b/openpype/hosts/max/api/plugin.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +"""3dsmax specific Avalon/Pyblish plugin definitions.""" +import sys +from pymxs import runtime as rt +import six +from abc import ABCMeta +from openpype.pipeline import ( + CreatorError, + Creator, + CreatedInstance +) +from openpype.lib import BoolDef +from .lib import imprint, read, lsattr + + +class OpenPypeCreatorError(CreatorError): + pass + + +class MaxCreatorBase(object): + + @staticmethod + def cache_subsets(shared_data): + if shared_data.get("max_cached_subsets") is None: + shared_data["max_cached_subsets"] = {} + cached_instances = lsattr("id", "pyblish.avalon.instance") + for i in cached_instances: + creator_id = i.get("creator_identifier") + if creator_id not in shared_data["max_cached_subsets"]: + shared_data["houdini_cached_subsets"][creator_id] = [i] + else: + shared_data[ + "houdini_cached_subsets"][creator_id].append(i) # noqa + return shared_data + + @staticmethod + def create_instance_node(node_name: str, parent: str = ""): + parent_node = rt.getNodeByName(parent) if parent else rt.rootScene + if not parent_node: + raise OpenPypeCreatorError(f"Specified parent {parent} not found") + + container = rt.container(name=node_name) + container.Parent = parent_node + + return container + + +@six.add_metaclass(ABCMeta) +class MaxCreator(Creator, MaxCreatorBase): + selected_nodes = [] + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + self.selected_nodes = rt.getCurrentSelection() + + instance_node = self.create_instance_node(subset_name) + instance_data["instance_node"] = instance_node.name + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self + ) + self._add_instance_to_context(instance) + imprint(instance_node.name, instance.data_to_store()) + return instance + + def collect_instances(self): + self.cache_subsets(self.collection_shared_data) + for instance in self.collection_shared_data[ + "max_cached_subsets"].get(self.identifier, []): + created_instance = CreatedInstance.from_existing( + read(instance), self + ) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + instance_node = created_inst.get("instance_node") + + new_values = { + key: new_value + for key, (_old_value, new_value) in _changes.items() + } + imprint( + instance_node, + new_values, + ) + + def remove_instances(self, instances): + """Remove specified instance from the scene. + + This is only removing `id` parameter so instance is no longer + instance, because it might contain valuable data for artist. + + """ + for instance in instances: + instance_node = rt.getNodeByName( + instance.data.get("instance_node")) + if instance_node: + rt.delete(instance_node) + + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection") + ] diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py new file mode 100644 index 0000000000..4c9ec7fb97 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating pointcache alembics.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreatePointCache(plugin.MaxCreator): + identifier = "io.openpype.creators.max.pointcache" + label = "Point Cache" + family = "pointcache" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + + instance = super(CreatePointCache, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/startup/startup.ms b/openpype/hosts/max/startup/startup.ms index 94318afb01..aee40eb6bc 100644 --- a/openpype/hosts/max/startup/startup.ms +++ b/openpype/hosts/max/startup/startup.ms @@ -2,7 +2,8 @@ ( local sysPath = dotNetClass "System.IO.Path" local sysDir = dotNetClass "System.IO.Directory" - local startup = sysPath.Combine (sysPath.GetDirectoryName getSourceFile) "startup.py" + local localScript = getThisScriptFilename() + local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) "startup.py" python.ExecuteFile startup ) \ No newline at end of file From 1c985ca0015ce4e3161e18a91205a4590401e243 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 6 Dec 2022 23:45:51 +0100 Subject: [PATCH 4/9] :bug: fix publishing of alembics --- openpype/hosts/max/__init__.py | 2 +- openpype/hosts/max/api/__init__.py | 11 +- openpype/hosts/max/api/lib.py | 78 ++++++++++++-- openpype/hosts/max/api/pipeline.py | 9 +- openpype/hosts/max/api/plugin.py | 15 +-- .../max/plugins/publish/collect_workfile.py | 63 +++++++++++ .../max/plugins/publish/extract_pointcache.py | 100 ++++++++++++++++++ .../plugins/publish/validate_scene_saved.py | 19 ++++ 8 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/collect_workfile.py create mode 100644 openpype/hosts/max/plugins/publish/extract_pointcache.py create mode 100644 openpype/hosts/max/plugins/publish/validate_scene_saved.py diff --git a/openpype/hosts/max/__init__.py b/openpype/hosts/max/__init__.py index 8da0e0ee42..9a5af8258c 100644 --- a/openpype/hosts/max/__init__.py +++ b/openpype/hosts/max/__init__.py @@ -7,4 +7,4 @@ from .addon import ( __all__ = ( "MaxAddon", "MAX_HOST_DIR", -) \ No newline at end of file +) diff --git a/openpype/hosts/max/api/__init__.py b/openpype/hosts/max/api/__init__.py index 503afade73..26190dcfb8 100644 --- a/openpype/hosts/max/api/__init__.py +++ b/openpype/hosts/max/api/__init__.py @@ -2,10 +2,19 @@ """Public API for 3dsmax""" from .pipeline import ( - MaxHost + MaxHost, ) +from .lib import( + maintained_selection, + lsattr, + get_all_children +) + __all__ = [ "MaxHost", + "maintained_selection", + "lsattr", + "get_all_children" ] diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8a57bb1bf6..9256ca9ac1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,7 +1,13 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" +import json +import six from pymxs import runtime as rt from typing import Union +import contextlib + + +JSON_PREFIX = "JSON::" def imprint(node_name: str, data: dict) -> bool: @@ -10,7 +16,10 @@ def imprint(node_name: str, data: dict) -> bool: return False for k, v in data.items(): - rt.setUserProp(node, k, v) + if isinstance(v, (dict, list)): + rt.setUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}') + else: + rt.setUserProp(node, k, v) return True @@ -39,10 +48,13 @@ def lsattr( nodes = [] output_node(root, nodes) - if not value: - return [n for n in nodes if rt.getUserProp(n, attr)] - - return [n for n in nodes if rt.getUserProp(n, attr) == value] + return [ + n for n in nodes + if rt.getUserProp(n, attr) == value + ] if value else [ + n for n in nodes + if rt.getUserProp(n, attr) + ] def read(container) -> dict: @@ -53,12 +65,58 @@ def read(container) -> dict: return data for line in props.split("\r\n"): - key, value = line.split("=") - # if the line cannot be split we can't really parse it - if not key: + try: + key, value = line.split("=") + except ValueError: + # if the line cannot be split we can't really parse it continue - data[key.strip()] = value.strip() - data["instance_node"] = container + value = value.strip() + if isinstance(value.strip(), six.string_types) and \ + value.startswith(JSON_PREFIX): + try: + value = json.loads(value[len(JSON_PREFIX):]) + except json.JSONDecodeError: + # not a json + pass + + data[key.strip()] = value + + data["instance_node"] = container.name return data + + +@contextlib.contextmanager +def maintained_selection(): + previous_selection = rt.getCurrentSelection() + try: + yield + finally: + if previous_selection: + rt.select(previous_selection) + else: + rt.select() + + +def get_all_children(parent, node_type=None): + """Handy function to get all the children of a given node + + Args: + parent (3dsmax Node1): Node to get all children of. + node_type (None, runtime.class): give class to check for + e.g. rt.FFDBox/rt.GeometryClass etc. + + Returns: + list: list of all children of the parent node + """ + def list_children(node): + children = [] + for c in node.Children: + children.append(c) + children = children + list_children(c) + return children + child_list = list_children(parent) + + return ([x for x in child_list if rt.superClassOf(x) == node_type] + if node_type else child_list) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index cef45193c4..4f8271fb7e 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Houdini integration.""" import os -import sys import logging -import contextlib import json @@ -101,12 +99,12 @@ attributes "OpenPypeContext" ( context type: #string ) - + rollout params "OpenPype Parameters" ( editText editTextContext "Context" type: #string ) -) +) """) attr = rt.execute(create_attr_script) @@ -149,6 +147,3 @@ def ls() -> list: for container in sorted(containers, key=lambda name: container.name): yield lib.read(container) - - - diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 0f01c94ce1..4788bfd383 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """3dsmax specific Avalon/Pyblish plugin definitions.""" -import sys from pymxs import runtime as rt import six from abc import ABCMeta @@ -25,12 +24,12 @@ class MaxCreatorBase(object): shared_data["max_cached_subsets"] = {} cached_instances = lsattr("id", "pyblish.avalon.instance") for i in cached_instances: - creator_id = i.get("creator_identifier") + creator_id = rt.getUserProp(i, "creator_identifier") if creator_id not in shared_data["max_cached_subsets"]: - shared_data["houdini_cached_subsets"][creator_id] = [i] + shared_data["max_cached_subsets"][creator_id] = [i.name] else: shared_data[ - "houdini_cached_subsets"][creator_id].append(i) # noqa + "max_cached_subsets"][creator_id].append(i.name) # noqa return shared_data @staticmethod @@ -61,8 +60,12 @@ class MaxCreator(Creator, MaxCreatorBase): instance_data, self ) + for node in self.selected_nodes: + node.Parent = instance_node + self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) + return instance def collect_instances(self): @@ -70,7 +73,7 @@ class MaxCreator(Creator, MaxCreatorBase): for instance in self.collection_shared_data[ "max_cached_subsets"].get(self.identifier, []): created_instance = CreatedInstance.from_existing( - read(instance), self + read(rt.getNodeByName(instance)), self ) self._add_instance_to_context(created_instance) @@ -98,7 +101,7 @@ class MaxCreator(Creator, MaxCreatorBase): instance_node = rt.getNodeByName( instance.data.get("instance_node")) if instance_node: - rt.delete(instance_node) + rt.delete(rt.getNodeByName(instance_node)) self._remove_instance_from_context(instance) diff --git a/openpype/hosts/max/plugins/publish/collect_workfile.py b/openpype/hosts/max/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..7112337575 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_workfile.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""Collect current work file.""" +import os +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline import legacy_io, KnownPublishError + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.01 + label = "Collect 3dsmax Workfile" + hosts = ['max'] + + def process(self, context): + """Inject the current working file.""" + folder = rt.maxFilePath + file = rt.maxFileName + if not folder or not file: + self.log.error("Scene is not saved.") + current_file = os.path.join(folder, file) + + context.data['currentFile'] = current_file + + filename, ext = os.path.splitext(file) + + task = legacy_io.Session["AVALON_TASK"] + + data = {} + + # create instance + instance = context.create_instance(name=filename) + subset = 'workfile' + task.capitalize() + + data.update({ + "subset": subset, + "asset": os.getenv("AVALON_ASSET", None), + "label": subset, + "publish": True, + "family": 'workfile', + "families": ['workfile'], + "setMembers": [current_file], + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'], + "handleStart": context.data['handleStart'], + "handleEnd": context.data['handleEnd'] + }) + + data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] + + instance.data.update(data) + + self.log.info('Collected instance: {}'.format(file)) + self.log.info('Scene path: {}'.format(current_file)) + self.log.info('staging Dir: {}'.format(folder)) + self.log.info('subset: {}'.format(subset)) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py new file mode 100644 index 0000000000..904c1656da --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +""" +Export alembic file. + +Note: + Parameters on AlembicExport (AlembicExport.Parameter): + + ParticleAsMesh (bool): Sets whether particle shapes are exported + as meshes. + AnimTimeRange (enum): How animation is saved: + #CurrentFrame: saves current frame + #TimeSlider: saves the active time segments on time slider (default) + #StartEnd: saves a range specified by the Step + StartFrame (int) + EnFrame (int) + ShapeSuffix (bool): When set to true, appends the string "Shape" to the + name of each exported mesh. This property is set to false by default. + SamplesPerFrame (int): Sets the number of animation samples per frame. + Hidden (bool): When true, export hidden geometry. + UVs (bool): When true, export the mesh UV map channel. + Normals (bool): When true, export the mesh normals. + VertexColors (bool): When true, export the mesh vertex color map 0 and the + current vertex color display data when it differs + ExtraChannels (bool): When true, export the mesh extra map channels + (map channels greater than channel 1) + Velocity (bool): When true, export the meh vertex and particle velocity + data. + MaterialIDs (bool): When true, export the mesh material ID as + Alembic face sets. + Visibility (bool): When true, export the node visibility data. + LayerName (bool): When true, export the node layer name as an Alembic + object property. + MaterialName (bool): When true, export the geometry node material name as + an Alembic object property + ObjectID (bool): When true, export the geometry node g-buffer object ID as + an Alembic object property. + CustomAttributes (bool): When true, export the node and its modifiers + custom attributes into an Alembic object compound property. +""" +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractAlembic(publish.Extractor): + order = pyblish.api.ExtractorOrder + label = "Extract Pointcache" + hosts = ["max"] + families = ["pointcache", "camera"] + + def process(self, instance): + start = float(instance.data.get("frameStartHandle", 1)) + end = float(instance.data.get("frameEndHandle", 1)) + + container = instance.data["instance_node"] + + self.log.info("Extracting pointcache ...") + + parent_dir = self.staging_dir(instance) + file_name = "{name}.abc".format(**instance.data) + path = os.path.join(parent_dir, file_name) + + # We run the render + self.log.info("Writing alembic '%s' to '%s'" % (file_name, + parent_dir)) + + abc_export_cmd = ( + f""" +AlembicExport.ArchiveType = #ogawa +AlembicExport.CoordinateSystem = #maya +AlembicExport.StartFrame = {start} +AlembicExport.EndFrame = {end} + +exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport + + """) + + self.log.debug(f"Executing command: {abc_export_cmd}") + + with maintained_selection(): + # select and export + + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(abc_export_cmd) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': file_name, + "stagingDir": parent_dir, + } + instance.data["representations"].append(representation) diff --git a/openpype/hosts/max/plugins/publish/validate_scene_saved.py b/openpype/hosts/max/plugins/publish/validate_scene_saved.py new file mode 100644 index 0000000000..6392b12d11 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_scene_saved.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction +from pymxs import runtime as rt + + +class ValidateSceneSaved(pyblish.api.InstancePlugin): + """Validate that workfile was saved.""" + + order = pyblish.api.ValidatorOrder + families = ["workfile"] + hosts = ["max"] + label = "Validate Workfile is saved" + + def process(self, instance): + if not rt.maxFilePath or not rt.maxFileName: + raise PublishValidationError( + "Workfile is not saved", title=self.label) From d29a3ca4379a88202bc4279fe8966d87a3509820 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Dec 2022 01:17:21 +0100 Subject: [PATCH 5/9] :art: simple loader for alembics --- .../hosts/max/plugins/load/load_pointcache.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 openpype/hosts/max/plugins/load/load_pointcache.py diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py new file mode 100644 index 0000000000..150206b8b8 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Simple alembic loader for 3dsmax. + +Because of limited api, alembics can be only loaded, but not easily updated. + +""" +import os +from openpype.pipeline import ( + load, + get_representation_path, +) + + +class AbcLoader(load.LoaderPlugin): + """Alembic loader.""" + + families = ["model", "animation", "pointcache"] + label = "Load Alembic" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.fname) + + abc_before = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + abc_export_cmd = (f""" +AlembicImport.ImportToRoot = false + +importFile @"{file_path}" #noPrompt + """) + + self.log.debug(f"Executing command: {abc_export_cmd}") + rt.execute(abc_export_cmd) + + abc_after = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) + + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") + + abc_container = abc_containers.pop() + + container_name = f"{name}_CON" + container = rt.container(name=container_name) + abc_container.Parent = container + + return container + + def remove(self, container): + from pymxs import runtime as rt + + node = container["node"] + rt.delete(node) From 7327334226c45fc0291c3b08e041cb8fc7fa328b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Dec 2022 01:20:56 +0100 Subject: [PATCH 6/9] :rotating_light: fix :dog: --- openpype/hosts/max/api/__init__.py | 2 +- openpype/hosts/max/api/pipeline.py | 6 +----- openpype/hosts/max/plugins/publish/collect_workfile.py | 2 +- openpype/hosts/max/plugins/publish/validate_scene_saved.py | 3 +-- openpype/hosts/max/startup/startup.py | 1 - 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/__init__.py b/openpype/hosts/max/api/__init__.py index 26190dcfb8..92097cc98b 100644 --- a/openpype/hosts/max/api/__init__.py +++ b/openpype/hosts/max/api/__init__.py @@ -6,7 +6,7 @@ from .pipeline import ( ) -from .lib import( +from .lib import ( maintained_selection, lsattr, get_all_children diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 4f8271fb7e..f3cdf245fb 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -15,11 +15,7 @@ from openpype.pipeline import ( from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max import MAX_HOST_DIR -from openpype.pipeline.load import any_outdated_containers -from openpype.lib import ( - register_event_callback, - emit_event, -) + from pymxs import runtime as rt # noqa log = logging.getLogger("openpype.hosts.max") diff --git a/openpype/hosts/max/plugins/publish/collect_workfile.py b/openpype/hosts/max/plugins/publish/collect_workfile.py index 7112337575..3500b2735c 100644 --- a/openpype/hosts/max/plugins/publish/collect_workfile.py +++ b/openpype/hosts/max/plugins/publish/collect_workfile.py @@ -4,7 +4,7 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline import legacy_io class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/max/plugins/publish/validate_scene_saved.py b/openpype/hosts/max/plugins/publish/validate_scene_saved.py index 6392b12d11..8506b17315 100644 --- a/openpype/hosts/max/plugins/publish/validate_scene_saved.py +++ b/openpype/hosts/max/plugins/publish/validate_scene_saved.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import pyblish.api -from openpype.pipeline import PublishValidationError -from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError from pymxs import runtime as rt diff --git a/openpype/hosts/max/startup/startup.py b/openpype/hosts/max/startup/startup.py index afcbd2d132..37bcef5db1 100644 --- a/openpype/hosts/max/startup/startup.py +++ b/openpype/hosts/max/startup/startup.py @@ -4,4 +4,3 @@ from openpype.pipeline import install_host host = MaxHost() install_host(host) - From 75606777695064693dca411bd47455988a669c14 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Dec 2022 01:23:22 +0100 Subject: [PATCH 7/9] :rotating_light: fix hound round 2 --- openpype/hosts/max/plugins/create/create_pointcache.py | 3 ++- openpype/hosts/max/plugins/load/load_pointcache.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index 4c9ec7fb97..c08b0dedfe 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -18,4 +18,5 @@ class CreatePointCache(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance - instance_node = rt.getNodeByName(instance.get("instance_node")) + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 150206b8b8..285d84b7b6 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -6,8 +6,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os from openpype.pipeline import ( - load, - get_representation_path, + load ) From ad95165765bc0841305888af177888bfaf7d1357 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Dec 2022 01:25:15 +0100 Subject: [PATCH 8/9] :rotating_light: fix hound round 3 --- openpype/hosts/max/plugins/create/create_pointcache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index c08b0dedfe..32f0838471 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -11,9 +11,9 @@ class CreatePointCache(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt + # from pymxs import runtime as rt - instance = super(CreatePointCache, self).create( + _ = super(CreatePointCache, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance From f4391cbeb2245e132f561cbdc89b8aefc88b06cb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Dec 2022 01:39:28 +0100 Subject: [PATCH 9/9] :recycle: add 3dsmax 2023 variant --- .../system_settings/applications.json | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index a4db0dd327..b8aa8cec74 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -114,6 +114,35 @@ } } }, + "3dsmax": { + "enabled": true, + "label": "3ds max", + "icon": "{}/app_icons/3dsmax.png", + "host_name": "max", + "environment": { + "ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR": "{OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup" + }, + "variants": { + "2023": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\3ds Max 2023\\3dsmax.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": { + "3DSMAX_VERSION": "2023" + } + } + } + }, "flame": { "enabled": true, "label": "Flame", @@ -1309,35 +1338,6 @@ } } }, - "3dsmax": { - "enabled": true, - "label": "3ds max", - "icon": "{}/app_icons/3dsmax.png", - "host_name": "3dsmax", - "environment": { - - }, - "variants": { - "2023": { - "use_python_2": false, - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\3ds Max 2023\\3dsmax.exe" - ], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": { - "3DSMAX_VERSION": "2023" - } - } - } - }, "djvview": { "enabled": true, "label": "DJV View",