diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py
index 15bd10fdb0..2d674b3fa7 100644
--- a/openpype/hosts/hiero/__init__.py
+++ b/openpype/hosts/hiero/__init__.py
@@ -18,6 +18,7 @@ def add_implementation_envs(env, _app):
new_hiero_paths.append(norm_path)
env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths)
+ env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
# Try to add QuickTime to PATH
quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem"
diff --git a/openpype/hosts/houdini/api/__init__.py b/openpype/hosts/houdini/api/__init__.py
index 7328236b97..e1500aa5f5 100644
--- a/openpype/hosts/houdini/api/__init__.py
+++ b/openpype/hosts/houdini/api/__init__.py
@@ -1,174 +1,60 @@
-import os
-import sys
-import logging
-import contextlib
+from .pipeline import (
+ install,
+ uninstall,
-import hou
-
-from pyblish import api as pyblish
-from avalon import api as avalon
-
-import openpype.hosts.houdini
-from openpype.hosts.houdini.api import lib
-
-from openpype.lib import (
- any_outdated
+ ls,
+ containerise,
)
-from .lib import get_asset_fps
+from .plugin import (
+ Creator,
+)
-log = logging.getLogger("openpype.hosts.houdini")
+from .workio import (
+ open_file,
+ save_file,
+ current_file,
+ has_unsaved_changes,
+ file_extensions,
+ work_root
+)
-HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.houdini.__file__))
-PLUGINS_DIR = os.path.join(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")
+from .lib import (
+ lsattr,
+ lsattrs,
+ read,
+
+ maintained_selection,
+ unique_name
+)
-def install():
+__all__ = [
+ "install",
+ "uninstall",
- pyblish.register_plugin_path(PUBLISH_PATH)
- avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
- avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
+ "ls",
+ "containerise",
- log.info("Installing callbacks ... ")
- # avalon.on("init", on_init)
- avalon.before("save", before_save)
- avalon.on("save", on_save)
- avalon.on("open", on_open)
- avalon.on("new", on_new)
+ "Creator",
- pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
+ # Workfiles API
+ "open_file",
+ "save_file",
+ "current_file",
+ "has_unsaved_changes",
+ "file_extensions",
+ "work_root",
- log.info("Setting default family states for loader..")
- avalon.data["familiesStateToggled"] = [
- "imagesequence",
- "review"
- ]
+ # Utility functions
+ "lsattr",
+ "lsattrs",
+ "read",
- # add houdini vendor packages
- hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor")
+ "maintained_selection",
+ "unique_name"
+]
- sys.path.append(hou_pythonpath)
-
- # Set asset FPS for the empty scene directly after launch of Houdini
- # so it initializes into the correct scene FPS
- _set_asset_fps()
-
-
-def before_save(*args):
- return lib.validate_fps()
-
-
-def on_save(*args):
-
- avalon.logger.info("Running callback on save..")
-
- nodes = lib.get_id_required_nodes()
- for node, new_id in lib.generate_ids(nodes):
- lib.set_id(node, new_id, overwrite=False)
-
-
-def on_open(*args):
-
- if not hou.isUIAvailable():
- log.debug("Batch mode detected, ignoring `on_open` callbacks..")
- return
-
- avalon.logger.info("Running callback on open..")
-
- # Validate FPS after update_task_from_path to
- # ensure it is using correct FPS for the asset
- lib.validate_fps()
-
- if any_outdated():
- from openpype.widgets import popup
-
- log.warning("Scene has outdated content.")
-
- # Get main window
- parent = hou.ui.mainQtWindow()
- if parent is None:
- log.info("Skipping outdated content pop-up "
- "because Houdini window can't be found.")
- else:
-
- # Show outdated pop-up
- def _on_show_inventory():
- import avalon.tools.sceneinventory as tool
- tool.show(parent=parent)
-
- dialog = popup.Popup(parent=parent)
- dialog.setWindowTitle("Houdini scene has outdated content")
- dialog.setMessage("There are outdated containers in "
- "your Houdini scene.")
- dialog.on_clicked.connect(_on_show_inventory)
- dialog.show()
-
-
-def on_new(_):
- """Set project resolution and fps when create a new file"""
- avalon.logger.info("Running callback on new..")
- _set_asset_fps()
-
-
-def _set_asset_fps():
- """Set Houdini scene FPS to the default required for current asset"""
-
- # Set new scene fps
- fps = get_asset_fps()
- print("Setting scene FPS to %i" % fps)
- lib.set_scene_fps(fps)
-
-
-def on_pyblish_instance_toggled(instance, new_value, old_value):
- """Toggle saver tool passthrough states on instance toggles."""
- @contextlib.contextmanager
- def main_take(no_update=True):
- """Enter root take during context"""
- original_take = hou.takes.currentTake()
- original_update_mode = hou.updateModeSetting()
- root = hou.takes.rootTake()
- has_changed = False
- try:
- if original_take != root:
- has_changed = True
- if no_update:
- hou.setUpdateMode(hou.updateMode.Manual)
- hou.takes.setCurrentTake(root)
- yield
- finally:
- if has_changed:
- if no_update:
- hou.setUpdateMode(original_update_mode)
- hou.takes.setCurrentTake(original_take)
-
- if not instance.data.get("_allowToggleBypass", True):
- return
-
- nodes = instance[:]
- if not nodes:
- return
-
- # Assume instance node is first node
- instance_node = nodes[0]
-
- if not hasattr(instance_node, "isBypassed"):
- # Likely not a node that can actually be bypassed
- log.debug("Can't bypass node: %s", instance_node.path())
- return
-
- if instance_node.isBypassed() != (not old_value):
- print("%s old bypass state didn't match old instance state, "
- "updating anyway.." % instance_node.path())
-
- try:
- # Go into the main take, because when in another take changing
- # the bypass state of a note cannot be done due to it being locked
- # by default.
- with main_take(no_update=True):
- instance_node.bypass(not new_value)
- except hou.PermissionError as exc:
- log.warning("%s - %s", instance_node.path(), exc)
+# Backwards API compatibility
+open = open_file
+save = save_file
diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index 53f0e59ea9..eaaba94ed5 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -2,9 +2,11 @@ import uuid
import logging
from contextlib import contextmanager
-from openpype.api import get_asset
+import six
+
from avalon import api, io
-from avalon.houdini import lib as houdini
+from openpype.api import get_asset
+
import hou
@@ -15,11 +17,11 @@ def get_asset_fps():
"""Return current asset fps."""
return get_asset()["data"].get("fps")
-def set_id(node, unique_id, overwrite=False):
+def set_id(node, unique_id, overwrite=False):
exists = node.parm("id")
if not exists:
- houdini.imprint(node, {"id": unique_id})
+ imprint(node, {"id": unique_id})
if not exists and overwrite:
node.setParm("id", unique_id)
@@ -342,3 +344,183 @@ def render_rop(ropnode):
import traceback
traceback.print_exc()
raise RuntimeError("Render failed: {0}".format(exc))
+
+
+def children_as_string(node):
+ return [c.name() for c in node.children()]
+
+
+def imprint(node, data):
+ """Store attributes with value on a node
+
+ Depending on the type of attribute it creates the correct parameter
+ template. Houdini uses a template per type, see the docs for more
+ information.
+
+ http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html
+
+ Args:
+ node(hou.Node): node object from Houdini
+ data(dict): collection of attributes and their value
+
+ Returns:
+ None
+
+ """
+
+ parm_group = node.parmTemplateGroup()
+
+ parm_folder = hou.FolderParmTemplate("folder", "Extra")
+ for key, value in data.items():
+ if value is None:
+ continue
+
+ if isinstance(value, float):
+ parm = hou.FloatParmTemplate(name=key,
+ label=key,
+ num_components=1,
+ default_value=(value,))
+ elif isinstance(value, bool):
+ parm = hou.ToggleParmTemplate(name=key,
+ label=key,
+ default_value=value)
+ elif isinstance(value, int):
+ parm = hou.IntParmTemplate(name=key,
+ label=key,
+ num_components=1,
+ default_value=(value,))
+ elif isinstance(value, six.string_types):
+ parm = hou.StringParmTemplate(name=key,
+ label=key,
+ num_components=1,
+ default_value=(value,))
+ else:
+ raise TypeError("Unsupported type: %r" % type(value))
+
+ parm_folder.addParmTemplate(parm)
+
+ parm_group.append(parm_folder)
+ node.setParmTemplateGroup(parm_group)
+
+
+def lsattr(attr, value=None):
+ if value is None:
+ nodes = list(hou.node("/obj").allNodes())
+ return [n for n in nodes if n.parm(attr)]
+ return lsattrs({attr: value})
+
+
+def lsattrs(attrs):
+ """Return nodes matching `key` and `value`
+
+ Arguments:
+ attrs (dict): collection of attribute: value
+
+ Example:
+ >> lsattrs({"id": "myId"})
+ ["myNode"]
+ >> lsattr("id")
+ ["myNode", "myOtherNode"]
+
+ Returns:
+ list
+ """
+
+ matches = set()
+ nodes = list(hou.node("/obj").allNodes()) # returns generator object
+ for node in nodes:
+ for attr in attrs:
+ if not node.parm(attr):
+ continue
+ elif node.evalParm(attr) != attrs[attr]:
+ continue
+ else:
+ matches.add(node)
+
+ return list(matches)
+
+
+def read(node):
+ """Read the container data in to a dict
+
+ Args:
+ node(hou.Node): Houdini node
+
+ Returns:
+ dict
+
+ """
+ # `spareParms` returns a tuple of hou.Parm objects
+ return {parameter.name(): parameter.eval() for
+ parameter in node.spareParms()}
+
+
+def unique_name(name, format="%03d", namespace="", prefix="", suffix="",
+ separator="_"):
+ """Return unique `name`
+
+ The function takes into consideration an optional `namespace`
+ and `suffix`. The suffix is included in evaluating whether a
+ name exists - such as `name` + "_GRP" - but isn't included
+ in the returned value.
+
+ If a namespace is provided, only names within that namespace
+ are considered when evaluating whether the name is unique.
+
+ Arguments:
+ format (str, optional): The `name` is given a number, this determines
+ how this number is formatted. Defaults to a padding of 2.
+ E.g. my_name01, my_name02.
+ namespace (str, optional): Only consider names within this namespace.
+ suffix (str, optional): Only consider names with this suffix.
+
+ Example:
+ >>> name = hou.node("/obj").createNode("geo", name="MyName")
+ >>> assert hou.node("/obj/MyName")
+ True
+ >>> unique = unique_name(name)
+ >>> assert hou.node("/obj/{}".format(unique))
+ False
+
+ """
+
+ iteration = 1
+
+ parts = [prefix, name, format % iteration, suffix]
+ if namespace:
+ parts.insert(0, namespace)
+
+ unique = separator.join(parts)
+ children = children_as_string(hou.node("/obj"))
+ while unique in children:
+ iteration += 1
+ unique = separator.join(parts)
+
+ if suffix:
+ return unique[:-len(suffix)]
+
+ return unique
+
+
+@contextmanager
+def maintained_selection():
+ """Maintain selection during context
+ Example:
+ >>> with maintained_selection():
+ ... # Modify selection
+ ... node.setSelected(on=False, clear_all_selected=True)
+ >>> # Selection restored
+ """
+
+ previous_selection = hou.selectedNodes()
+ try:
+ yield
+ finally:
+ # Clear the selection
+ # todo: does hou.clearAllSelected() do the same?
+ for node in hou.selectedNodes():
+ node.setSelected(on=False)
+
+ if previous_selection:
+ for node in previous_selection:
+ node.setSelected(on=True)
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
new file mode 100644
index 0000000000..4054d5991f
--- /dev/null
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -0,0 +1,349 @@
+import os
+import sys
+import logging
+import contextlib
+
+import hou
+
+import pyblish.api
+import avalon.api
+from avalon.pipeline import AVALON_CONTAINER_ID
+from avalon.lib import find_submodule
+
+import openpype.hosts.houdini
+from openpype.hosts.houdini.api import lib
+
+from openpype.lib import (
+ any_outdated
+)
+
+from .lib import get_asset_fps
+
+log = logging.getLogger("openpype.hosts.houdini")
+
+AVALON_CONTAINERS = "/obj/AVALON_CONTAINERS"
+IS_HEADLESS = not hasattr(hou, "ui")
+
+HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.houdini.__file__))
+PLUGINS_DIR = os.path.join(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")
+
+
+self = sys.modules[__name__]
+self._has_been_setup = False
+self._parent = None
+self._events = dict()
+
+
+def install():
+ _register_callbacks()
+
+ pyblish.api.register_host("houdini")
+ pyblish.api.register_host("hython")
+ pyblish.api.register_host("hpython")
+
+ pyblish.api.register_plugin_path(PUBLISH_PATH)
+ avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
+
+ log.info("Installing callbacks ... ")
+ # avalon.on("init", on_init)
+ avalon.api.before("save", before_save)
+ avalon.api.on("save", on_save)
+ avalon.api.on("open", on_open)
+ avalon.api.on("new", on_new)
+
+ pyblish.api.register_callback(
+ "instanceToggled", on_pyblish_instance_toggled
+ )
+
+ log.info("Setting default family states for loader..")
+ avalon.api.data["familiesStateToggled"] = [
+ "imagesequence",
+ "review"
+ ]
+
+ self._has_been_setup = True
+ # add houdini vendor packages
+ hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor")
+
+ sys.path.append(hou_pythonpath)
+
+ # Set asset FPS for the empty scene directly after launch of Houdini
+ # so it initializes into the correct scene FPS
+ _set_asset_fps()
+
+
+def uninstall():
+ """Uninstall Houdini-specific functionality of avalon-core.
+
+ This function is called automatically on calling `api.uninstall()`.
+ """
+
+ pyblish.api.deregister_host("hython")
+ pyblish.api.deregister_host("hpython")
+ pyblish.api.deregister_host("houdini")
+
+
+def _register_callbacks():
+ for event in self._events.copy().values():
+ if event is None:
+ continue
+
+ try:
+ hou.hipFile.removeEventCallback(event)
+ except RuntimeError as e:
+ log.info(e)
+
+ self._events[on_file_event_callback] = hou.hipFile.addEventCallback(
+ on_file_event_callback
+ )
+
+
+def on_file_event_callback(event):
+ if event == hou.hipFileEventType.AfterLoad:
+ avalon.api.emit("open", [event])
+ elif event == hou.hipFileEventType.AfterSave:
+ avalon.api.emit("save", [event])
+ elif event == hou.hipFileEventType.BeforeSave:
+ avalon.api.emit("before_save", [event])
+ elif event == hou.hipFileEventType.AfterClear:
+ avalon.api.emit("new", [event])
+
+
+def get_main_window():
+ """Acquire Houdini's main window"""
+ if self._parent is None:
+ self._parent = hou.ui.mainQtWindow()
+ return self._parent
+
+
+def teardown():
+ """Remove integration"""
+ if not self._has_been_setup:
+ return
+
+ self._has_been_setup = False
+ print("pyblish: Integration torn down successfully")
+
+
+def containerise(name,
+ namespace,
+ nodes,
+ context,
+ loader=None,
+ suffix=""):
+ """Bundle `nodes` into a subnet and imprint it with metadata
+
+ Containerisation enables a tracking of version, author and origin
+ for loaded assets.
+
+ Arguments:
+ name (str): Name of resulting assembly
+ namespace (str): Namespace under which to host container
+ nodes (list): Long names of nodes to containerise
+ context (dict): Asset information
+ loader (str, optional): Name of loader used to produce this container.
+ suffix (str, optional): Suffix of container, defaults to `_CON`.
+
+ Returns:
+ container (str): Name of container assembly
+
+ """
+
+ # Ensure AVALON_CONTAINERS subnet exists
+ subnet = hou.node(AVALON_CONTAINERS)
+ if subnet is None:
+ obj_network = hou.node("/obj")
+ subnet = obj_network.createNode("subnet",
+ node_name="AVALON_CONTAINERS")
+
+ # Create proper container name
+ container_name = "{}_{}".format(name, suffix or "CON")
+ container = hou.node("/obj/{}".format(name))
+ container.setName(container_name, unique_name=True)
+
+ data = {
+ "schema": "openpype:container-2.0",
+ "id": AVALON_CONTAINER_ID,
+ "name": name,
+ "namespace": namespace,
+ "loader": str(loader),
+ "representation": str(context["representation"]["_id"]),
+ }
+
+ lib.imprint(container, data)
+
+ # "Parent" the container under the container network
+ hou.moveNodesTo([container], subnet)
+
+ subnet.node(container_name).moveToGoodPosition()
+
+ return container
+
+
+def parse_container(container):
+ """Return the container node's full container data.
+
+ Args:
+ container (hou.Node): A container node name.
+
+ Returns:
+ dict: The container schema data for this container node.
+
+ """
+ data = lib.read(container)
+
+ # Backwards compatibility pre-schemas for containers
+ data["schema"] = data.get("schema", "openpype:container-1.0")
+
+ # Append transient data
+ data["objectName"] = container.path()
+ data["node"] = container
+
+ return data
+
+
+def ls():
+ containers = []
+ for identifier in (AVALON_CONTAINER_ID,
+ "pyblish.mindbender.container"):
+ containers += lib.lsattr("id", identifier)
+
+ has_metadata_collector = False
+ config_host = find_submodule(avalon.api.registered_config(), "houdini")
+ if hasattr(config_host, "collect_container_metadata"):
+ has_metadata_collector = True
+
+ for container in sorted(containers,
+ # Hou 19+ Python 3 hou.ObjNode are not
+ # sortable due to not supporting greater
+ # than comparisons
+ key=lambda node: node.path()):
+ data = parse_container(container)
+
+ # Collect custom data if attribute is present
+ if has_metadata_collector:
+ metadata = config_host.collect_container_metadata(container)
+ data.update(metadata)
+
+ yield data
+
+
+def before_save(*args):
+ return lib.validate_fps()
+
+
+def on_save(*args):
+
+ log.info("Running callback on save..")
+
+ nodes = lib.get_id_required_nodes()
+ for node, new_id in lib.generate_ids(nodes):
+ lib.set_id(node, new_id, overwrite=False)
+
+
+def on_open(*args):
+
+ if not hou.isUIAvailable():
+ log.debug("Batch mode detected, ignoring `on_open` callbacks..")
+ return
+
+ log.info("Running callback on open..")
+
+ # Validate FPS after update_task_from_path to
+ # ensure it is using correct FPS for the asset
+ lib.validate_fps()
+
+ if any_outdated():
+ from openpype.widgets import popup
+
+ log.warning("Scene has outdated content.")
+
+ # Get main window
+ parent = get_main_window()
+ if parent is None:
+ log.info("Skipping outdated content pop-up "
+ "because Houdini window can't be found.")
+ else:
+
+ # Show outdated pop-up
+ def _on_show_inventory():
+ from openpype.tools.utils import host_tools
+ host_tools.show_scene_inventory(parent=parent)
+
+ dialog = popup.Popup(parent=parent)
+ dialog.setWindowTitle("Houdini scene has outdated content")
+ dialog.setMessage("There are outdated containers in "
+ "your Houdini scene.")
+ dialog.on_clicked.connect(_on_show_inventory)
+ dialog.show()
+
+
+def on_new(_):
+ """Set project resolution and fps when create a new file"""
+ log.info("Running callback on new..")
+ _set_asset_fps()
+
+
+def _set_asset_fps():
+ """Set Houdini scene FPS to the default required for current asset"""
+
+ # Set new scene fps
+ fps = get_asset_fps()
+ print("Setting scene FPS to %i" % fps)
+ lib.set_scene_fps(fps)
+
+
+def on_pyblish_instance_toggled(instance, new_value, old_value):
+ """Toggle saver tool passthrough states on instance toggles."""
+ @contextlib.contextmanager
+ def main_take(no_update=True):
+ """Enter root take during context"""
+ original_take = hou.takes.currentTake()
+ original_update_mode = hou.updateModeSetting()
+ root = hou.takes.rootTake()
+ has_changed = False
+ try:
+ if original_take != root:
+ has_changed = True
+ if no_update:
+ hou.setUpdateMode(hou.updateMode.Manual)
+ hou.takes.setCurrentTake(root)
+ yield
+ finally:
+ if has_changed:
+ if no_update:
+ hou.setUpdateMode(original_update_mode)
+ hou.takes.setCurrentTake(original_take)
+
+ if not instance.data.get("_allowToggleBypass", True):
+ return
+
+ nodes = instance[:]
+ if not nodes:
+ return
+
+ # Assume instance node is first node
+ instance_node = nodes[0]
+
+ if not hasattr(instance_node, "isBypassed"):
+ # Likely not a node that can actually be bypassed
+ log.debug("Can't bypass node: %s", instance_node.path())
+ return
+
+ if instance_node.isBypassed() != (not old_value):
+ print("%s old bypass state didn't match old instance state, "
+ "updating anyway.." % instance_node.path())
+
+ try:
+ # Go into the main take, because when in another take changing
+ # the bypass state of a note cannot be done due to it being locked
+ # by default.
+ with main_take(no_update=True):
+ instance_node.bypass(not new_value)
+ except hou.PermissionError as exc:
+ log.warning("%s - %s", instance_node.path(), exc)
diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py
index 63d9bba470..e64b505d2c 100644
--- a/openpype/hosts/houdini/api/plugin.py
+++ b/openpype/hosts/houdini/api/plugin.py
@@ -1,25 +1,82 @@
# -*- coding: utf-8 -*-
"""Houdini specific Avalon/Pyblish plugin definitions."""
import sys
-from avalon.api import CreatorError
-from avalon import houdini
import six
+import avalon.api
+from avalon.api import CreatorError
import hou
from openpype.api import PypeCreatorMixin
+from .lib import imprint
class OpenPypeCreatorError(CreatorError):
pass
-class Creator(PypeCreatorMixin, houdini.Creator):
+class Creator(PypeCreatorMixin, avalon.api.Creator):
+ """Creator plugin to create instances in Houdini
+
+ To support the wide range of node types for render output (Alembic, VDB,
+ Mantra) the Creator needs a node type to create the correct instance
+
+ By default, if none is given, is `geometry`. An example of accepted node
+ types: geometry, alembic, ifd (mantra)
+
+ Please check the Houdini documentation for more node types.
+
+ Tip: to find the exact node type to create press the `i` left of the node
+ when hovering over a node. The information is visible under the name of
+ the node.
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(Creator, self).__init__(*args, **kwargs)
+ self.nodes = list()
+
def process(self):
+ """This is the base functionality to create instances in Houdini
+
+ The selected nodes are stored in self to be used in an override method.
+ This is currently necessary in order to support the multiple output
+ types in Houdini which can only be rendered through their own node.
+
+ Default node type if none is given is `geometry`
+
+ It also makes it easier to apply custom settings per instance type
+
+ Example of override method for Alembic:
+
+ def process(self):
+ instance = super(CreateEpicNode, self, process()
+ # Set paramaters for Alembic node
+ instance.setParms(
+ {"sop_path": "$HIP/%s.abc" % self.nodes[0]}
+ )
+
+ Returns:
+ hou.Node
+
+ """
try:
- # re-raise as standard Python exception so
- # Avalon can catch it
- instance = super(Creator, self).process()
+ if (self.options or {}).get("useSelection"):
+ self.nodes = hou.selectedNodes()
+
+ # Get the node type and remove it from the data, not needed
+ node_type = self.data.pop("node_type", None)
+ if node_type is None:
+ node_type = "geometry"
+
+ # Get out node
+ out = hou.node("/out")
+ instance = out.createNode(node_type, node_name=self.name)
+ instance.moveToGoodPosition()
+
+ imprint(instance, self.data)
+
self._process(instance)
+
except hou.Error as er:
six.reraise(
OpenPypeCreatorError,
diff --git a/openpype/hosts/houdini/api/workio.py b/openpype/hosts/houdini/api/workio.py
new file mode 100644
index 0000000000..e7310163ea
--- /dev/null
+++ b/openpype/hosts/houdini/api/workio.py
@@ -0,0 +1,58 @@
+"""Host API required Work Files tool"""
+import os
+
+import hou
+from avalon import api
+
+
+def file_extensions():
+ return api.HOST_WORKFILE_EXTENSIONS["houdini"]
+
+
+def has_unsaved_changes():
+ return hou.hipFile.hasUnsavedChanges()
+
+
+def save_file(filepath):
+
+ # Force forwards slashes to avoid segfault
+ filepath = filepath.replace("\\", "/")
+
+ hou.hipFile.save(file_name=filepath,
+ save_to_recent_files=True)
+
+ return filepath
+
+
+def open_file(filepath):
+
+ # Force forwards slashes to avoid segfault
+ filepath = filepath.replace("\\", "/")
+
+ hou.hipFile.load(filepath,
+ suppress_save_prompt=True,
+ ignore_load_warnings=False)
+
+ return filepath
+
+
+def current_file():
+
+ current_filepath = hou.hipFile.path()
+ if (os.path.basename(current_filepath) == "untitled.hip" and
+ not os.path.exists(current_filepath)):
+ # By default a new scene in houdini is saved in the current
+ # working directory as "untitled.hip" so we need to capture
+ # that and consider it 'not saved' when it's in that state.
+ return None
+
+ return current_filepath
+
+
+def work_root(session):
+ work_dir = session["AVALON_WORKDIR"]
+ scene_dir = session.get("AVALON_SCENEDIR")
+ if scene_dir:
+ return os.path.join(work_dir, scene_dir)
+ else:
+ return work_dir
diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py
index 459da8bfdf..0a9c1bad1e 100644
--- a/openpype/hosts/houdini/plugins/create/create_hda.py
+++ b/openpype/hosts/houdini/plugins/create/create_hda.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-from openpype.hosts.houdini.api import plugin
-from avalon.houdini import lib
-from avalon import io
import hou
+from avalon import io
+from openpype.hosts.houdini.api import lib
+from openpype.hosts.houdini.api import plugin
class CreateHDA(plugin.Creator):
diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py
index df66d56008..eaab81f396 100644
--- a/openpype/hosts/houdini/plugins/load/load_alembic.py
+++ b/openpype/hosts/houdini/plugins/load/load_alembic.py
@@ -1,6 +1,7 @@
+import os
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class AbcLoader(api.Loader):
@@ -14,8 +15,6 @@ class AbcLoader(api.Loader):
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
-
- import os
import hou
# Format file name, Houdini only wants forward slashes
diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py
index 8b98b7c05e..8916d3b9b7 100644
--- a/openpype/hosts/houdini/plugins/load/load_camera.py
+++ b/openpype/hosts/houdini/plugins/load/load_camera.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")'
diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py
index 6610d5e513..f5f2fb7481 100644
--- a/openpype/hosts/houdini/plugins/load/load_hda.py
+++ b/openpype/hosts/houdini/plugins/load/load_hda.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class HdaLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py
index 4ff2777d77..39f583677b 100644
--- a/openpype/hosts/houdini/plugins/load/load_image.py
+++ b/openpype/hosts/houdini/plugins/load/load_image.py
@@ -1,7 +1,7 @@
import os
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
import hou
diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py
index 7483101409..232ce59479 100644
--- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py
+++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
class USDSublayerLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py
index cab3cb5269..224bfc2d61 100644
--- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py
+++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
class USDReferenceLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py
index 5f7e400b39..40aa7a1d18 100644
--- a/openpype/hosts/houdini/plugins/load/load_vdb.py
+++ b/openpype/hosts/houdini/plugins/load/load_vdb.py
@@ -2,7 +2,7 @@ import os
import re
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class VdbLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py
index 12d118f0cc..d38927984a 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py
@@ -2,7 +2,7 @@ import hou
import pyblish.api
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
class CollectInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
index 7df5e8b6f2..0600730d00 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
@@ -1,6 +1,6 @@
import hou
import pyblish.api
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
import openpype.hosts.houdini.api.usd as hou_usdlib
import openpype.lib.usdlib as usdlib
diff --git a/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py b/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
index 3ae16efe56..c635a53074 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
@@ -2,7 +2,7 @@ import pyblish.api
import openpype.api
import hou
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
class CollectRemotePublishSettings(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index c34310cf72..bc4a2e809a 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -56,18 +56,6 @@ host_tools.show_workfiles(parent)
]]>
-
-
-
-
-
-
-
-
-
-
diff --git a/openpype/hosts/houdini/startup/scripts/123.py b/openpype/hosts/houdini/startup/scripts/123.py
index 4233d68c15..eb33b49759 100644
--- a/openpype/hosts/houdini/startup/scripts/123.py
+++ b/openpype/hosts/houdini/startup/scripts/123.py
@@ -1,9 +1,10 @@
-from avalon import api, houdini
+import avalon.api
+from openpype.hosts.houdini import api
def main():
print("Installing OpenPype ...")
- api.install(houdini)
+ avalon.api.install(api)
main()
diff --git a/openpype/hosts/houdini/startup/scripts/houdinicore.py b/openpype/hosts/houdini/startup/scripts/houdinicore.py
index 4233d68c15..eb33b49759 100644
--- a/openpype/hosts/houdini/startup/scripts/houdinicore.py
+++ b/openpype/hosts/houdini/startup/scripts/houdinicore.py
@@ -1,9 +1,10 @@
-from avalon import api, houdini
+import avalon.api
+from openpype.hosts.houdini import api
def main():
print("Installing OpenPype ...")
- api.install(houdini)
+ avalon.api.install(api)
main()
diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py
index 549f100007..b7d26a7818 100644
--- a/openpype/hosts/maya/__init__.py
+++ b/openpype/hosts/maya/__init__.py
@@ -5,9 +5,7 @@ def add_implementation_envs(env, _app):
# Add requirements to PYTHONPATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_python_paths = [
- os.path.join(pype_root, "openpype", "hosts", "maya", "startup"),
- os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"),
- os.path.join(pype_root, "tools", "mayalookassigner")
+ os.path.join(pype_root, "openpype", "hosts", "maya", "startup")
]
old_python_path = env.get("PYTHONPATH") or ""
for path in old_python_path.split(os.pathsep):
diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py
index 0ad1c8ba29..9ea798e927 100644
--- a/openpype/hosts/maya/api/__init__.py
+++ b/openpype/hosts/maya/api/__init__.py
@@ -1,233 +1,91 @@
-import os
-import logging
-import weakref
+"""Public API
-from maya import utils, cmds
+Anything that isn't defined here is INTERNAL and unreliable for external use.
-from avalon import api as avalon
-from avalon import pipeline
-from avalon.maya import suspended_refresh
-from avalon.maya.pipeline import IS_HEADLESS
-from openpype.tools.utils import host_tools
-from pyblish import api as pyblish
-from openpype.lib import any_outdated
-import openpype.hosts.maya
-from openpype.hosts.maya.lib import copy_workspace_mel
-from openpype.lib.path_tools import HostDirmap
-from . import menu, lib
+"""
-log = logging.getLogger("openpype.hosts.maya")
+from .pipeline import (
+ install,
+ uninstall,
-HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.maya.__file__))
-PLUGINS_DIR = os.path.join(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")
+ ls,
+ containerise,
+
+ lock,
+ unlock,
+ is_locked,
+ lock_ignored,
+
+)
+from .plugin import (
+ Creator,
+ Loader
+)
+
+from .workio import (
+ open_file,
+ save_file,
+ current_file,
+ has_unsaved_changes,
+ file_extensions,
+ work_root
+)
+
+from .lib import (
+ export_alembic,
+ lsattr,
+ lsattrs,
+ read,
+
+ apply_shaders,
+ without_extension,
+ maintained_selection,
+ suspended_refresh,
+
+ unique_name,
+ unique_namespace,
+)
-def install():
- from openpype.settings import get_project_settings
+__all__ = [
+ "install",
+ "uninstall",
- project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
- # process path mapping
- dirmap_processor = MayaDirmap("maya", project_settings)
- dirmap_processor.process_dirmap()
+ "ls",
+ "containerise",
- pyblish.register_plugin_path(PUBLISH_PATH)
- avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
- avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
- avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
- log.info(PUBLISH_PATH)
- menu.install()
+ "lock",
+ "unlock",
+ "is_locked",
+ "lock_ignored",
- log.info("Installing callbacks ... ")
- avalon.on("init", on_init)
+ "Creator",
+ "Loader",
- # Callbacks below are not required for headless mode, the `init` however
- # is important to load referenced Alembics correctly at rendertime.
- if IS_HEADLESS:
- log.info("Running in headless mode, skipping Maya "
- "save/open/new callback installation..")
- return
+ # Workfiles API
+ "open_file",
+ "save_file",
+ "current_file",
+ "has_unsaved_changes",
+ "file_extensions",
+ "work_root",
- avalon.on("save", on_save)
- avalon.on("open", on_open)
- avalon.on("new", on_new)
- avalon.before("save", on_before_save)
- avalon.on("taskChanged", on_task_changed)
- avalon.on("before.workfile.save", before_workfile_save)
+ # Utility functions
+ "export_alembic",
+ "lsattr",
+ "lsattrs",
+ "read",
- log.info("Setting default family states for loader..")
- avalon.data["familiesStateToggled"] = ["imagesequence"]
+ "unique_name",
+ "unique_namespace",
+ "apply_shaders",
+ "without_extension",
+ "maintained_selection",
+ "suspended_refresh",
-def uninstall():
- pyblish.deregister_plugin_path(PUBLISH_PATH)
- avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
- avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
- avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
+]
- menu.uninstall()
-
-
-def on_init(_):
- avalon.logger.info("Running callback on init..")
-
- def safe_deferred(fn):
- """Execute deferred the function in a try-except"""
-
- def _fn():
- """safely call in deferred callback"""
- try:
- fn()
- except Exception as exc:
- print(exc)
-
- try:
- utils.executeDeferred(_fn)
- except Exception as exc:
- print(exc)
-
- # Force load Alembic so referenced alembics
- # work correctly on scene open
- cmds.loadPlugin("AbcImport", quiet=True)
- cmds.loadPlugin("AbcExport", quiet=True)
-
- # Force load objExport plug-in (requested by artists)
- cmds.loadPlugin("objExport", quiet=True)
-
- from .customize import (
- override_component_mask_commands,
- override_toolbox_ui
- )
- safe_deferred(override_component_mask_commands)
-
- launch_workfiles = os.environ.get("WORKFILES_STARTUP")
-
- if launch_workfiles:
- safe_deferred(host_tools.show_workfiles)
-
- if not IS_HEADLESS:
- safe_deferred(override_toolbox_ui)
-
-
-def on_before_save(return_code, _):
- """Run validation for scene's FPS prior to saving"""
- return lib.validate_fps()
-
-
-def on_save(_):
- """Automatically add IDs to new nodes
-
- Any transform of a mesh, without an existing ID, is given one
- automatically on file save.
- """
-
- avalon.logger.info("Running callback on save..")
-
- # # Update current task for the current scene
- # update_task_from_path(cmds.file(query=True, sceneName=True))
-
- # Generate ids of the current context on nodes in the scene
- nodes = lib.get_id_required_nodes(referenced_nodes=False)
- for node, new_id in lib.generate_ids(nodes):
- lib.set_id(node, new_id, overwrite=False)
-
-
-def on_open(_):
- """On scene open let's assume the containers have changed."""
-
- from Qt import QtWidgets
- from openpype.widgets import popup
-
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.remove_render_layer_observer()")
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.add_render_layer_observer()")
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.add_render_layer_change_observer()")
- # # Update current task for the current scene
- # update_task_from_path(cmds.file(query=True, sceneName=True))
-
- # Validate FPS after update_task_from_path to
- # ensure it is using correct FPS for the asset
- lib.validate_fps()
- lib.fix_incompatible_containers()
-
- if any_outdated():
- log.warning("Scene has outdated content.")
-
- # Find maya main window
- top_level_widgets = {w.objectName(): w for w in
- QtWidgets.QApplication.topLevelWidgets()}
- parent = top_level_widgets.get("MayaWindow", None)
-
- if parent is None:
- log.info("Skipping outdated content pop-up "
- "because Maya window can't be found.")
- else:
-
- # Show outdated pop-up
- def _on_show_inventory():
- host_tools.show_scene_inventory(parent=parent)
-
- dialog = popup.Popup(parent=parent)
- dialog.setWindowTitle("Maya scene has outdated content")
- dialog.setMessage("There are outdated containers in "
- "your Maya scene.")
- dialog.on_show.connect(_on_show_inventory)
- dialog.show()
-
-
-def on_new(_):
- """Set project resolution and fps when create a new file"""
- avalon.logger.info("Running callback on new..")
- with suspended_refresh():
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.remove_render_layer_observer()")
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.add_render_layer_observer()")
- cmds.evalDeferred(
- "from openpype.hosts.maya.api import lib;"
- "lib.add_render_layer_change_observer()")
- lib.set_context_settings()
-
-
-def on_task_changed(*args):
- """Wrapped function of app initialize and maya's on task changed"""
- # Run
- with suspended_refresh():
- lib.set_context_settings()
- lib.update_content_on_context_change()
-
- msg = " project: {}\n asset: {}\n task:{}".format(
- avalon.Session["AVALON_PROJECT"],
- avalon.Session["AVALON_ASSET"],
- avalon.Session["AVALON_TASK"]
- )
-
- lib.show_message(
- "Context was changed",
- ("Context was changed to:\n{}".format(msg)),
- )
-
-
-def before_workfile_save(event):
- workdir_path = event.workdir_path
- if workdir_path:
- copy_workspace_mel(workdir_path)
-
-
-class MayaDirmap(HostDirmap):
- def on_enable_dirmap(self):
- cmds.dirmap(en=True)
-
- def dirmap_routine(self, source_path, destination_path):
- cmds.dirmap(m=(source_path, destination_path))
- cmds.dirmap(m=(destination_path, source_path))
+# Backwards API compatibility
+open = open_file
+save = save_file
diff --git a/openpype/hosts/maya/api/action.py b/openpype/hosts/maya/api/action.py
index a98b906d8c..ab26748c8a 100644
--- a/openpype/hosts/maya/api/action.py
+++ b/openpype/hosts/maya/api/action.py
@@ -2,7 +2,7 @@
from __future__ import absolute_import
import pyblish.api
-
+from avalon import io
from openpype.api import get_errored_instances_from_context
@@ -72,8 +72,7 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action):
nodes (list): all nodes to regenerate ids on
"""
- from openpype.hosts.maya.api import lib
- import avalon.io as io
+ from . import lib
asset = instance.data['asset']
asset_id = io.find_one({"name": asset, "type": "asset"},
diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py
index d4c2b6a225..c774afcc12 100644
--- a/openpype/hosts/maya/api/commands.py
+++ b/openpype/hosts/maya/api/commands.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
"""OpenPype script commands to be used directly in Maya."""
+from maya import cmds
+from avalon import api, io
class ToolWindows:
@@ -51,3 +53,134 @@ def edit_shader_definitions():
window = ShaderDefinitionsEditor(parent=main_window)
ToolWindows.set_window("shader_definition_editor", window)
window.show()
+
+
+def reset_frame_range():
+ """Set frame range to current asset"""
+ # Set FPS first
+ fps = {15: 'game',
+ 24: 'film',
+ 25: 'pal',
+ 30: 'ntsc',
+ 48: 'show',
+ 50: 'palf',
+ 60: 'ntscf',
+ 23.98: '23.976fps',
+ 23.976: '23.976fps',
+ 29.97: '29.97fps',
+ 47.952: '47.952fps',
+ 47.95: '47.952fps',
+ 59.94: '59.94fps',
+ 44100: '44100fps',
+ 48000: '48000fps'
+ }.get(float(api.Session.get("AVALON_FPS", 25)), "pal")
+
+ cmds.currentUnit(time=fps)
+
+ # Set frame start/end
+ asset_name = api.Session["AVALON_ASSET"]
+ asset = io.find_one({"name": asset_name, "type": "asset"})
+
+ frame_start = asset["data"].get("frameStart")
+ frame_end = asset["data"].get("frameEnd")
+ # Backwards compatibility
+ if frame_start is None or frame_end is None:
+ frame_start = asset["data"].get("edit_in")
+ frame_end = asset["data"].get("edit_out")
+
+ if frame_start is None or frame_end is None:
+ cmds.warning("No edit information found for %s" % asset_name)
+ return
+
+ handles = asset["data"].get("handles") or 0
+ handle_start = asset["data"].get("handleStart")
+ if handle_start is None:
+ handle_start = handles
+
+ handle_end = asset["data"].get("handleEnd")
+ if handle_end is None:
+ handle_end = handles
+
+ frame_start -= int(handle_start)
+ frame_end += int(handle_end)
+
+ cmds.playbackOptions(minTime=frame_start)
+ cmds.playbackOptions(maxTime=frame_end)
+ cmds.playbackOptions(animationStartTime=frame_start)
+ cmds.playbackOptions(animationEndTime=frame_end)
+ cmds.playbackOptions(minTime=frame_start)
+ cmds.playbackOptions(maxTime=frame_end)
+ cmds.currentTime(frame_start)
+
+ cmds.setAttr("defaultRenderGlobals.startFrame", frame_start)
+ cmds.setAttr("defaultRenderGlobals.endFrame", frame_end)
+
+
+def _resolution_from_document(doc):
+ if not doc or "data" not in doc:
+ print("Entered document is not valid. \"{}\"".format(str(doc)))
+ return None
+
+ resolution_width = doc["data"].get("resolutionWidth")
+ resolution_height = doc["data"].get("resolutionHeight")
+ # Backwards compatibility
+ if resolution_width is None or resolution_height is None:
+ resolution_width = doc["data"].get("resolution_width")
+ resolution_height = doc["data"].get("resolution_height")
+
+ # Make sure both width and heigh are set
+ if resolution_width is None or resolution_height is None:
+ cmds.warning(
+ "No resolution information found for \"{}\"".format(doc["name"])
+ )
+ return None
+
+ return int(resolution_width), int(resolution_height)
+
+
+def reset_resolution():
+ # Default values
+ resolution_width = 1920
+ resolution_height = 1080
+
+ # Get resolution from asset
+ asset_name = api.Session["AVALON_ASSET"]
+ asset_doc = io.find_one({"name": asset_name, "type": "asset"})
+ resolution = _resolution_from_document(asset_doc)
+ # Try get resolution from project
+ if resolution is None:
+ # TODO go through visualParents
+ print((
+ "Asset \"{}\" does not have set resolution."
+ " Trying to get resolution from project"
+ ).format(asset_name))
+ project_doc = io.find_one({"type": "project"})
+ resolution = _resolution_from_document(project_doc)
+
+ if resolution is None:
+ msg = "Using default resolution {}x{}"
+ else:
+ resolution_width, resolution_height = resolution
+ msg = "Setting resolution to {}x{}"
+
+ print(msg.format(resolution_width, resolution_height))
+
+ # set for different renderers
+ # arnold, vray, redshift, renderman
+
+ renderer = cmds.getAttr("defaultRenderGlobals.currentRenderer").lower()
+ # handle various renderman names
+ if renderer.startswith("renderman"):
+ renderer = "renderman"
+
+ # default attributes are usable for Arnold, Renderman and Redshift
+ width_attr_name = "defaultResolution.width"
+ height_attr_name = "defaultResolution.height"
+
+ # Vray has its own way
+ if renderer == "vray":
+ width_attr_name = "vraySettings.width"
+ height_attr_name = "vraySettings.height"
+
+ cmds.setAttr(width_attr_name, resolution_width)
+ cmds.setAttr(height_attr_name, resolution_height)
diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py
index c7fb042ead..37fd543315 100644
--- a/openpype/hosts/maya/api/customize.py
+++ b/openpype/hosts/maya/api/customize.py
@@ -8,10 +8,9 @@ from functools import partial
import maya.cmds as mc
import maya.mel as mel
-from avalon.maya import pipeline
from openpype.api import resources
from openpype.tools.utils import host_tools
-
+from .lib import get_main_window
log = logging.getLogger(__name__)
@@ -76,6 +75,7 @@ def override_component_mask_commands():
def override_toolbox_ui():
"""Add custom buttons in Toolbox as replacement for Maya web help icon."""
icons = resources.get_resource("icons")
+ parent_widget = get_main_window()
# Ensure the maya web icon on toolbox exists
web_button = "ToolBox|MainToolboxLayout|mayaWebButton"
@@ -115,7 +115,7 @@ def override_toolbox_ui():
label="Work Files",
image=os.path.join(icons, "workfiles.png"),
command=lambda: host_tools.show_workfiles(
- parent=pipeline._parent
+ parent=parent_widget
),
width=icon_size,
height=icon_size,
@@ -130,7 +130,7 @@ def override_toolbox_ui():
label="Loader",
image=os.path.join(icons, "loader.png"),
command=lambda: host_tools.show_loader(
- parent=pipeline._parent, use_context=True
+ parent=parent_widget, use_context=True
),
width=icon_size,
height=icon_size,
@@ -145,7 +145,7 @@ def override_toolbox_ui():
label="Inventory",
image=os.path.join(icons, "inventory.png"),
command=lambda: host_tools.show_scene_inventory(
- parent=pipeline._parent
+ parent=parent_widget
),
width=icon_size,
height=icon_size,
diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py
index b236fa7cdb..27a7061f74 100644
--- a/openpype/hosts/maya/api/lib.py
+++ b/openpype/hosts/maya/api/lib.py
@@ -1,7 +1,8 @@
"""Standalone helper functions"""
-import re
import os
+import sys
+import re
import platform
import uuid
import math
@@ -18,16 +19,19 @@ import bson
from maya import cmds, mel
import maya.api.OpenMaya as om
-from avalon import api, maya, io, pipeline
-import avalon.maya.lib
-import avalon.maya.interactive
+from avalon import api, io, pipeline
from openpype import lib
from openpype.api import get_anatomy_settings
+from .commands import reset_frame_range
+self = sys.modules[__name__]
+self._parent = None
+
log = logging.getLogger(__name__)
+IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True)
ATTRIBUTE_DICT = {"int": {"attributeType": "long"},
"str": {"dataType": "string"},
"unicode": {"dataType": "string"},
@@ -100,6 +104,155 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94}
RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"]
+def get_main_window():
+ """Acquire Maya's main window"""
+ from Qt import QtWidgets
+
+ if self._parent is None:
+ self._parent = {
+ widget.objectName(): widget
+ for widget in QtWidgets.QApplication.topLevelWidgets()
+ }["MayaWindow"]
+ return self._parent
+
+
+@contextlib.contextmanager
+def suspended_refresh():
+ """Suspend viewport refreshes"""
+
+ try:
+ cmds.refresh(suspend=True)
+ yield
+ finally:
+ cmds.refresh(suspend=False)
+
+
+@contextlib.contextmanager
+def maintained_selection():
+ """Maintain selection during context
+
+ Example:
+ >>> scene = cmds.file(new=True, force=True)
+ >>> node = cmds.createNode("transform", name="Test")
+ >>> cmds.select("persp")
+ >>> with maintained_selection():
+ ... cmds.select("Test", replace=True)
+ >>> "Test" in cmds.ls(selection=True)
+ False
+
+ """
+
+ previous_selection = cmds.ls(selection=True)
+ try:
+ yield
+ finally:
+ if previous_selection:
+ cmds.select(previous_selection,
+ replace=True,
+ noExpand=True)
+ else:
+ cmds.select(clear=True)
+
+
+def unique_name(name, format="%02d", namespace="", prefix="", suffix=""):
+ """Return unique `name`
+
+ The function takes into consideration an optional `namespace`
+ and `suffix`. The suffix is included in evaluating whether a
+ name exists - such as `name` + "_GRP" - but isn't included
+ in the returned value.
+
+ If a namespace is provided, only names within that namespace
+ are considered when evaluating whether the name is unique.
+
+ Arguments:
+ format (str, optional): The `name` is given a number, this determines
+ how this number is formatted. Defaults to a padding of 2.
+ E.g. my_name01, my_name02.
+ namespace (str, optional): Only consider names within this namespace.
+ suffix (str, optional): Only consider names with this suffix.
+
+ Example:
+ >>> name = cmds.createNode("transform", name="MyName")
+ >>> cmds.objExists(name)
+ True
+ >>> unique = unique_name(name)
+ >>> cmds.objExists(unique)
+ False
+
+ """
+
+ iteration = 1
+ unique = prefix + (name + format % iteration) + suffix
+
+ while cmds.objExists(namespace + ":" + unique):
+ iteration += 1
+ unique = prefix + (name + format % iteration) + suffix
+
+ if suffix:
+ return unique[:-len(suffix)]
+
+ return unique
+
+
+def unique_namespace(namespace, format="%02d", prefix="", suffix=""):
+ """Return unique namespace
+
+ Similar to :func:`unique_name` but evaluating namespaces
+ as opposed to object names.
+
+ Arguments:
+ namespace (str): Name of namespace to consider
+ format (str, optional): Formatting of the given iteration number
+ suffix (str, optional): Only consider namespaces with this suffix.
+
+ """
+
+ iteration = 1
+ unique = prefix + (namespace + format % iteration) + suffix
+
+ # The `existing` set does not just contain the namespaces but *all* nodes
+ # within "current namespace". We need all because the namespace could
+ # also clash with a node name. To be truly unique and valid one needs to
+ # check against all.
+ existing = set(cmds.namespaceInfo(listNamespace=True))
+ while unique in existing:
+ iteration += 1
+ unique = prefix + (namespace + format % iteration) + suffix
+
+ return unique
+
+
+def read(node):
+ """Return user-defined attributes from `node`"""
+
+ data = dict()
+
+ for attr in cmds.listAttr(node, userDefined=True) or list():
+ try:
+ value = cmds.getAttr(node + "." + attr, asString=True)
+
+ except RuntimeError:
+ # For Message type attribute or others that have connections,
+ # take source node name as value.
+ source = cmds.listConnections(node + "." + attr,
+ source=True,
+ destination=False)
+ source = cmds.ls(source, long=True) or [None]
+ value = source[0]
+
+ except ValueError:
+ # Some attributes cannot be read directly,
+ # such as mesh and color attributes. These
+ # are considered non-essential to this
+ # particular publishing pipeline.
+ value = None
+
+ data[attr] = value
+
+ return data
+
+
def _get_mel_global(name):
"""Return the value of a mel global variable"""
return mel.eval("$%s = $%s;" % (name, name))
@@ -280,6 +433,73 @@ def shape_from_element(element):
return node
+def export_alembic(nodes,
+ file,
+ frame_range=None,
+ write_uv=True,
+ write_visibility=True,
+ attribute_prefix=None):
+ """Wrap native MEL command with limited set of arguments
+
+ Arguments:
+ nodes (list): Long names of nodes to cache
+
+ file (str): Absolute path to output destination
+
+ frame_range (tuple, optional): Start- and end-frame of cache,
+ default to current animation range.
+
+ write_uv (bool, optional): Whether or not to include UVs,
+ default to True
+
+ write_visibility (bool, optional): Turn on to store the visibility
+ state of objects in the Alembic file. Otherwise, all objects are
+ considered visible, default to True
+
+ attribute_prefix (str, optional): Include all user-defined
+ attributes with this prefix.
+
+ """
+
+ if frame_range is None:
+ frame_range = (
+ cmds.playbackOptions(query=True, ast=True),
+ cmds.playbackOptions(query=True, aet=True)
+ )
+
+ options = [
+ ("file", file),
+ ("frameRange", "%s %s" % frame_range),
+ ] + [("root", mesh) for mesh in nodes]
+
+ if isinstance(attribute_prefix, string_types):
+ # Include all attributes prefixed with "mb"
+ # TODO(marcus): This would be a good candidate for
+ # external registration, so that the developer
+ # doesn't have to edit this function to modify
+ # the behavior of Alembic export.
+ options.append(("attrPrefix", str(attribute_prefix)))
+
+ if write_uv:
+ options.append(("uvWrite", ""))
+
+ if write_visibility:
+ options.append(("writeVisibility", ""))
+
+ # Generate MEL command
+ mel_args = list()
+ for key, value in options:
+ mel_args.append("-{0} {1}".format(key, value))
+
+ mel_args_string = " ".join(mel_args)
+ mel_cmd = "AbcExport -j \"{0}\"".format(mel_args_string)
+
+ # For debuggability, put the string passed to MEL in the Script editor.
+ print("mel.eval('%s')" % mel_cmd)
+
+ return mel.eval(mel_cmd)
+
+
def collect_animation_data(fps=False):
"""Get the basic animation data
@@ -305,6 +525,256 @@ def collect_animation_data(fps=False):
return data
+def imprint(node, data):
+ """Write `data` to `node` as userDefined attributes
+
+ Arguments:
+ node (str): Long name of node
+ data (dict): Dictionary of key/value pairs
+
+ Example:
+ >>> from maya import cmds
+ >>> def compute():
+ ... return 6
+ ...
+ >>> cube, generator = cmds.polyCube()
+ >>> imprint(cube, {
+ ... "regularString": "myFamily",
+ ... "computedValue": lambda: compute()
+ ... })
+ ...
+ >>> cmds.getAttr(cube + ".computedValue")
+ 6
+
+ """
+
+ for key, value in data.items():
+
+ if callable(value):
+ # Support values evaluated at imprint
+ value = value()
+
+ if isinstance(value, bool):
+ add_type = {"attributeType": "bool"}
+ set_type = {"keyable": False, "channelBox": True}
+ elif isinstance(value, string_types):
+ add_type = {"dataType": "string"}
+ set_type = {"type": "string"}
+ elif isinstance(value, int):
+ add_type = {"attributeType": "long"}
+ set_type = {"keyable": False, "channelBox": True}
+ elif isinstance(value, float):
+ add_type = {"attributeType": "double"}
+ set_type = {"keyable": False, "channelBox": True}
+ elif isinstance(value, (list, tuple)):
+ add_type = {"attributeType": "enum", "enumName": ":".join(value)}
+ set_type = {"keyable": False, "channelBox": True}
+ value = 0 # enum default
+ else:
+ raise TypeError("Unsupported type: %r" % type(value))
+
+ cmds.addAttr(node, longName=key, **add_type)
+ cmds.setAttr(node + "." + key, value, **set_type)
+
+
+def serialise_shaders(nodes):
+ """Generate a shader set dictionary
+
+ Arguments:
+ nodes (list): Absolute paths to nodes
+
+ Returns:
+ dictionary of (shader: id) pairs
+
+ Schema:
+ {
+ "shader1": ["id1", "id2"],
+ "shader2": ["id3", "id1"]
+ }
+
+ Example:
+ {
+ "Bazooka_Brothers01_:blinn4SG": [
+ "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4922:5001]",
+ "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4587:4634]",
+ "f9520572-ac1d-11e6-b39e-3085a99791c9.f[1120:1567]",
+ "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4251:4362]"
+ ],
+ "lambert2SG": [
+ "f9520571-ac1d-11e6-9dbb-3085a99791c9"
+ ]
+ }
+
+ """
+
+ valid_nodes = cmds.ls(
+ nodes,
+ long=True,
+ recursive=True,
+ showType=True,
+ objectsOnly=True,
+ type="transform"
+ )
+
+ meshes_by_id = {}
+ for mesh in valid_nodes:
+ shapes = cmds.listRelatives(valid_nodes[0],
+ shapes=True,
+ fullPath=True) or list()
+
+ if shapes:
+ shape = shapes[0]
+ if not cmds.nodeType(shape):
+ continue
+
+ try:
+ id_ = cmds.getAttr(mesh + ".mbID")
+
+ if id_ not in meshes_by_id:
+ meshes_by_id[id_] = list()
+
+ meshes_by_id[id_].append(mesh)
+
+ except ValueError:
+ continue
+
+ meshes_by_shader = dict()
+ for mesh in meshes_by_id.values():
+ shape = cmds.listRelatives(mesh,
+ shapes=True,
+ fullPath=True) or list()
+
+ for shader in cmds.listConnections(shape,
+ type="shadingEngine") or list():
+
+ # Objects in this group are those that haven't got
+ # any shaders. These are expected to be managed
+ # elsewhere, such as by the default model loader.
+ if shader == "initialShadingGroup":
+ continue
+
+ if shader not in meshes_by_shader:
+ meshes_by_shader[shader] = list()
+
+ shaded = cmds.sets(shader, query=True) or list()
+ meshes_by_shader[shader].extend(shaded)
+
+ shader_by_id = {}
+ for shader, shaded in meshes_by_shader.items():
+
+ if shader not in shader_by_id:
+ shader_by_id[shader] = list()
+
+ for mesh in shaded:
+
+ # Enable shader assignment to faces.
+ name = mesh.split(".f[")[0]
+
+ transform = name
+ if cmds.objectType(transform) == "mesh":
+ transform = cmds.listRelatives(name, parent=True)[0]
+
+ try:
+ id_ = cmds.getAttr(transform + ".mbID")
+ shader_by_id[shader].append(mesh.replace(name, id_))
+ except KeyError:
+ continue
+
+ # Remove duplicates
+ shader_by_id[shader] = list(set(shader_by_id[shader]))
+
+ return shader_by_id
+
+
+def lsattr(attr, value=None):
+ """Return nodes matching `key` and `value`
+
+ Arguments:
+ attr (str): Name of Maya attribute
+ value (object, optional): Value of attribute. If none
+ is provided, return all nodes with this attribute.
+
+ Example:
+ >> lsattr("id", "myId")
+ ["myNode"]
+ >> lsattr("id")
+ ["myNode", "myOtherNode"]
+
+ """
+
+ if value is None:
+ return cmds.ls("*.%s" % attr,
+ recursive=True,
+ objectsOnly=True,
+ long=True)
+ return lsattrs({attr: value})
+
+
+def lsattrs(attrs):
+ """Return nodes with the given attribute(s).
+
+ Arguments:
+ attrs (dict): Name and value pairs of expected matches
+
+ Example:
+ >> # Return nodes with an `age` of five.
+ >> lsattr({"age": "five"})
+ >> # Return nodes with both `age` and `color` of five and blue.
+ >> lsattr({"age": "five", "color": "blue"})
+
+ Return:
+ list: matching nodes.
+
+ """
+
+ dep_fn = om.MFnDependencyNode()
+ dag_fn = om.MFnDagNode()
+ selection_list = om.MSelectionList()
+
+ first_attr = next(iter(attrs))
+
+ try:
+ selection_list.add("*.{0}".format(first_attr),
+ searchChildNamespaces=True)
+ except RuntimeError as exc:
+ if str(exc).endswith("Object does not exist"):
+ return []
+
+ matches = set()
+ for i in range(selection_list.length()):
+ node = selection_list.getDependNode(i)
+ if node.hasFn(om.MFn.kDagNode):
+ fn_node = dag_fn.setObject(node)
+ full_path_names = [path.fullPathName()
+ for path in fn_node.getAllPaths()]
+ else:
+ fn_node = dep_fn.setObject(node)
+ full_path_names = [fn_node.name()]
+
+ for attr in attrs:
+ try:
+ plug = fn_node.findPlug(attr, True)
+ if plug.asString() != attrs[attr]:
+ break
+ except RuntimeError:
+ break
+ else:
+ matches.update(full_path_names)
+
+ return list(matches)
+
+
+@contextlib.contextmanager
+def without_extension():
+ """Use cmds.file with defaultExtensions=False"""
+ previous_setting = cmds.file(defaultExtensions=True, query=True)
+ try:
+ cmds.file(defaultExtensions=False)
+ yield
+ finally:
+ cmds.file(defaultExtensions=previous_setting)
+
+
@contextlib.contextmanager
def attribute_values(attr_values):
"""Remaps node attributes to values during context.
@@ -736,7 +1206,7 @@ def namespaced(namespace, new=True):
"""
original = cmds.namespaceInfo(cur=True, absoluteName=True)
if new:
- namespace = avalon.maya.lib.unique_namespace(namespace)
+ namespace = unique_namespace(namespace)
cmds.namespace(add=namespace)
try:
@@ -1408,7 +1878,7 @@ def assign_look_by_version(nodes, version_id):
raise RuntimeError("Could not find LookLoader, this is a bug")
# Reference the look file
- with maya.maintained_selection():
+ with maintained_selection():
container_node = pipeline.load(Loader, look_representation)
# Get container members
@@ -1947,7 +2417,7 @@ def set_context_settings():
reset_scene_resolution()
# Set frame range.
- avalon.maya.interactive.reset_frame_range()
+ reset_frame_range()
# Set colorspace
set_colorspace()
@@ -2386,7 +2856,7 @@ def get_attr_in_layer(attr, layer):
def fix_incompatible_containers():
"""Return whether the current scene has any outdated content"""
- host = avalon.api.registered_host()
+ host = api.registered_host()
for container in host.ls():
loader = container['loader']
diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py
index df5058dfd5..40ffd825f3 100644
--- a/openpype/hosts/maya/api/menu.py
+++ b/openpype/hosts/maya/api/menu.py
@@ -1,58 +1,146 @@
-import sys
import os
import logging
from Qt import QtWidgets, QtGui
+import maya.utils
import maya.cmds as cmds
-from avalon.maya import pipeline
+import avalon.api
from openpype.api import BuildWorkfile
from openpype.settings import get_project_settings
from openpype.tools.utils import host_tools
from openpype.hosts.maya.api import lib
+from .lib import get_main_window, IS_HEADLESS
+from .commands import reset_frame_range
log = logging.getLogger(__name__)
+MENU_NAME = "op_maya_menu"
+
def _get_menu(menu_name=None):
"""Return the menu instance if it currently exists in Maya"""
if menu_name is None:
- menu_name = pipeline._menu
+ menu_name = MENU_NAME
widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()}
return widgets.get(menu_name)
-def deferred():
- def add_build_workfiles_item():
- # Add build first workfile
- cmds.menuItem(divider=True, parent=pipeline._menu)
+def install():
+ if cmds.about(batch=True):
+ log.info("Skipping openpype.menu initialization in batch mode..")
+ return
+
+ def deferred():
+ from avalon.tools import publish
+ parent_widget = get_main_window()
+ cmds.menu(
+ MENU_NAME,
+ label=avalon.api.Session["AVALON_LABEL"],
+ tearOff=True,
+ parent="MayaWindow"
+ )
+
+ # Create context menu
+ context_label = "{}, {}".format(
+ avalon.api.Session["AVALON_ASSET"],
+ avalon.api.Session["AVALON_TASK"]
+ )
+ cmds.menuItem(
+ "currentContext",
+ label=context_label,
+ parent=MENU_NAME,
+ enable=False
+ )
+
+ cmds.setParent("..", menu=True)
+
+ cmds.menuItem(divider=True)
+
+ # Create default items
+ cmds.menuItem(
+ "Create...",
+ command=lambda *args: host_tools.show_creator(parent=parent_widget)
+ )
+
+ cmds.menuItem(
+ "Load...",
+ command=lambda *args: host_tools.show_loader(
+ parent=parent_widget,
+ use_context=True
+ )
+ )
+
+ cmds.menuItem(
+ "Publish...",
+ command=lambda *args: host_tools.show_publish(
+ parent=parent_widget
+ ),
+ image=publish.ICON
+ )
+
+ cmds.menuItem(
+ "Manage...",
+ command=lambda *args: host_tools.show_scene_inventory(
+ parent=parent_widget
+ )
+ )
+
+ cmds.menuItem(
+ "Library...",
+ command=lambda *args: host_tools.show_library_loader(
+ parent=parent_widget
+ )
+ )
+
+ cmds.menuItem(divider=True)
+
+ cmds.menuItem(
+ "Work Files...",
+ command=lambda *args: host_tools.show_workfiles(
+ parent=parent_widget
+ ),
+ )
+
+ cmds.menuItem(
+ "Reset Frame Range",
+ command=reset_frame_range
+ )
+
+ cmds.menuItem(
+ "Reset Resolution",
+ command=lib.reset_scene_resolution
+ )
+
+ cmds.menuItem(
+ "Set Colorspace",
+ command=lib.set_colorspace,
+ )
+ cmds.menuItem(divider=True, parent=MENU_NAME)
cmds.menuItem(
"Build First Workfile",
- parent=pipeline._menu,
+ parent=MENU_NAME,
command=lambda *args: BuildWorkfile().process()
)
- def add_look_assigner_item():
cmds.menuItem(
- "Look assigner",
- parent=pipeline._menu,
+ "Look assigner...",
command=lambda *args: host_tools.show_look_assigner(
- pipeline._parent
+ parent_widget
)
)
- def add_experimental_item():
cmds.menuItem(
"Experimental tools...",
- parent=pipeline._menu,
command=lambda *args: host_tools.show_experimental_tools_dialog(
- pipeline._parent
+ parent_widget
)
)
+ cmds.setParent(MENU_NAME, menu=True)
def add_scripts_menu():
try:
@@ -82,124 +170,13 @@ def deferred():
# apply configuration
studio_menu.build_from_configuration(studio_menu, config)
- def modify_workfiles():
- # Find the pipeline menu
- top_menu = _get_menu()
-
- # Try to find workfile tool action in the menu
- workfile_action = None
- for action in top_menu.actions():
- if action.text() == "Work Files":
- workfile_action = action
- break
-
- # Add at the top of menu if "Work Files" action was not found
- after_action = ""
- if workfile_action:
- # Use action's object name for `insertAfter` argument
- after_action = workfile_action.objectName()
-
- # Insert action to menu
- cmds.menuItem(
- "Work Files",
- parent=pipeline._menu,
- command=lambda *args: host_tools.show_workfiles(pipeline._parent),
- insertAfter=after_action
- )
-
- # Remove replaced action
- if workfile_action:
- top_menu.removeAction(workfile_action)
-
- def modify_resolution():
- # Find the pipeline menu
- top_menu = _get_menu()
-
- # Try to find resolution tool action in the menu
- resolution_action = None
- for action in top_menu.actions():
- if action.text() == "Reset Resolution":
- resolution_action = action
- break
-
- # Add at the top of menu if "Work Files" action was not found
- after_action = ""
- if resolution_action:
- # Use action's object name for `insertAfter` argument
- after_action = resolution_action.objectName()
-
- # Insert action to menu
- cmds.menuItem(
- "Reset Resolution",
- parent=pipeline._menu,
- command=lambda *args: lib.reset_scene_resolution(),
- insertAfter=after_action
- )
-
- # Remove replaced action
- if resolution_action:
- top_menu.removeAction(resolution_action)
-
- def remove_project_manager():
- top_menu = _get_menu()
-
- # Try to find "System" menu action in the menu
- system_menu = None
- for action in top_menu.actions():
- if action.text() == "System":
- system_menu = action
- break
-
- if system_menu is None:
- return
-
- # Try to find "Project manager" action in "System" menu
- project_manager_action = None
- for action in system_menu.menu().children():
- if hasattr(action, "text") and action.text() == "Project Manager":
- project_manager_action = action
- break
-
- # Remove "Project manager" action if was found
- if project_manager_action is not None:
- system_menu.menu().removeAction(project_manager_action)
-
- def add_colorspace():
- # Find the pipeline menu
- top_menu = _get_menu()
-
- # Try to find workfile tool action in the menu
- workfile_action = None
- for action in top_menu.actions():
- if action.text() == "Reset Resolution":
- workfile_action = action
- break
-
- # Add at the top of menu if "Work Files" action was not found
- after_action = ""
- if workfile_action:
- # Use action's object name for `insertAfter` argument
- after_action = workfile_action.objectName()
-
- # Insert action to menu
- cmds.menuItem(
- "Set Colorspace",
- parent=pipeline._menu,
- command=lambda *args: lib.set_colorspace(),
- insertAfter=after_action
- )
-
- log.info("Attempting to install scripts menu ...")
-
- # add_scripts_menu()
- add_build_workfiles_item()
- add_look_assigner_item()
- add_experimental_item()
- modify_workfiles()
- modify_resolution()
- remove_project_manager()
- add_colorspace()
- add_scripts_menu()
+ # Allow time for uninstallation to finish.
+ # We use Maya's executeDeferred instead of QTimer.singleShot
+ # so that it only gets called after Maya UI has initialized too.
+ # This is crucial with Maya 2020+ which initializes without UI
+ # first as a QCoreApplication
+ maya.utils.executeDeferred(deferred)
+ cmds.evalDeferred(add_scripts_menu, lowestPriority=True)
def uninstall():
@@ -214,18 +191,27 @@ def uninstall():
log.error(e)
-def install():
- if cmds.about(batch=True):
- log.info("Skipping openpype.menu initialization in batch mode..")
- return
-
- # Allow time for uninstallation to finish.
- cmds.evalDeferred(deferred, lowestPriority=True)
-
-
def popup():
"""Pop-up the existing menu near the mouse cursor."""
menu = _get_menu()
cursor = QtGui.QCursor()
point = cursor.pos()
menu.exec_(point)
+
+
+def update_menu_task_label():
+ """Update the task label in Avalon menu to current session"""
+
+ if IS_HEADLESS:
+ return
+
+ object_name = "{}|currentContext".format(MENU_NAME)
+ if not cmds.menuItem(object_name, query=True, exists=True):
+ log.warning("Can't find menuItem: {}".format(object_name))
+ return
+
+ label = "{}, {}".format(
+ avalon.api.Session["AVALON_ASSET"],
+ avalon.api.Session["AVALON_TASK"]
+ )
+ cmds.menuItem(object_name, edit=True, label=label)
diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py
new file mode 100644
index 0000000000..476ceb840b
--- /dev/null
+++ b/openpype/hosts/maya/api/pipeline.py
@@ -0,0 +1,596 @@
+import os
+import sys
+import errno
+import logging
+import contextlib
+
+from maya import utils, cmds, OpenMaya
+import maya.api.OpenMaya as om
+
+import pyblish.api
+import avalon.api
+
+from avalon.lib import find_submodule
+from avalon.pipeline import AVALON_CONTAINER_ID
+
+import openpype.hosts.maya
+from openpype.tools.utils import host_tools
+from openpype.lib import any_outdated
+from openpype.lib.path_tools import HostDirmap
+from openpype.hosts.maya.lib import copy_workspace_mel
+from . import menu, lib
+
+log = logging.getLogger("openpype.hosts.maya")
+
+HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.maya.__file__))
+PLUGINS_DIR = os.path.join(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")
+
+AVALON_CONTAINERS = ":AVALON_CONTAINERS"
+
+self = sys.modules[__name__]
+self._ignore_lock = False
+self._events = {}
+
+
+def install():
+ from openpype.settings import get_project_settings
+
+ project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
+ # process path mapping
+ dirmap_processor = MayaDirmap("maya", project_settings)
+ dirmap_processor.process_dirmap()
+
+ pyblish.api.register_plugin_path(PUBLISH_PATH)
+ pyblish.api.register_host("mayabatch")
+ pyblish.api.register_host("mayapy")
+ pyblish.api.register_host("maya")
+
+ avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
+ avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH)
+ log.info(PUBLISH_PATH)
+
+ log.info("Installing callbacks ... ")
+ avalon.api.on("init", on_init)
+
+ # Callbacks below are not required for headless mode, the `init` however
+ # is important to load referenced Alembics correctly at rendertime.
+ if lib.IS_HEADLESS:
+ log.info(("Running in headless mode, skipping Maya "
+ "save/open/new callback installation.."))
+ return
+
+ _set_project()
+ _register_callbacks()
+
+ menu.install()
+
+ avalon.api.on("save", on_save)
+ avalon.api.on("open", on_open)
+ avalon.api.on("new", on_new)
+ avalon.api.before("save", on_before_save)
+ avalon.api.on("taskChanged", on_task_changed)
+ avalon.api.on("before.workfile.save", before_workfile_save)
+
+ log.info("Setting default family states for loader..")
+ avalon.api.data["familiesStateToggled"] = ["imagesequence"]
+
+
+def _set_project():
+ """Sets the maya project to the current Session's work directory.
+
+ Returns:
+ None
+
+ """
+ workdir = avalon.api.Session["AVALON_WORKDIR"]
+
+ try:
+ os.makedirs(workdir)
+ except OSError as e:
+ # An already existing working directory is fine.
+ if e.errno == errno.EEXIST:
+ pass
+ else:
+ raise
+
+ cmds.workspace(workdir, openWorkspace=True)
+
+
+def _register_callbacks():
+ for handler, event in self._events.copy().items():
+ if event is None:
+ continue
+
+ try:
+ OpenMaya.MMessage.removeCallback(event)
+ self._events[handler] = None
+ except RuntimeError as e:
+ log.info(e)
+
+ self._events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback(
+ OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save
+ )
+
+ self._events[_before_scene_save] = OpenMaya.MSceneMessage.addCheckCallback(
+ OpenMaya.MSceneMessage.kBeforeSaveCheck, _before_scene_save
+ )
+
+ self._events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback(
+ OpenMaya.MSceneMessage.kAfterNew, _on_scene_new
+ )
+
+ self._events[_on_maya_initialized] = OpenMaya.MSceneMessage.addCallback(
+ OpenMaya.MSceneMessage.kMayaInitialized, _on_maya_initialized
+ )
+
+ self._events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback(
+ OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open
+ )
+
+ log.info("Installed event handler _on_scene_save..")
+ log.info("Installed event handler _before_scene_save..")
+ log.info("Installed event handler _on_scene_new..")
+ log.info("Installed event handler _on_maya_initialized..")
+ log.info("Installed event handler _on_scene_open..")
+
+
+def _on_maya_initialized(*args):
+ avalon.api.emit("init", args)
+
+ if cmds.about(batch=True):
+ log.warning("Running batch mode ...")
+ return
+
+ # Keep reference to the main Window, once a main window exists.
+ lib.get_main_window()
+
+
+def _on_scene_new(*args):
+ avalon.api.emit("new", args)
+
+
+def _on_scene_save(*args):
+ avalon.api.emit("save", args)
+
+
+def _on_scene_open(*args):
+ avalon.api.emit("open", args)
+
+
+def _before_scene_save(return_code, client_data):
+
+ # Default to allowing the action. Registered
+ # callbacks can optionally set this to False
+ # in order to block the operation.
+ OpenMaya.MScriptUtil.setBool(return_code, True)
+
+ avalon.api.emit("before_save", [return_code, client_data])
+
+
+def uninstall():
+ pyblish.api.deregister_plugin_path(PUBLISH_PATH)
+ pyblish.api.deregister_host("mayabatch")
+ pyblish.api.deregister_host("mayapy")
+ pyblish.api.deregister_host("maya")
+
+ avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
+ avalon.api.deregister_plugin_path(
+ avalon.api.InventoryAction, INVENTORY_PATH
+ )
+
+ menu.uninstall()
+
+
+def lock():
+ """Lock scene
+
+ Add an invisible node to your Maya scene with the name of the
+ current file, indicating that this file is "locked" and cannot
+ be modified any further.
+
+ """
+
+ if not cmds.objExists("lock"):
+ with lib.maintained_selection():
+ cmds.createNode("objectSet", name="lock")
+ cmds.addAttr("lock", ln="basename", dataType="string")
+
+ # Permanently hide from outliner
+ cmds.setAttr("lock.verticesOnlySet", True)
+
+ fname = cmds.file(query=True, sceneName=True)
+ basename = os.path.basename(fname)
+ cmds.setAttr("lock.basename", basename, type="string")
+
+
+def unlock():
+ """Permanently unlock a locked scene
+
+ Doesn't throw an error if scene is already unlocked.
+
+ """
+
+ try:
+ cmds.delete("lock")
+ except ValueError:
+ pass
+
+
+def is_locked():
+ """Query whether current scene is locked"""
+ fname = cmds.file(query=True, sceneName=True)
+ basename = os.path.basename(fname)
+
+ if self._ignore_lock:
+ return False
+
+ try:
+ return cmds.getAttr("lock.basename") == basename
+ except ValueError:
+ return False
+
+
+@contextlib.contextmanager
+def lock_ignored():
+ """Context manager for temporarily ignoring the lock of a scene
+
+ The purpose of this function is to enable locking a scene and
+ saving it with the lock still in place.
+
+ Example:
+ >>> with lock_ignored():
+ ... pass # Do things without lock
+
+ """
+
+ self._ignore_lock = True
+
+ try:
+ yield
+ finally:
+ self._ignore_lock = False
+
+
+def parse_container(container):
+ """Return the container node's full container data.
+
+ Args:
+ container (str): A container node name.
+
+ Returns:
+ dict: The container schema data for this container node.
+
+ """
+ data = lib.read(container)
+
+ # Backwards compatibility pre-schemas for containers
+ data["schema"] = data.get("schema", "openpype:container-1.0")
+
+ # Append transient data
+ data["objectName"] = container
+
+ return data
+
+
+def _ls():
+ """Yields Avalon container node names.
+
+ Used by `ls()` to retrieve the nodes and then query the full container's
+ data.
+
+ Yields:
+ str: Avalon container node name (objectSet)
+
+ """
+
+ def _maya_iterate(iterator):
+ """Helper to iterate a maya iterator"""
+ while not iterator.isDone():
+ yield iterator.thisNode()
+ iterator.next()
+
+ ids = {AVALON_CONTAINER_ID,
+ # Backwards compatibility
+ "pyblish.mindbender.container"}
+
+ # Iterate over all 'set' nodes in the scene to detect whether
+ # they have the avalon container ".id" attribute.
+ fn_dep = om.MFnDependencyNode()
+ iterator = om.MItDependencyNodes(om.MFn.kSet)
+ for mobject in _maya_iterate(iterator):
+ if mobject.apiTypeStr != "kSet":
+ # Only match by exact type
+ continue
+
+ fn_dep.setObject(mobject)
+ if not fn_dep.hasAttribute("id"):
+ continue
+
+ plug = fn_dep.findPlug("id", True)
+ value = plug.asString()
+ if value in ids:
+ yield fn_dep.name()
+
+
+def ls():
+ """Yields containers from active Maya scene
+
+ This is the host-equivalent of api.ls(), but instead of listing
+ assets on disk, it lists assets already loaded in Maya; once loaded
+ they are called 'containers'
+
+ Yields:
+ dict: container
+
+ """
+ container_names = _ls()
+
+ has_metadata_collector = False
+ config_host = find_submodule(avalon.api.registered_config(), "maya")
+ if hasattr(config_host, "collect_container_metadata"):
+ has_metadata_collector = True
+
+ for container in sorted(container_names):
+ data = parse_container(container)
+
+ # Collect custom data if attribute is present
+ if has_metadata_collector:
+ metadata = config_host.collect_container_metadata(container)
+ data.update(metadata)
+
+ yield data
+
+
+def containerise(name,
+ namespace,
+ nodes,
+ context,
+ loader=None,
+ suffix="CON"):
+ """Bundle `nodes` into an assembly and imprint it with metadata
+
+ Containerisation enables a tracking of version, author and origin
+ for loaded assets.
+
+ Arguments:
+ name (str): Name of resulting assembly
+ namespace (str): Namespace under which to host container
+ nodes (list): Long names of nodes to containerise
+ context (dict): Asset information
+ loader (str, optional): Name of loader used to produce this container.
+ suffix (str, optional): Suffix of container, defaults to `_CON`.
+
+ Returns:
+ container (str): Name of container assembly
+
+ """
+ container = cmds.sets(nodes, name="%s_%s_%s" % (namespace, name, suffix))
+
+ data = [
+ ("schema", "openpype:container-2.0"),
+ ("id", AVALON_CONTAINER_ID),
+ ("name", name),
+ ("namespace", namespace),
+ ("loader", str(loader)),
+ ("representation", context["representation"]["_id"]),
+ ]
+
+ for key, value in data:
+ if not value:
+ continue
+
+ if isinstance(value, (int, float)):
+ cmds.addAttr(container, longName=key, attributeType="short")
+ cmds.setAttr(container + "." + key, value)
+
+ else:
+ cmds.addAttr(container, longName=key, dataType="string")
+ cmds.setAttr(container + "." + key, value, type="string")
+
+ main_container = cmds.ls(AVALON_CONTAINERS, type="objectSet")
+ if not main_container:
+ main_container = cmds.sets(empty=True, name=AVALON_CONTAINERS)
+
+ # Implement #399: Maya 2019+ hide AVALON_CONTAINERS on creation..
+ if cmds.attributeQuery("hiddenInOutliner",
+ node=main_container,
+ exists=True):
+ cmds.setAttr(main_container + ".hiddenInOutliner", True)
+ else:
+ main_container = main_container[0]
+
+ cmds.sets(container, addElement=main_container)
+
+ # Implement #399: Maya 2019+ hide containers in outliner
+ if cmds.attributeQuery("hiddenInOutliner",
+ node=container,
+ exists=True):
+ cmds.setAttr(container + ".hiddenInOutliner", True)
+
+ return container
+
+
+def on_init(_):
+ log.info("Running callback on init..")
+
+ def safe_deferred(fn):
+ """Execute deferred the function in a try-except"""
+
+ def _fn():
+ """safely call in deferred callback"""
+ try:
+ fn()
+ except Exception as exc:
+ print(exc)
+
+ try:
+ utils.executeDeferred(_fn)
+ except Exception as exc:
+ print(exc)
+
+ # Force load Alembic so referenced alembics
+ # work correctly on scene open
+ cmds.loadPlugin("AbcImport", quiet=True)
+ cmds.loadPlugin("AbcExport", quiet=True)
+
+ # Force load objExport plug-in (requested by artists)
+ cmds.loadPlugin("objExport", quiet=True)
+
+ from .customize import (
+ override_component_mask_commands,
+ override_toolbox_ui
+ )
+ safe_deferred(override_component_mask_commands)
+
+ launch_workfiles = os.environ.get("WORKFILES_STARTUP")
+
+ if launch_workfiles:
+ safe_deferred(host_tools.show_workfiles)
+
+ if not lib.IS_HEADLESS:
+ safe_deferred(override_toolbox_ui)
+
+
+def on_before_save(return_code, _):
+ """Run validation for scene's FPS prior to saving"""
+ return lib.validate_fps()
+
+
+def on_save(_):
+ """Automatically add IDs to new nodes
+
+ Any transform of a mesh, without an existing ID, is given one
+ automatically on file save.
+ """
+
+ log.info("Running callback on save..")
+
+ # # Update current task for the current scene
+ # update_task_from_path(cmds.file(query=True, sceneName=True))
+
+ # Generate ids of the current context on nodes in the scene
+ nodes = lib.get_id_required_nodes(referenced_nodes=False)
+ for node, new_id in lib.generate_ids(nodes):
+ lib.set_id(node, new_id, overwrite=False)
+
+
+def on_open(_):
+ """On scene open let's assume the containers have changed."""
+
+ from Qt import QtWidgets
+ from openpype.widgets import popup
+
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.remove_render_layer_observer()")
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.add_render_layer_observer()")
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.add_render_layer_change_observer()")
+ # # Update current task for the current scene
+ # update_task_from_path(cmds.file(query=True, sceneName=True))
+
+ # Validate FPS after update_task_from_path to
+ # ensure it is using correct FPS for the asset
+ lib.validate_fps()
+ lib.fix_incompatible_containers()
+
+ if any_outdated():
+ log.warning("Scene has outdated content.")
+
+ # Find maya main window
+ top_level_widgets = {w.objectName(): w for w in
+ QtWidgets.QApplication.topLevelWidgets()}
+ parent = top_level_widgets.get("MayaWindow", None)
+
+ if parent is None:
+ log.info("Skipping outdated content pop-up "
+ "because Maya window can't be found.")
+ else:
+
+ # Show outdated pop-up
+ def _on_show_inventory():
+ host_tools.show_scene_inventory(parent=parent)
+
+ dialog = popup.Popup(parent=parent)
+ dialog.setWindowTitle("Maya scene has outdated content")
+ dialog.setMessage("There are outdated containers in "
+ "your Maya scene.")
+ dialog.on_show.connect(_on_show_inventory)
+ dialog.show()
+
+
+def on_new(_):
+ """Set project resolution and fps when create a new file"""
+ log.info("Running callback on new..")
+ with lib.suspended_refresh():
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.remove_render_layer_observer()")
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.add_render_layer_observer()")
+ cmds.evalDeferred(
+ "from openpype.hosts.maya.api import lib;"
+ "lib.add_render_layer_change_observer()")
+ lib.set_context_settings()
+
+
+def on_task_changed(*args):
+ """Wrapped function of app initialize and maya's on task changed"""
+ # Run
+ menu.update_menu_task_label()
+
+ workdir = avalon.api.Session["AVALON_WORKDIR"]
+ if os.path.exists(workdir):
+ log.info("Updating Maya workspace for task change to %s", workdir)
+
+ _set_project()
+
+ # Set Maya fileDialog's start-dir to /scenes
+ frule_scene = cmds.workspace(fileRuleEntry="scene")
+ cmds.optionVar(stringValue=("browserLocationmayaBinaryscene",
+ workdir + "/" + frule_scene))
+
+ else:
+ log.warning((
+ "Can't set project for new context because path does not exist: {}"
+ ).format(workdir))
+
+ with lib.suspended_refresh():
+ lib.set_context_settings()
+ lib.update_content_on_context_change()
+
+ msg = " project: {}\n asset: {}\n task:{}".format(
+ avalon.api.Session["AVALON_PROJECT"],
+ avalon.api.Session["AVALON_ASSET"],
+ avalon.api.Session["AVALON_TASK"]
+ )
+
+ lib.show_message(
+ "Context was changed",
+ ("Context was changed to:\n{}".format(msg)),
+ )
+
+
+def before_workfile_save(event):
+ workdir_path = event.workdir_path
+ if workdir_path:
+ copy_workspace_mel(workdir_path)
+
+
+class MayaDirmap(HostDirmap):
+ def on_enable_dirmap(self):
+ cmds.dirmap(en=True)
+
+ def dirmap_routine(self, source_path, destination_path):
+ cmds.dirmap(m=(source_path, destination_path))
+ cmds.dirmap(m=(destination_path, source_path))
diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py
index a5f03cd576..64e910627d 100644
--- a/openpype/hosts/maya/api/plugin.py
+++ b/openpype/hosts/maya/api/plugin.py
@@ -1,8 +1,14 @@
+import os
+
+from maya import cmds
+
from avalon import api
from avalon.vendor import qargparse
-import avalon.maya
from openpype.api import PypeCreatorMixin
+from .pipeline import containerise
+from . import lib
+
def get_reference_node(members, log=None):
"""Get the reference node from the container members
@@ -14,8 +20,6 @@ def get_reference_node(members, log=None):
"""
- from maya import cmds
-
# Collect the references without .placeHolderList[] attributes as
# unique entries (objects only) and skipping the sharedReferenceNode.
references = set()
@@ -61,8 +65,6 @@ def get_reference_node_parents(ref):
list: The upstream parent reference nodes.
"""
- from maya import cmds
-
parent = cmds.referenceQuery(ref,
referenceNode=True,
parent=True)
@@ -75,11 +77,25 @@ def get_reference_node_parents(ref):
return parents
-class Creator(PypeCreatorMixin, avalon.maya.Creator):
- pass
+class Creator(PypeCreatorMixin, api.Creator):
+ def process(self):
+ nodes = list()
+
+ with lib.undo_chunk():
+ if (self.options or {}).get("useSelection"):
+ nodes = cmds.ls(selection=True)
+
+ instance = cmds.sets(nodes, name=self.name)
+ lib.imprint(instance, self.data)
+
+ return instance
-class ReferenceLoader(api.Loader):
+class Loader(api.Loader):
+ hosts = ["maya"]
+
+
+class ReferenceLoader(Loader):
"""A basic ReferenceLoader for Maya
This will implement the basic behavior for a loader to inherit from that
@@ -117,11 +133,6 @@ class ReferenceLoader(api.Loader):
namespace=None,
options=None
):
-
- import os
- from avalon.maya import lib
- from avalon.maya.pipeline import containerise
-
assert os.path.exists(self.fname), "%s does not exist." % self.fname
asset = context['asset']
@@ -182,8 +193,6 @@ class ReferenceLoader(api.Loader):
def update(self, container, representation):
-
- import os
from maya import cmds
node = container["objectName"]
diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py
index 4f826b8fde..1a7c3933a1 100644
--- a/openpype/hosts/maya/api/setdress.py
+++ b/openpype/hosts/maya/api/setdress.py
@@ -9,8 +9,10 @@ import six
from maya import cmds
from avalon import api, io
-from avalon.maya.lib import unique_namespace
-from openpype.hosts.maya.api.lib import matrix_equals
+from openpype.hosts.maya.api.lib import (
+ matrix_equals,
+ unique_namespace
+)
log = logging.getLogger("PackageLoader")
@@ -239,7 +241,7 @@ def get_contained_containers(container):
"""
import avalon.schema
- from avalon.maya.pipeline import parse_container
+ from .pipeline import parse_container
# Get avalon containers in this package setdress container
containers = []
diff --git a/openpype/hosts/maya/api/workio.py b/openpype/hosts/maya/api/workio.py
new file mode 100644
index 0000000000..698c48e81e
--- /dev/null
+++ b/openpype/hosts/maya/api/workio.py
@@ -0,0 +1,67 @@
+"""Host API required Work Files tool"""
+import os
+from maya import cmds
+from avalon import api
+
+
+def file_extensions():
+ return api.HOST_WORKFILE_EXTENSIONS["maya"]
+
+
+def has_unsaved_changes():
+ return cmds.file(query=True, modified=True)
+
+
+def save_file(filepath):
+ cmds.file(rename=filepath)
+ ext = os.path.splitext(filepath)[1]
+ if ext == ".mb":
+ file_type = "mayaBinary"
+ else:
+ file_type = "mayaAscii"
+ cmds.file(save=True, type=file_type)
+
+
+def open_file(filepath):
+ return cmds.file(filepath, open=True, force=True)
+
+
+def current_file():
+
+ current_filepath = cmds.file(query=True, sceneName=True)
+ if not current_filepath:
+ return None
+
+ return current_filepath
+
+
+def work_root(session):
+ work_dir = session["AVALON_WORKDIR"]
+ scene_dir = None
+
+ # Query scene file rule from workspace.mel if it exists in WORKDIR
+ # We are parsing the workspace.mel manually as opposed to temporarily
+ # setting the Workspace in Maya in a context manager since Maya had a
+ # tendency to crash on frequently changing the workspace when this
+ # function was called many times as one scrolled through Work Files assets.
+ workspace_mel = os.path.join(work_dir, "workspace.mel")
+ if os.path.exists(workspace_mel):
+ scene_rule = 'workspace -fr "scene" '
+ # We need to use builtins as `open` is overridden by the workio API
+ open_file = __builtins__["open"]
+ with open_file(workspace_mel, "r") as f:
+ for line in f:
+ if line.strip().startswith(scene_rule):
+ # remainder == "rule";
+ remainder = line[len(scene_rule):]
+ # scene_dir == rule
+ scene_dir = remainder.split('"')[1]
+ else:
+ # We can't query a workspace that does not exist
+ # so we return similar to what we do in other hosts.
+ scene_dir = session.get("AVALON_SCENEDIR")
+
+ if scene_dir:
+ return os.path.join(work_dir, scene_dir)
+ else:
+ return work_dir
diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py
index e3cad4cf2e..119edccb7a 100644
--- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py
+++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py
@@ -1,4 +1,9 @@
-from avalon import api, io
+import json
+from avalon import api, io, pipeline
+from openpype.hosts.maya.api.lib import (
+ maintained_selection,
+ apply_shaders
+)
class ImportModelRender(api.InventoryAction):
@@ -49,10 +54,8 @@ class ImportModelRender(api.InventoryAction):
Returns:
None
"""
- import json
+
from maya import cmds
- from avalon import maya, io, pipeline
- from openpype.hosts.maya.api import lib
# Get representations of shader file and relationships
look_repr = io.find_one({
@@ -77,7 +80,7 @@ class ImportModelRender(api.InventoryAction):
json_file = pipeline.get_representation_path_from_context(context)
# Import the look file
- with maya.maintained_selection():
+ with maintained_selection():
shader_nodes = cmds.file(maya_file,
i=True, # import
returnNewNodes=True)
@@ -89,4 +92,4 @@ class ImportModelRender(api.InventoryAction):
relationships = json.load(f)
# Assign relationships
- lib.apply_shaders(relationships, shader_nodes, nodes)
+ apply_shaders(relationships, shader_nodes, nodes)
diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py
index b1784f1590..bce1f0fc67 100644
--- a/openpype/hosts/maya/plugins/load/_load_animation.py
+++ b/openpype/hosts/maya/plugins/load/_load_animation.py
@@ -17,7 +17,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
def process_reference(self, context, name, namespace, data):
import maya.cmds as cmds
- from avalon import maya
+ from openpype.hosts.maya.api.lib import unique_namespace
cmds.loadPlugin("AbcImport.mll", quiet=True)
# Prevent identical alembic nodes from being shared
@@ -27,9 +27,11 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# Assuming name is subset name from the animation, we split the number
# suffix from the name to ensure the namespace is unique
name = name.split("_")[0]
- namespace = maya.unique_namespace("{}_".format(name),
- format="%03d",
- suffix="_abc")
+ namespace = unique_namespace(
+ "{}_".format(name),
+ format="%03d",
+ suffix="_abc"
+ )
# hero_001 (abc)
# asset_counter{optional}
diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py
index 1a9adf6142..1cc7ee0c03 100644
--- a/openpype/hosts/maya/plugins/load/actions.py
+++ b/openpype/hosts/maya/plugins/load/actions.py
@@ -3,6 +3,10 @@
"""
from avalon import api
+from openpype.hosts.maya.api.lib import (
+ maintained_selection,
+ unique_namespace
+)
class SetFrameRangeLoader(api.Loader):
@@ -98,22 +102,19 @@ class ImportMayaLoader(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
import maya.cmds as cmds
- from avalon import maya
- from avalon.maya import lib
-
choice = self.display_warning()
if choice is False:
return
asset = context['asset']
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset["name"] + "_",
prefix="_" if asset["name"][0].isdigit() else "",
suffix="_",
)
- with maya.maintained_selection():
+ with maintained_selection():
cmds.file(self.fname,
i=True,
preserveReferences=True,
diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py
index 891f21916c..18b34d2233 100644
--- a/openpype/hosts/maya/plugins/load/load_ass.py
+++ b/openpype/hosts/maya/plugins/load/load_ass.py
@@ -1,9 +1,15 @@
+import os
+import clique
+
from avalon import api
+from openpype.api import get_project_settings
import openpype.hosts.maya.api.plugin
from openpype.hosts.maya.api.plugin import get_reference_node
-import os
-from openpype.api import get_project_settings
-import clique
+from openpype.hosts.maya.api.lib import (
+ maintained_selection,
+ unique_namespace
+)
+from openpype.hosts.maya.api.pipeline import containerise
class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
@@ -20,7 +26,6 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
def process_reference(self, context, name, namespace, options):
import maya.cmds as cmds
- from avalon import maya
import pymel.core as pm
version = context['version']
@@ -35,7 +40,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
except ValueError:
family = "ass"
- with maya.maintained_selection():
+ with maintained_selection():
groupName = "{}:{}".format(namespace, name)
path = self.fname
@@ -95,8 +100,6 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
self.update(container, representation)
def update(self, container, representation):
-
- import os
from maya import cmds
import pymel.core as pm
@@ -175,8 +178,6 @@ class AssStandinLoader(api.Loader):
def load(self, context, name, namespace, options):
import maya.cmds as cmds
- import avalon.maya.lib as lib
- from avalon.maya.pipeline import containerise
import mtoa.ui.arnoldmenu
import pymel.core as pm
@@ -188,7 +189,7 @@ class AssStandinLoader(api.Loader):
frameStart = version_data.get("frameStart", None)
asset = context['asset']['name']
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py
index 2f0ca3922e..0151da7253 100644
--- a/openpype/hosts/maya/plugins/load/load_assembly.py
+++ b/openpype/hosts/maya/plugins/load/load_assembly.py
@@ -13,11 +13,11 @@ class AssemblyLoader(api.Loader):
def load(self, context, name, namespace, data):
- from avalon.maya.pipeline import containerise
- from avalon.maya import lib
+ from openpype.hosts.maya.api.pipeline import containerise
+ from openpype.hosts.maya.api.lib import unique_namespace
asset = context['asset']['name']
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
@@ -25,9 +25,11 @@ class AssemblyLoader(api.Loader):
from openpype.hosts.maya.api import setdress
- containers = setdress.load_package(filepath=self.fname,
- name=name,
- namespace=namespace)
+ containers = setdress.load_package(
+ filepath=self.fname,
+ name=name,
+ namespace=namespace
+ )
self[:] = containers
diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py
index 0611dcc189..99f1f7c172 100644
--- a/openpype/hosts/maya/plugins/load/load_audio.py
+++ b/openpype/hosts/maya/plugins/load/load_audio.py
@@ -1,7 +1,7 @@
-from avalon import api, io
-from avalon.maya.pipeline import containerise
-from avalon.maya import lib
from maya import cmds, mel
+from avalon import api, io
+from openpype.hosts.maya.api.pipeline import containerise
+from openpype.hosts.maya.api.lib import unique_namespace
class AudioLoader(api.Loader):
@@ -27,7 +27,7 @@ class AudioLoader(api.Loader):
)
asset = context["asset"]["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py
index 444f98f22e..2e0b7bb810 100644
--- a/openpype/hosts/maya/plugins/load/load_gpucache.py
+++ b/openpype/hosts/maya/plugins/load/load_gpucache.py
@@ -17,11 +17,11 @@ class GpuCacheLoader(api.Loader):
def load(self, context, name, namespace, data):
import maya.cmds as cmds
- import avalon.maya.lib as lib
- from avalon.maya.pipeline import containerise
+ from openpype.hosts.maya.api.pipeline import containerise
+ from openpype.hosts.maya.api.lib import unique_namespace
asset = context['asset']['name']
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py
index 0652147324..8e33f51389 100644
--- a/openpype/hosts/maya/plugins/load/load_image_plane.py
+++ b/openpype/hosts/maya/plugins/load/load_image_plane.py
@@ -1,8 +1,9 @@
-from avalon import api, io
-from avalon.maya.pipeline import containerise
-from avalon.maya import lib
from Qt import QtWidgets, QtCore
+from avalon import api, io
+from openpype.hosts.maya.api.pipeline import containerise
+from openpype.hosts.maya.api.lib import unique_namespace
+
from maya import cmds
@@ -88,7 +89,7 @@ class ImagePlaneLoader(api.Loader):
new_nodes = []
image_plane_depth = 1000
asset = context['asset']['name']
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py
index 8e14778fd2..ef1076f2cb 100644
--- a/openpype/hosts/maya/plugins/load/load_look.py
+++ b/openpype/hosts/maya/plugins/load/load_look.py
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
"""Look loader."""
-import openpype.hosts.maya.api.plugin
-from avalon import api, io
import json
-import openpype.hosts.maya.api.lib
from collections import defaultdict
-from openpype.widgets.message_window import ScrollMessageBox
+
from Qt import QtWidgets
+from avalon import api, io
+import openpype.hosts.maya.api.plugin
+from openpype.hosts.maya.api import lib
+from openpype.widgets.message_window import ScrollMessageBox
+
from openpype.hosts.maya.api.plugin import get_reference_node
@@ -36,9 +38,8 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""
import maya.cmds as cmds
- from avalon import maya
- with maya.maintained_selection():
+ with lib.maintained_selection():
nodes = cmds.file(self.fname,
namespace=namespace,
reference=True,
@@ -140,9 +141,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
cmds.file(cr=reference_node) # cleanReference
# reapply shading groups from json representation on orig nodes
- openpype.hosts.maya.api.lib.apply_shaders(json_data,
- shader_nodes,
- orig_nodes)
+ lib.apply_shaders(json_data, shader_nodes, orig_nodes)
msg = ["During reference update some edits failed.",
"All successful edits were kept intact.\n",
@@ -159,8 +158,8 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# region compute lookup
nodes_by_id = defaultdict(list)
for n in nodes:
- nodes_by_id[openpype.hosts.maya.api.lib.get_id(n)].append(n)
- openpype.hosts.maya.api.lib.apply_attributes(attributes, nodes_by_id)
+ nodes_by_id[lib.get_id(n)].append(n)
+ lib.apply_attributes(attributes, nodes_by_id)
# Update metadata
cmds.setAttr("{}.representation".format(node),
diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py
index 4c6a187bc3..fd2ae0f1d3 100644
--- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py
+++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py
@@ -1,11 +1,18 @@
# -*- coding: utf-8 -*-
"""Loader for Redshift proxy."""
-from avalon.maya import lib
+import os
+import clique
+
+import maya.cmds as cmds
+
from avalon import api
from openpype.api import get_project_settings
-import os
-import maya.cmds as cmds
-import clique
+from openpype.hosts.maya.api.lib import (
+ namespaced,
+ maintained_selection,
+ unique_namespace
+)
+from openpype.hosts.maya.api.pipeline import containerise
class RedshiftProxyLoader(api.Loader):
@@ -21,17 +28,13 @@ class RedshiftProxyLoader(api.Loader):
def load(self, context, name=None, namespace=None, options=None):
"""Plugin entry point."""
-
- from avalon.maya.pipeline import containerise
- from openpype.hosts.maya.api.lib import namespaced
-
try:
family = context["representation"]["context"]["family"]
except ValueError:
family = "redshiftproxy"
asset_name = context['asset']["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
@@ -40,7 +43,7 @@ class RedshiftProxyLoader(api.Loader):
# Ensure Redshift for Maya is loaded.
cmds.loadPlugin("redshift4maya", quiet=True)
- with lib.maintained_selection():
+ with maintained_selection():
cmds.namespace(addNamespace=namespace)
with namespaced(namespace, new=False):
nodes, group_node = self.create_rs_proxy(
diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py
index 2cc24f1360..0565b0b95c 100644
--- a/openpype/hosts/maya/plugins/load/load_reference.py
+++ b/openpype/hosts/maya/plugins/load/load_reference.py
@@ -1,9 +1,10 @@
-import openpype.hosts.maya.api.plugin
-from avalon import api, maya
-from maya import cmds
import os
+from maya import cmds
+from avalon import api
from openpype.api import get_project_settings
from openpype.lib import get_creator_by_name
+import openpype.hosts.maya.api.plugin
+from openpype.hosts.maya.api.lib import maintained_selection
class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
@@ -32,7 +33,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
def process_reference(self, context, name, namespace, options):
import maya.cmds as cmds
- from avalon import maya
import pymel.core as pm
try:
@@ -44,7 +44,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# True by default to keep legacy behaviours
attach_to_root = options.get("attach_to_root", True)
- with maya.maintained_selection():
+ with maintained_selection():
cmds.loadPlugin("AbcImport.mll", quiet=True)
nodes = cmds.file(self.fname,
namespace=namespace,
@@ -149,7 +149,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# Create the animation instance
creator_plugin = get_creator_by_name(self.animation_creator_name)
- with maya.maintained_selection():
+ with maintained_selection():
cmds.select([output, controls] + roots, noExpand=True)
api.create(
creator_plugin,
diff --git a/openpype/hosts/maya/plugins/load/load_rendersetup.py b/openpype/hosts/maya/plugins/load/load_rendersetup.py
index 574ae9bd3d..efeff2f193 100644
--- a/openpype/hosts/maya/plugins/load/load_rendersetup.py
+++ b/openpype/hosts/maya/plugins/load/load_rendersetup.py
@@ -11,8 +11,8 @@ import six
import sys
from avalon import api
-from avalon.maya import lib
-from openpype.hosts.maya.api import lib as pypelib
+from openpype.hosts.maya.api import lib
+from openpype.hosts.maya.api.pipeline import containerise
from maya import cmds
import maya.app.renderSetup.model.renderSetup as renderSetup
@@ -31,7 +31,6 @@ class RenderSetupLoader(api.Loader):
def load(self, context, name, namespace, data):
"""Load RenderSetup settings."""
- from avalon.maya.pipeline import containerise
# from openpype.hosts.maya.api.lib import namespaced
@@ -83,7 +82,7 @@ class RenderSetupLoader(api.Loader):
def update(self, container, representation):
"""Update RenderSetup setting by overwriting existing settings."""
- pypelib.show_message(
+ lib.show_message(
"Render setup update",
"Render setup setting will be overwritten by new version. All "
"setting specified by user not included in loaded version "
diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py
index f5662ba462..3e1d67ae9a 100644
--- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py
+++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py
@@ -1,7 +1,8 @@
-from avalon import api
import os
+from avalon import api
from openpype.api import get_project_settings
+
class LoadVDBtoRedShift(api.Loader):
"""Load OpenVDB in a Redshift Volume Shape"""
@@ -15,8 +16,8 @@ class LoadVDBtoRedShift(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
from maya import cmds
- import avalon.maya.lib as lib
- from avalon.maya.pipeline import containerise
+ from openpype.hosts.maya.api.pipeline import containerise
+ from openpype.hosts.maya.api.lib import unique_namespace
try:
family = context["representation"]["context"]["family"]
@@ -45,7 +46,7 @@ class LoadVDBtoRedShift(api.Loader):
asset = context['asset']
asset_name = asset["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py
index ed561e1131..099c020093 100644
--- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py
+++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py
@@ -1,6 +1,6 @@
+import os
from avalon import api
from openpype.api import get_project_settings
-import os
from maya import cmds
@@ -80,8 +80,8 @@ class LoadVDBtoVRay(api.Loader):
def load(self, context, name, namespace, data):
- import avalon.maya.lib as lib
- from avalon.maya.pipeline import containerise
+ from openpype.hosts.maya.api.lib import unique_namespace
+ from openpype.hosts.maya.api.pipeline import containerise
assert os.path.exists(self.fname), (
"Path does not exist: %s" % self.fname
@@ -111,7 +111,7 @@ class LoadVDBtoVRay(api.Loader):
asset = context['asset']
asset_name = asset["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
diff --git a/openpype/hosts/maya/plugins/load/load_vrayproxy.py b/openpype/hosts/maya/plugins/load/load_vrayproxy.py
index 806cf1fd18..ac2fe635b3 100644
--- a/openpype/hosts/maya/plugins/load/load_vrayproxy.py
+++ b/openpype/hosts/maya/plugins/load/load_vrayproxy.py
@@ -9,9 +9,14 @@ import os
import maya.cmds as cmds
-from avalon.maya import lib
from avalon import api, io
from openpype.api import get_project_settings
+from openpype.hosts.maya.api.lib import (
+ maintained_selection,
+ namespaced,
+ unique_namespace
+)
+from openpype.hosts.maya.api.pipeline import containerise
class VRayProxyLoader(api.Loader):
@@ -36,8 +41,6 @@ class VRayProxyLoader(api.Loader):
options (dict): Optional loader options.
"""
- from avalon.maya.pipeline import containerise
- from openpype.hosts.maya.api.lib import namespaced
try:
family = context["representation"]["context"]["family"]
@@ -48,7 +51,7 @@ class VRayProxyLoader(api.Loader):
self.fname = self._get_abc(context["version"]["_id"]) or self.fname
asset_name = context['asset']["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
@@ -57,7 +60,7 @@ class VRayProxyLoader(api.Loader):
# Ensure V-Ray for Maya is loaded.
cmds.loadPlugin("vrayformaya", quiet=True)
- with lib.maintained_selection():
+ with maintained_selection():
cmds.namespace(addNamespace=namespace)
with namespaced(namespace, new=False):
nodes, group_node = self.create_vray_proxy(
diff --git a/openpype/hosts/maya/plugins/load/load_vrayscene.py b/openpype/hosts/maya/plugins/load/load_vrayscene.py
index 465dab2a76..2e85514938 100644
--- a/openpype/hosts/maya/plugins/load/load_vrayscene.py
+++ b/openpype/hosts/maya/plugins/load/load_vrayscene.py
@@ -1,8 +1,13 @@
-from avalon.maya import lib
-from avalon import api
-from openpype.api import config
import os
import maya.cmds as cmds
+from avalon import api
+from openpype.api import get_project_settings
+from openpype.hosts.maya.api.lib import (
+ maintained_selection,
+ namespaced,
+ unique_namespace
+)
+from openpype.hosts.maya.api.pipeline import containerise
class VRaySceneLoader(api.Loader):
@@ -18,8 +23,6 @@ class VRaySceneLoader(api.Loader):
def load(self, context, name, namespace, data):
- from avalon.maya.pipeline import containerise
- from openpype.hosts.maya.lib import namespaced
try:
family = context["representation"]["context"]["family"]
@@ -27,7 +30,7 @@ class VRaySceneLoader(api.Loader):
family = "vrayscene_layer"
asset_name = context['asset']["name"]
- namespace = namespace or lib.unique_namespace(
+ namespace = namespace or unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
@@ -36,7 +39,7 @@ class VRaySceneLoader(api.Loader):
# Ensure V-Ray for Maya is loaded.
cmds.loadPlugin("vrayformaya", quiet=True)
- with lib.maintained_selection():
+ with maintained_selection():
cmds.namespace(addNamespace=namespace)
with namespaced(namespace, new=False):
nodes, group_node = self.create_vray_scene(name,
@@ -47,8 +50,8 @@ class VRaySceneLoader(api.Loader):
return
# colour the group node
- presets = config.get_presets(project=os.environ['AVALON_PROJECT'])
- colors = presets['plugins']['maya']['load']['colors']
+ presets = get_project_settings(os.environ['AVALON_PROJECT'])
+ colors = presets['maya']['load']['colors']
c = colors.get(family)
if c is not None:
cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1)
diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py
index de0ea6823c..dfe75173ac 100644
--- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py
+++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py
@@ -3,14 +3,14 @@ import json
import re
import glob
from collections import defaultdict
+from pprint import pprint
from maya import cmds
from avalon import api, io
-from avalon.maya import lib as avalon_lib, pipeline
-from openpype.hosts.maya.api import lib
from openpype.api import get_project_settings
-from pprint import pprint
+from openpype.hosts.maya.api import lib
+from openpype.hosts.maya.api.pipeline import containerise
class YetiCacheLoader(api.Loader):
@@ -75,11 +75,13 @@ class YetiCacheLoader(api.Loader):
self[:] = nodes
- return pipeline.containerise(name=name,
- namespace=namespace,
- nodes=nodes,
- context=context,
- loader=self.__class__.__name__)
+ return containerise(
+ name=name,
+ namespace=namespace,
+ nodes=nodes,
+ context=context,
+ loader=self.__class__.__name__
+ )
def remove(self, container):
@@ -239,9 +241,11 @@ class YetiCacheLoader(api.Loader):
asset_name = "{}_".format(asset)
prefix = "_" if asset_name[0].isdigit()else ""
- namespace = avalon_lib.unique_namespace(asset_name,
- prefix=prefix,
- suffix="_")
+ namespace = lib.unique_namespace(
+ asset_name,
+ prefix=prefix,
+ suffix="_"
+ )
return namespace
diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py
index 3f67f98f51..b4d31b473f 100644
--- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py
+++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py
@@ -25,7 +25,6 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
self, context, name=None, namespace=None, options=None):
import maya.cmds as cmds
- from avalon import maya
# get roots of selected hierarchies
selected_roots = []
@@ -53,7 +52,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
scene_lookup[cb_id] = node
# load rig
- with maya.maintained_selection():
+ with lib.maintained_selection():
nodes = cmds.file(self.fname,
namespace=namespace,
reference=True,
diff --git a/openpype/hosts/maya/plugins/publish/collect_assembly.py b/openpype/hosts/maya/plugins/publish/collect_assembly.py
index 313636793b..1a65bf1fde 100644
--- a/openpype/hosts/maya/plugins/publish/collect_assembly.py
+++ b/openpype/hosts/maya/plugins/publish/collect_assembly.py
@@ -2,7 +2,7 @@ from collections import defaultdict
import pyblish.api
from maya import cmds, mel
-from avalon import maya as avalon
+from openpype.hosts.maya import api
from openpype.hosts.maya.api import lib
# TODO : Publish of assembly: -unique namespace for all assets, VALIDATOR!
@@ -30,7 +30,7 @@ class CollectAssembly(pyblish.api.InstancePlugin):
def process(self, instance):
# Find containers
- containers = avalon.ls()
+ containers = api.ls()
# Get all content from the instance
instance_lookup = set(cmds.ls(instance, type="transform", long=True))
diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py
index cbddb86e53..13ae1924b9 100644
--- a/openpype/hosts/maya/plugins/publish/collect_render.py
+++ b/openpype/hosts/maya/plugins/publish/collect_render.py
@@ -49,7 +49,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup
import pyblish.api
-from avalon import maya, api
+from avalon import api
from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501
from openpype.hosts.maya.api import lib
@@ -409,7 +409,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
dict: only overrides with values
"""
- attributes = maya.read(render_globals)
+ attributes = lib.read(render_globals)
options = {"renderGlobals": {}}
options["renderGlobals"]["Priority"] = attributes["priority"]
diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py
index 7ecc40a68d..269972d996 100644
--- a/openpype/hosts/maya/plugins/publish/extract_animation.py
+++ b/openpype/hosts/maya/plugins/publish/extract_animation.py
@@ -2,9 +2,12 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
-from openpype.hosts.maya.api.lib import extract_alembic
+from openpype.hosts.maya.api.lib import (
+ extract_alembic,
+ suspended_refresh,
+ maintained_selection
+)
class ExtractAnimation(openpype.api.Extractor):
@@ -71,8 +74,8 @@ class ExtractAnimation(openpype.api.Extractor):
# Since Maya 2017 alembic supports multiple uv sets - write them.
options["writeUVSets"] = True
- with avalon.maya.suspended_refresh():
- with avalon.maya.maintained_selection():
+ with suspended_refresh():
+ with maintained_selection():
cmds.select(nodes, noExpand=True)
extract_alembic(file=path,
startFrame=float(start),
diff --git a/openpype/hosts/maya/plugins/publish/extract_ass.py b/openpype/hosts/maya/plugins/publish/extract_ass.py
index 7461ccdf78..ab149de700 100644
--- a/openpype/hosts/maya/plugins/publish/extract_ass.py
+++ b/openpype/hosts/maya/plugins/publish/extract_ass.py
@@ -1,9 +1,9 @@
import os
-import avalon.maya
import openpype.api
from maya import cmds
+from openpype.hosts.maya.api.lib import maintained_selection
class ExtractAssStandin(openpype.api.Extractor):
@@ -30,7 +30,7 @@ class ExtractAssStandin(openpype.api.Extractor):
# Write out .ass file
self.log.info("Writing: '%s'" % file_path)
- with avalon.maya.maintained_selection():
+ with maintained_selection():
self.log.info("Writing: {}".format(instance.data["setMembers"]))
cmds.select(instance.data["setMembers"], noExpand=True)
diff --git a/openpype/hosts/maya/plugins/publish/extract_assproxy.py b/openpype/hosts/maya/plugins/publish/extract_assproxy.py
index f53f385b8b..93720dbb82 100644
--- a/openpype/hosts/maya/plugins/publish/extract_assproxy.py
+++ b/openpype/hosts/maya/plugins/publish/extract_assproxy.py
@@ -1,10 +1,10 @@
import os
-
-from maya import cmds
import contextlib
-import avalon.maya
+from maya import cmds
+
import openpype.api
+from openpype.hosts.maya.api.lib import maintained_selection
class ExtractAssProxy(openpype.api.Extractor):
@@ -54,7 +54,7 @@ class ExtractAssProxy(openpype.api.Extractor):
noIntermediate=True)
self.log.info(members)
- with avalon.maya.maintained_selection():
+ with maintained_selection():
with unparent(members[0]):
cmds.select(members, noExpand=True)
cmds.file(path,
diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
index 8950ed6254..806a079940 100644
--- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
+++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
@@ -2,9 +2,7 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
-
from openpype.hosts.maya.api import lib
@@ -54,7 +52,7 @@ class ExtractCameraAlembic(openpype.api.Extractor):
path = os.path.join(dir_path, filename)
# Perform alembic extraction
- with avalon.maya.maintained_selection():
+ with lib.maintained_selection():
cmds.select(camera, replace=True, noExpand=True)
# Enforce forward slashes for AbcExport because we're
@@ -86,7 +84,7 @@ class ExtractCameraAlembic(openpype.api.Extractor):
job_str += " -attr {0}".format(attr)
with lib.evaluation("off"):
- with avalon.maya.suspended_refresh():
+ with lib.suspended_refresh():
cmds.AbcExport(j=job_str, verbose=False)
if "representations" not in instance.data:
diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
index 888dc636b2..9d25b147de 100644
--- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
+++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
@@ -5,7 +5,6 @@ import itertools
from maya import cmds
-import avalon.maya
import openpype.api
from openpype.hosts.maya.api import lib
@@ -157,9 +156,9 @@ class ExtractCameraMayaScene(openpype.api.Extractor):
path = os.path.join(dir_path, filename)
# Perform extraction
- with avalon.maya.maintained_selection():
+ with lib.maintained_selection():
with lib.evaluation("off"):
- with avalon.maya.suspended_refresh():
+ with lib.suspended_refresh():
if bake_to_worldspace:
self.log.info(
"Performing camera bakes: {}".format(transform))
diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py
index e4894f28cd..844084b9ab 100644
--- a/openpype/hosts/maya/plugins/publish/extract_fbx.py
+++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py
@@ -3,12 +3,12 @@ import os
from maya import cmds # noqa
import maya.mel as mel # noqa
-from openpype.hosts.maya.api.lib import root_parent
-
import pyblish.api
-import avalon.maya
-
import openpype.api
+from openpype.hosts.maya.api.lib import (
+ root_parent,
+ maintained_selection
+)
class ExtractFBX(openpype.api.Extractor):
@@ -205,13 +205,13 @@ class ExtractFBX(openpype.api.Extractor):
# Export
if "unrealStaticMesh" in instance.data["families"]:
- with avalon.maya.maintained_selection():
+ with maintained_selection():
with root_parent(members):
self.log.info("Un-parenting: {}".format(members))
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
else:
- with avalon.maya.maintained_selection():
+ with maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py
index bf79ddbf44..fe89038a24 100644
--- a/openpype/hosts/maya/plugins/publish/extract_look.py
+++ b/openpype/hosts/maya/plugins/publish/extract_look.py
@@ -11,8 +11,7 @@ from collections import OrderedDict
from maya import cmds # noqa
import pyblish.api
-import avalon.maya
-from avalon import io, api
+from avalon import io
import openpype.api
from openpype.hosts.maya.api import lib
@@ -239,7 +238,7 @@ class ExtractLook(openpype.api.Extractor):
# getting incorrectly remapped. (LKD-17, PLN-101)
with no_workspace_dir():
with lib.attribute_values(remap):
- with avalon.maya.maintained_selection():
+ with lib.maintained_selection():
cmds.select(sets, noExpand=True)
cmds.file(
maya_path,
diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py
index e7fb5bc8cb..9c432cbc67 100644
--- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py
+++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py
@@ -4,8 +4,8 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
+from openpype.hosts.maya.api.lib import maintained_selection
class ExtractMayaSceneRaw(openpype.api.Extractor):
@@ -59,7 +59,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor):
# Perform extraction
self.log.info("Performing extraction ...")
- with avalon.maya.maintained_selection():
+ with maintained_selection():
cmds.select(members, noExpand=True)
cmds.file(path,
force=True,
diff --git a/openpype/hosts/maya/plugins/publish/extract_model.py b/openpype/hosts/maya/plugins/publish/extract_model.py
index 40cc9427f3..0282d1e9c8 100644
--- a/openpype/hosts/maya/plugins/publish/extract_model.py
+++ b/openpype/hosts/maya/plugins/publish/extract_model.py
@@ -4,7 +4,6 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
from openpype.hosts.maya.api import lib
@@ -74,7 +73,7 @@ class ExtractModel(openpype.api.Extractor):
polygonObject=1):
with lib.shader(members,
shadingEngine="initialShadingGroup"):
- with avalon.maya.maintained_selection():
+ with lib.maintained_selection():
cmds.select(members, noExpand=True)
cmds.file(path,
force=True,
diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py
index 630cc39398..60502fdde1 100644
--- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py
+++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py
@@ -2,9 +2,12 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
-from openpype.hosts.maya.api.lib import extract_alembic
+from openpype.hosts.maya.api.lib import (
+ extract_alembic,
+ suspended_refresh,
+ maintained_selection
+)
class ExtractAlembic(openpype.api.Extractor):
@@ -70,8 +73,8 @@ class ExtractAlembic(openpype.api.Extractor):
# Since Maya 2017 alembic supports multiple uv sets - write them.
options["writeUVSets"] = True
- with avalon.maya.suspended_refresh():
- with avalon.maya.maintained_selection():
+ with suspended_refresh():
+ with maintained_selection():
cmds.select(nodes, noExpand=True)
extract_alembic(file=path,
startFrame=start,
diff --git a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py
index 7c9e201986..23cac9190d 100644
--- a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py
+++ b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py
@@ -2,11 +2,11 @@
"""Redshift Proxy extractor."""
import os
-import avalon.maya
-import openpype.api
-
from maya import cmds
+import openpype.api
+from openpype.hosts.maya.api.lib import maintained_selection
+
class ExtractRedshiftProxy(openpype.api.Extractor):
"""Extract the content of the instance to a redshift proxy file."""
@@ -54,7 +54,7 @@ class ExtractRedshiftProxy(openpype.api.Extractor):
# Write out rs file
self.log.info("Writing: '%s'" % file_path)
- with avalon.maya.maintained_selection():
+ with maintained_selection():
cmds.select(instance.data["setMembers"], noExpand=True)
cmds.file(file_path,
pr=False,
diff --git a/openpype/hosts/maya/plugins/publish/extract_rig.py b/openpype/hosts/maya/plugins/publish/extract_rig.py
index b28b60114e..53c1eeb671 100644
--- a/openpype/hosts/maya/plugins/publish/extract_rig.py
+++ b/openpype/hosts/maya/plugins/publish/extract_rig.py
@@ -4,8 +4,8 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
+from openpype.hosts.maya.api.lib import maintained_selection
class ExtractRig(openpype.api.Extractor):
@@ -40,7 +40,7 @@ class ExtractRig(openpype.api.Extractor):
# Perform extraction
self.log.info("Performing extraction ...")
- with avalon.maya.maintained_selection():
+ with maintained_selection():
cmds.select(instance, noExpand=True)
cmds.file(path,
force=True,
diff --git a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
index 7103601b85..615bc27878 100644
--- a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
+++ b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
@@ -1,10 +1,10 @@
import os
-import avalon.maya
-import openpype.api
-
from maya import cmds
+import openpype.api
+from openpype.hosts.maya.api.lib import maintained_selection
+
class ExtractVRayProxy(openpype.api.Extractor):
"""Extract the content of the instance to a vrmesh file
@@ -41,7 +41,7 @@ class ExtractVRayProxy(openpype.api.Extractor):
# Write out vrmesh file
self.log.info("Writing: '%s'" % file_path)
- with avalon.maya.maintained_selection():
+ with maintained_selection():
cmds.select(instance.data["setMembers"], noExpand=True)
cmds.vrayCreateProxy(exportType=1,
dir=staging_dir,
diff --git a/openpype/hosts/maya/plugins/publish/extract_vrayscene.py b/openpype/hosts/maya/plugins/publish/extract_vrayscene.py
index 1d7c0fa717..5d41697e5f 100644
--- a/openpype/hosts/maya/plugins/publish/extract_vrayscene.py
+++ b/openpype/hosts/maya/plugins/publish/extract_vrayscene.py
@@ -3,9 +3,9 @@
import os
import re
-import avalon.maya
import openpype.api
from openpype.hosts.maya.api.render_setup_tools import export_in_rs_layer
+from openpype.hosts.maya.api.lib import maintained_selection
from maya import cmds
@@ -57,7 +57,7 @@ class ExtractVrayscene(openpype.api.Extractor):
# Write out vrscene file
self.log.info("Writing: '%s'" % file_path)
- with avalon.maya.maintained_selection():
+ with maintained_selection():
if "*" not in instance.data["setMembers"]:
self.log.info(
"Exporting: {}".format(instance.data["setMembers"]))
diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py b/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py
index d69911c404..5728682abe 100644
--- a/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py
+++ b/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py
@@ -2,8 +2,11 @@ import os
from maya import cmds
-import avalon.maya
import openpype.api
+from openpype.hosts.maya.api.lib import (
+ suspended_refresh,
+ maintained_selection
+)
class ExtractXgenCache(openpype.api.Extractor):
@@ -32,8 +35,8 @@ class ExtractXgenCache(openpype.api.Extractor):
filename = "{name}.abc".format(**instance.data)
path = os.path.join(parent_dir, filename)
- with avalon.maya.suspended_refresh():
- with avalon.maya.maintained_selection():
+ with suspended_refresh():
+ with maintained_selection():
command = (
'-file '
+ path
diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py
index 56d5dfe901..d12567a55a 100644
--- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py
+++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py
@@ -7,9 +7,8 @@ import contextlib
from maya import cmds
-import avalon.maya.lib as lib
import openpype.api
-import openpype.hosts.maya.api.lib as maya
+from openpype.hosts.maya.api import lib
@contextlib.contextmanager
diff --git a/openpype/hosts/maya/plugins/publish/validate_cycle_error.py b/openpype/hosts/maya/plugins/publish/validate_cycle_error.py
index d4faf2e562..4dfe0b8add 100644
--- a/openpype/hosts/maya/plugins/publish/validate_cycle_error.py
+++ b/openpype/hosts/maya/plugins/publish/validate_cycle_error.py
@@ -2,10 +2,9 @@ from maya import cmds
import pyblish.api
-from avalon import maya
-
import openpype.api
import openpype.hosts.maya.api.action
+from openpype.hosts.maya.api.lib import maintained_selection
class ValidateCycleError(pyblish.api.InstancePlugin):
@@ -26,7 +25,7 @@ class ValidateCycleError(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
- with maya.maintained_selection():
+ with maintained_selection():
cmds.select(instance[:], noExpand=True)
plugs = cmds.cycleCheck(all=False, # check selection only
list=True)
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py
index 6b3f508561..90eb01aa12 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py
@@ -3,7 +3,7 @@ from maya import cmds
import pyblish.api
import openpype.api
import openpype.hosts.maya.api.action
-from avalon import maya
+from openpype.hosts.maya.api.lib import maintained_selection
class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin):
@@ -67,7 +67,7 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin):
@classmethod
def repair(cls, instance):
- with maya.maintained_selection():
+ with maintained_selection():
with pc.UndoChunk():
temp_transform = pc.polyCube()[0]
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
index 839aab0d0b..ab0beb2a9c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
@@ -3,7 +3,6 @@ from maya import cmds
import pyblish.api
import openpype.api
import openpype.hosts.maya.api.action
-from avalon import maya
from openpype.hosts.maya.api import lib
diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
index 6b5c5d1398..343eaccb7d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
+++ b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
@@ -5,8 +5,6 @@ import openpype.api
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api import lib
-from avalon.maya import maintained_selection
-
class ValidateShapeZero(pyblish.api.Validator):
"""Shape components may not have any "tweak" values
@@ -51,7 +49,7 @@ class ValidateShapeZero(pyblish.api.Validator):
if not invalid_shapes:
return
- with maintained_selection():
+ with lib.maintained_selection():
with lib.tool("selectSuperContext"):
for shape in invalid_shapes:
cmds.polyCollapseTweaks(shape)
diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py
index 6d27c66882..b89244817a 100644
--- a/openpype/hosts/maya/startup/userSetup.py
+++ b/openpype/hosts/maya/startup/userSetup.py
@@ -1,8 +1,12 @@
import os
+import avalon.api
from openpype.api import get_project_settings
+from openpype.hosts.maya import api
import openpype.hosts.maya.api.lib as mlib
from maya import cmds
+avalon.api.install(api)
+
print("starting OpenPype usersetup")
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index a704c3ae68..4227195235 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -1040,10 +1040,19 @@ class ApplicationLaunchContext:
# Prepare data that will be passed to midprocess
# - store arguments to a json and pass path to json as last argument
# - pass environments to set
+ app_env = self.kwargs.pop("env", {})
json_data = {
"args": self.launch_args,
- "env": self.kwargs.pop("env", {})
+ "env": app_env
}
+ if app_env:
+ # Filter environments of subprocess
+ self.kwargs["env"] = {
+ key: value
+ for key, value in os.environ.items()
+ if key in app_env
+ }
+
# Create temp file
json_temp = tempfile.NamedTemporaryFile(
mode="w", prefix="op_app_args", suffix=".json", delete=False