This repository has been archived by the owner on Sep 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/develop' into feature/OP-3924_im…
…plement-ass-extractor
- Loading branch information
Showing
23 changed files
with
929 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from .addon import ( | ||
MaxAddon, | ||
MAX_HOST_DIR, | ||
) | ||
|
||
|
||
__all__ = ( | ||
"MaxAddon", | ||
"MAX_HOST_DIR", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# -*- coding: utf-8 -*- | ||
"""Public API for 3dsmax""" | ||
|
||
from .pipeline import ( | ||
MaxHost, | ||
) | ||
|
||
|
||
from .lib import ( | ||
maintained_selection, | ||
lsattr, | ||
get_all_children | ||
) | ||
|
||
__all__ = [ | ||
"MaxHost", | ||
"maintained_selection", | ||
"lsattr", | ||
"get_all_children" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# -*- 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: | ||
node = rt.getNodeByName(node_name) | ||
if not node: | ||
return False | ||
|
||
for k, v in data.items(): | ||
if isinstance(v, (dict, list)): | ||
rt.setUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}') | ||
else: | ||
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) | ||
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: | ||
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"): | ||
try: | ||
key, value = line.split("=") | ||
except ValueError: | ||
# if the line cannot be split we can't really parse it | ||
continue | ||
|
||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# -*- coding: utf-8 -*- | ||
"""3dsmax menu definition of OpenPype.""" | ||
from Qt import QtWidgets, QtCore | ||
from pymxs import runtime as rt | ||
|
||
from openpype.tools.utils import host_tools | ||
|
||
|
||
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) -> 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: 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) | ||
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(help_action, op_menu) | ||
|
||
self.menu = op_menu | ||
return op_menu | ||
|
||
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) | ||
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) | ||
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) |
Oops, something went wrong.