mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
Merge branch 'develop' of github.com:pypeclub/pype into feature/webpublisher_backend
This commit is contained in:
commit
35f35e9c31
38 changed files with 3313 additions and 102 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,12 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## [3.3.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.3.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892)
|
||||
- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891)
|
||||
- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886)
|
||||
- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885)
|
||||
- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882)
|
||||
- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869)
|
||||
- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868)
|
||||
|
|
@ -23,24 +26,28 @@
|
|||
- Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823)
|
||||
- Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819)
|
||||
- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797)
|
||||
- Maya: Shader name validation [\#1762](https://github.com/pypeclub/OpenPype/pull/1762)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893)
|
||||
- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890)
|
||||
- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889)
|
||||
- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880)
|
||||
- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862)
|
||||
- imageio: fix grouping [\#1856](https://github.com/pypeclub/OpenPype/pull/1856)
|
||||
- publisher: missing version in subset prop [\#1849](https://github.com/pypeclub/OpenPype/pull/1849)
|
||||
- Ftrack type error fix in sync to avalon event handler [\#1845](https://github.com/pypeclub/OpenPype/pull/1845)
|
||||
- Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841)
|
||||
- Fix - Standalone Publish better handling of loading multiple versions… [\#1837](https://github.com/pypeclub/OpenPype/pull/1837)
|
||||
- nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836)
|
||||
- Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813)
|
||||
- Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809)
|
||||
- Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806)
|
||||
- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798)
|
||||
- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863)
|
||||
- Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822)
|
||||
- PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811)
|
||||
|
||||
|
|
@ -62,8 +69,6 @@
|
|||
- Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756)
|
||||
- Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753)
|
||||
- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739)
|
||||
- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736)
|
||||
- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -71,7 +76,6 @@
|
|||
- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801)
|
||||
- Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788)
|
||||
- Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786)
|
||||
- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782)
|
||||
- FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775)
|
||||
- Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772)
|
||||
- Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768)
|
||||
|
|
@ -84,8 +88,6 @@
|
|||
- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742)
|
||||
- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741)
|
||||
- StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738)
|
||||
- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737)
|
||||
- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ def install():
|
|||
.get(platform_name)
|
||||
) or []
|
||||
for path in project_plugins:
|
||||
try:
|
||||
path = str(path.format(**os.environ))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not path or not os.path.exists(path):
|
||||
continue
|
||||
|
||||
|
|
|
|||
53
openpype/hosts/maya/api/commands.py
Normal file
53
openpype/hosts/maya/api/commands.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""OpenPype script commands to be used directly in Maya."""
|
||||
|
||||
|
||||
class ToolWindows:
|
||||
|
||||
_windows = {}
|
||||
|
||||
@classmethod
|
||||
def get_window(cls, tool):
|
||||
"""Get widget for specific tool.
|
||||
|
||||
Args:
|
||||
tool (str): Name of the tool.
|
||||
|
||||
Returns:
|
||||
Stored widget.
|
||||
|
||||
"""
|
||||
try:
|
||||
return cls._windows[tool]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def set_window(cls, tool, window):
|
||||
"""Set widget for the tool.
|
||||
|
||||
Args:
|
||||
tool (str): Name of the tool.
|
||||
window (QtWidgets.QWidget): Widget
|
||||
|
||||
"""
|
||||
cls._windows[tool] = window
|
||||
|
||||
|
||||
def edit_shader_definitions():
|
||||
from avalon.tools import lib
|
||||
from Qt import QtWidgets
|
||||
from openpype.hosts.maya.api.shader_definition_editor import (
|
||||
ShaderDefinitionsEditor
|
||||
)
|
||||
|
||||
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
|
||||
main_window = next(widget for widget in top_level_widgets
|
||||
if widget.objectName() == "MayaWindow")
|
||||
|
||||
with lib.application():
|
||||
window = ToolWindows.get_window("shader_definition_editor")
|
||||
if not window:
|
||||
window = ShaderDefinitionsEditor(parent=main_window)
|
||||
ToolWindows.set_window("shader_definition_editor", window)
|
||||
window.show()
|
||||
|
|
@ -6,9 +6,9 @@ from avalon.vendor.Qt import QtWidgets, QtGui
|
|||
from avalon.maya import pipeline
|
||||
from openpype.api import BuildWorkfile
|
||||
import maya.cmds as cmds
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._menu = os.environ.get("AVALON_LABEL")
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -17,8 +17,11 @@ log = logging.getLogger(__name__)
|
|||
def _get_menu(menu_name=None):
|
||||
"""Return the menu instance if it currently exists in Maya"""
|
||||
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
|
||||
if menu_name is None:
|
||||
menu_name = self._menu
|
||||
menu_name = _menu
|
||||
widgets = dict((
|
||||
w.objectName(), w) for w in QtWidgets.QApplication.allWidgets())
|
||||
menu = widgets.get(menu_name)
|
||||
|
|
@ -55,35 +58,7 @@ def deferred():
|
|||
parent=pipeline._parent
|
||||
)
|
||||
|
||||
# Find the pipeline menu
|
||||
top_menu = _get_menu(pipeline._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=launch_workfiles_app,
|
||||
insertAfter=after_action
|
||||
)
|
||||
|
||||
# Remove replaced action
|
||||
if workfile_action:
|
||||
top_menu.removeAction(workfile_action)
|
||||
|
||||
log.info("Attempting to install scripts menu..")
|
||||
log.info("Attempting to install scripts menu ...")
|
||||
|
||||
add_build_workfiles_item()
|
||||
add_look_assigner_item()
|
||||
|
|
@ -100,13 +75,18 @@ def deferred():
|
|||
return
|
||||
|
||||
# load configuration of custom menu
|
||||
config_path = os.path.join(os.path.dirname(__file__), "menu.json")
|
||||
config = scriptsmenu.load_configuration(config_path)
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
config = project_settings["maya"]["scriptsmenu"]["definition"]
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
|
||||
if not config:
|
||||
log.warning("Skipping studio menu, no definition found.")
|
||||
return
|
||||
|
||||
# run the launcher for Maya menu
|
||||
studio_menu = launchformaya.main(
|
||||
title=self._menu.title(),
|
||||
objectName=self._menu
|
||||
title=_menu.title(),
|
||||
objectName=_menu.title().lower().replace(" ", "_")
|
||||
)
|
||||
|
||||
# apply configuration
|
||||
|
|
@ -116,7 +96,7 @@ def deferred():
|
|||
def uninstall():
|
||||
menu = _get_menu()
|
||||
if menu:
|
||||
log.info("Attempting to uninstall..")
|
||||
log.info("Attempting to uninstall ...")
|
||||
|
||||
try:
|
||||
menu.deleteLater()
|
||||
|
|
@ -136,9 +116,8 @@ def install():
|
|||
|
||||
|
||||
def popup():
|
||||
"""Pop-up the existing menu near the mouse cursor"""
|
||||
"""Pop-up the existing menu near the mouse cursor."""
|
||||
menu = _get_menu()
|
||||
|
||||
cursor = QtGui.QCursor()
|
||||
point = cursor.pos()
|
||||
menu.exec_(point)
|
||||
|
|
|
|||
176
openpype/hosts/maya/api/shader_definition_editor.py
Normal file
176
openpype/hosts/maya/api/shader_definition_editor.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Editor for shader definitions.
|
||||
|
||||
Shader names are stored as simple text file over GridFS in mongodb.
|
||||
|
||||
"""
|
||||
import os
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from openpype.lib.mongo import OpenPypeMongoConnection
|
||||
from openpype import resources
|
||||
import gridfs
|
||||
|
||||
|
||||
DEFINITION_FILENAME = "{}/maya/shader_definition.txt".format(
|
||||
os.getenv("AVALON_PROJECT"))
|
||||
|
||||
|
||||
class ShaderDefinitionsEditor(QtWidgets.QWidget):
|
||||
"""Widget serving as simple editor for shader name definitions."""
|
||||
|
||||
# name of the file used to store definitions
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ShaderDefinitionsEditor, self).__init__(parent)
|
||||
self._mongo = OpenPypeMongoConnection.get_mongo_client()
|
||||
self._gridfs = gridfs.GridFS(
|
||||
self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")])
|
||||
self._editor = None
|
||||
|
||||
self._original_content = self._read_definition_file()
|
||||
|
||||
self.setObjectName("shaderDefinitionEditor")
|
||||
self.setWindowTitle("OpenPype shader name definition editor")
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setParent(parent)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
self.resize(750, 500)
|
||||
|
||||
self._setup_ui()
|
||||
self._reload()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup UI of Widget."""
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
label = QtWidgets.QLabel()
|
||||
label.setText("Put shader names here - one name per line:")
|
||||
layout.addWidget(label)
|
||||
self._editor = QtWidgets.QPlainTextEdit()
|
||||
self._editor.setStyleSheet("border: none;")
|
||||
layout.addWidget(self._editor)
|
||||
|
||||
btn_layout = QtWidgets.QHBoxLayout()
|
||||
save_btn = QtWidgets.QPushButton("Save")
|
||||
save_btn.clicked.connect(self._save)
|
||||
|
||||
reload_btn = QtWidgets.QPushButton("Reload")
|
||||
reload_btn.clicked.connect(self._reload)
|
||||
|
||||
exit_btn = QtWidgets.QPushButton("Exit")
|
||||
exit_btn.clicked.connect(self._close)
|
||||
|
||||
btn_layout.addWidget(reload_btn)
|
||||
btn_layout.addWidget(save_btn)
|
||||
btn_layout.addWidget(exit_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def _read_definition_file(self, file=None):
|
||||
"""Read definition file from database.
|
||||
|
||||
Args:
|
||||
file (gridfs.grid_file.GridOut, Optional): File to read. If not
|
||||
set, new query will be issued to find it.
|
||||
|
||||
Returns:
|
||||
str: Content of the file or empty string if file doesn't exist.
|
||||
|
||||
"""
|
||||
content = ""
|
||||
if not file:
|
||||
file = self._gridfs.find_one(
|
||||
{"filename": DEFINITION_FILENAME})
|
||||
if not file:
|
||||
print(">>> [SNDE]: nothing in database yet")
|
||||
return content
|
||||
content = file.read()
|
||||
file.close()
|
||||
return content
|
||||
|
||||
def _write_definition_file(self, content, force=False):
|
||||
"""Write content as definition to file in database.
|
||||
|
||||
Before file is writen, check is made if its content has not
|
||||
changed. If is changed, warning is issued to user if he wants
|
||||
it to overwrite. Note: GridFs doesn't allow changing file content.
|
||||
You need to delete existing file and create new one.
|
||||
|
||||
Args:
|
||||
content (str): Content to write.
|
||||
|
||||
Raises:
|
||||
ContentException: If file is changed in database while
|
||||
editor is running.
|
||||
"""
|
||||
file = self._gridfs.find_one(
|
||||
{"filename": DEFINITION_FILENAME})
|
||||
if file:
|
||||
content_check = self._read_definition_file(file)
|
||||
if content == content_check:
|
||||
print(">>> [SNDE]: content not changed")
|
||||
return
|
||||
if self._original_content != content_check:
|
||||
if not force:
|
||||
raise ContentException("Content changed")
|
||||
print(">>> [SNDE]: overwriting data")
|
||||
file.close()
|
||||
self._gridfs.delete(file._id)
|
||||
|
||||
file = self._gridfs.new_file(
|
||||
filename=DEFINITION_FILENAME,
|
||||
content_type='text/plain',
|
||||
encoding='utf-8')
|
||||
file.write(content)
|
||||
file.close()
|
||||
QtCore.QTimer.singleShot(200, self._reset_style)
|
||||
self._editor.setStyleSheet("border: 1px solid #33AF65;")
|
||||
self._original_content = content
|
||||
|
||||
def _reset_style(self):
|
||||
"""Reset editor style back.
|
||||
|
||||
Used to visually indicate save.
|
||||
|
||||
"""
|
||||
self._editor.setStyleSheet("border: none;")
|
||||
|
||||
def _close(self):
|
||||
self.hide()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
|
||||
def _reload(self):
|
||||
print(">>> [SNDE]: reloading")
|
||||
self._set_content(self._read_definition_file())
|
||||
|
||||
def _save(self):
|
||||
try:
|
||||
self._write_definition_file(content=self._editor.toPlainText())
|
||||
except ContentException:
|
||||
# content has changed meanwhile
|
||||
print(">>> [SNDE]: content has changed")
|
||||
self._show_overwrite_warning()
|
||||
|
||||
def _set_content(self, content):
|
||||
self._editor.setPlainText(content)
|
||||
|
||||
def _show_overwrite_warning(self):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Warning",
|
||||
("Content you are editing was changed meanwhile in database.\n"
|
||||
"Please, reload and solve the conflict."),
|
||||
QtWidgets.QMessageBox.OK)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.OK:
|
||||
# do nothing
|
||||
pass
|
||||
|
||||
|
||||
class ContentException(Exception):
|
||||
"""This is risen during save if file is changed in database."""
|
||||
pass
|
||||
|
|
@ -167,6 +167,8 @@ def get_file_node_path(node):
|
|||
|
||||
if cmds.nodeType(node) == 'aiImage':
|
||||
return cmds.getAttr('{0}.filename'.format(node))
|
||||
if cmds.nodeType(node) == 'RedshiftNormalMap':
|
||||
return cmds.getAttr('{}.tex0'.format(node))
|
||||
|
||||
# otherwise use fileTextureName
|
||||
return cmds.getAttr('{0}.fileTextureName'.format(node))
|
||||
|
|
@ -357,6 +359,7 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
|
||||
files = cmds.ls(history, type="file", long=True)
|
||||
files.extend(cmds.ls(history, type="aiImage", long=True))
|
||||
files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True))
|
||||
|
||||
self.log.info("Collected file nodes:\n{}".format(files))
|
||||
# Collect textures if any file nodes are found
|
||||
|
|
@ -487,7 +490,7 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
self.log.debug("processing: {}".format(node))
|
||||
if cmds.nodeType(node) not in ["file", "aiImage"]:
|
||||
if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]:
|
||||
self.log.error(
|
||||
"Unsupported file node: {}".format(cmds.nodeType(node)))
|
||||
raise AssertionError("Unsupported file node")
|
||||
|
|
@ -500,11 +503,19 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
self.log.debug("aiImage node")
|
||||
attribute = "{}.filename".format(node)
|
||||
computed_attribute = attribute
|
||||
elif cmds.nodeType(node) == 'RedshiftNormalMap':
|
||||
self.log.debug("RedshiftNormalMap node")
|
||||
attribute = "{}.tex0".format(node)
|
||||
computed_attribute = attribute
|
||||
|
||||
source = cmds.getAttr(attribute)
|
||||
self.log.info(" - file source: {}".format(source))
|
||||
color_space_attr = "{}.colorSpace".format(node)
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
try:
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
except ValueError:
|
||||
# node doesn't have colorspace attribute
|
||||
color_space = "raw"
|
||||
# Compare with the computed file path, e.g. the one with the <UDIM>
|
||||
# pattern in it, to generate some logging information about this
|
||||
# difference
|
||||
|
|
|
|||
|
|
@ -233,11 +233,14 @@ class ExtractLook(openpype.api.Extractor):
|
|||
for filepath in files_metadata:
|
||||
|
||||
linearize = False
|
||||
if do_maketx and files_metadata[filepath]["color_space"] == "sRGB": # noqa: E501
|
||||
if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501
|
||||
linearize = True
|
||||
# set its file node to 'raw' as tx will be linearized
|
||||
files_metadata[filepath]["color_space"] = "raw"
|
||||
|
||||
if do_maketx:
|
||||
color_space = "raw"
|
||||
|
||||
source, mode, texture_hash = self._process_texture(
|
||||
filepath,
|
||||
do_maketx,
|
||||
|
|
@ -280,14 +283,19 @@ class ExtractLook(openpype.api.Extractor):
|
|||
# This will also trigger in the same order at end of context to
|
||||
# ensure after context it's still the original value.
|
||||
color_space_attr = resource["node"] + ".colorSpace"
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
if files_metadata[source]["color_space"] == "raw":
|
||||
# set color space to raw if we linearized it
|
||||
color_space = "Raw"
|
||||
# Remap file node filename to destination
|
||||
try:
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
except ValueError:
|
||||
# node doesn't have color space attribute
|
||||
color_space = "raw"
|
||||
else:
|
||||
if files_metadata[source]["color_space"] == "raw":
|
||||
# set color space to raw if we linearized it
|
||||
color_space = "raw"
|
||||
# Remap file node filename to destination
|
||||
remap[color_space_attr] = color_space
|
||||
attr = resource["attribute"]
|
||||
remap[attr] = destinations[source]
|
||||
remap[color_space_attr] = color_space
|
||||
|
||||
self.log.info("Finished remapping destinations ...")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate model nodes names."""
|
||||
from maya import cmds
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
import avalon.api
|
||||
import openpype.hosts.maya.api.action
|
||||
from openpype.hosts.maya.api.shader_definition_editor import (
|
||||
DEFINITION_FILENAME)
|
||||
from openpype.lib.mongo import OpenPypeMongoConnection
|
||||
import gridfs
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
class ValidateModelName(pyblish.api.InstancePlugin):
|
||||
|
|
@ -19,18 +27,18 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
families = ["model"]
|
||||
label = "Model Name"
|
||||
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
|
||||
# path to shader names definitions
|
||||
# TODO: move it to preset file
|
||||
material_file = None
|
||||
regex = '(.*)_(\\d)*_(.*)_(GEO)'
|
||||
database_file = DEFINITION_FILENAME
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Get invalid nodes."""
|
||||
use_db = cls.database
|
||||
|
||||
# find out if supplied transform is group or not
|
||||
def is_group(groupName):
|
||||
def is_group(group_name):
|
||||
"""Find out if supplied transform is group or not."""
|
||||
try:
|
||||
children = cmds.listRelatives(groupName, children=True)
|
||||
children = cmds.listRelatives(group_name, children=True)
|
||||
for child in children:
|
||||
if not cmds.ls(child, transforms=True):
|
||||
return False
|
||||
|
|
@ -44,29 +52,74 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
cls.log.error("Instance has no nodes!")
|
||||
return True
|
||||
pass
|
||||
|
||||
# validate top level group name
|
||||
assemblies = cmds.ls(content_instance, assemblies=True, long=True)
|
||||
if len(assemblies) != 1:
|
||||
cls.log.error("Must have exactly one top group")
|
||||
return assemblies or True
|
||||
top_group = assemblies[0]
|
||||
regex = cls.top_level_regex
|
||||
r = re.compile(regex)
|
||||
m = r.match(top_group)
|
||||
if m is None:
|
||||
cls.log.error("invalid name on: {}".format(top_group))
|
||||
cls.log.error("name doesn't match regex {}".format(regex))
|
||||
invalid.append(top_group)
|
||||
else:
|
||||
if "asset" in r.groupindex:
|
||||
if m.group("asset") != avalon.api.Session["AVALON_ASSET"]:
|
||||
cls.log.error("Invalid asset name in top level group.")
|
||||
return top_group
|
||||
if "subset" in r.groupindex:
|
||||
if m.group("subset") != instance.data.get("subset"):
|
||||
cls.log.error("Invalid subset name in top level group.")
|
||||
return top_group
|
||||
if "project" in r.groupindex:
|
||||
if m.group("project") != avalon.api.Session["AVALON_PROJECT"]:
|
||||
cls.log.error("Invalid project name in top level group.")
|
||||
return top_group
|
||||
|
||||
descendants = cmds.listRelatives(content_instance,
|
||||
allDescendents=True,
|
||||
fullPath=True) or []
|
||||
|
||||
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
|
||||
trns = cmds.ls(descendants, long=False, type=('transform'))
|
||||
trns = cmds.ls(descendants, long=False, type='transform')
|
||||
|
||||
# filter out groups
|
||||
filter = [node for node in trns if not is_group(node)]
|
||||
filtered = [node for node in trns if not is_group(node)]
|
||||
|
||||
# load shader list file as utf-8
|
||||
if cls.material_file:
|
||||
shader_file = open(cls.material_file, "r")
|
||||
shaders = shader_file.readlines()
|
||||
shaders = []
|
||||
if not use_db:
|
||||
if cls.material_file:
|
||||
if os.path.isfile(cls.material_file):
|
||||
shader_file = open(cls.material_file, "r")
|
||||
shaders = shader_file.readlines()
|
||||
shader_file.close()
|
||||
else:
|
||||
cls.log.error("Missing shader name definition file.")
|
||||
return True
|
||||
else:
|
||||
client = OpenPypeMongoConnection.get_mongo_client()
|
||||
fs = gridfs.GridFS(client[os.getenv("OPENPYPE_DATABASE_NAME")])
|
||||
shader_file = fs.find_one({"filename": cls.database_file})
|
||||
if not shader_file:
|
||||
cls.log.error("Missing shader name definition in database.")
|
||||
return True
|
||||
shaders = shader_file.read().splitlines()
|
||||
shader_file.close()
|
||||
|
||||
# strip line endings from list
|
||||
shaders = map(lambda s: s.rstrip(), shaders)
|
||||
|
||||
# compile regex for testing names
|
||||
r = re.compile(cls.regex)
|
||||
regex = cls.regex
|
||||
r = re.compile(regex)
|
||||
|
||||
for obj in filter:
|
||||
for obj in filtered:
|
||||
cls.log.info("testing: {}".format(obj))
|
||||
m = r.match(obj)
|
||||
if m is None:
|
||||
cls.log.error("invalid name on: {}".format(obj))
|
||||
|
|
@ -74,7 +127,7 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
# if we have shader files and shader named group is in
|
||||
# regex, test this group against names in shader file
|
||||
if 'shader' in r.groupindex and shaders:
|
||||
if "shader" in r.groupindex and shaders:
|
||||
try:
|
||||
if not m.group('shader') in shaders:
|
||||
cls.log.error(
|
||||
|
|
@ -90,8 +143,8 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
invalid = self.get_invalid(instance)
|
||||
|
||||
if invalid:
|
||||
raise RuntimeError("Model naming is invalid. See log.")
|
||||
raise RuntimeError("Model naming is invalid. See the log.")
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class CollectInstances(pyblish.api.InstancePlugin):
|
|||
})
|
||||
for subset, properities in self.subsets.items():
|
||||
version = properities.get("version")
|
||||
if version and version == 0:
|
||||
if version == 0:
|
||||
properities.pop("version")
|
||||
|
||||
# adding Review-able instance
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
|
||||
# return if any
|
||||
if entity_type:
|
||||
return {"entityType": entity_type, "entityName": value}
|
||||
return {"entity_type": entity_type, "entity_name": value}
|
||||
|
||||
def rename_with_hierarchy(self, instance):
|
||||
search_text = ""
|
||||
|
|
@ -76,8 +76,8 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
# add current selection context hierarchy from standalonepublisher
|
||||
for entity in reversed(visual_hierarchy):
|
||||
parents.append({
|
||||
"entityType": entity["data"]["entityType"],
|
||||
"entityName": entity["name"]
|
||||
"entity_type": entity["data"]["entityType"],
|
||||
"entity_name": entity["name"]
|
||||
})
|
||||
|
||||
if self.shot_add_hierarchy:
|
||||
|
|
@ -98,7 +98,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
|
||||
# in case SP context is set to the same folder
|
||||
if (_index == 0) and ("folder" in parent_key) \
|
||||
and (parents[-1]["entityName"] == parent_filled):
|
||||
and (parents[-1]["entity_name"] == parent_filled):
|
||||
self.log.debug(f" skiping : {parent_filled}")
|
||||
continue
|
||||
|
||||
|
|
@ -280,9 +280,9 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin):
|
|||
|
||||
for parent in reversed(parents):
|
||||
next_dict = {}
|
||||
parent_name = parent["entityName"]
|
||||
parent_name = parent["entity_name"]
|
||||
next_dict[parent_name] = {}
|
||||
next_dict[parent_name]["entity_type"] = parent["entityType"]
|
||||
next_dict[parent_name]["entity_type"] = parent["entity_type"]
|
||||
next_dict[parent_name]["childs"] = actual
|
||||
actual = next_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
|
|||
]
|
||||
|
||||
args = [
|
||||
ffmpeg_path,
|
||||
f"\"{ffmpeg_path}\"",
|
||||
"-ss", str(start / fps),
|
||||
"-i", f"\"{video_file_path}\"",
|
||||
"-t", str(dur / fps)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import pyblish.api
|
||||
|
||||
from avalon.tvpaint import workio
|
||||
from openpype.api import version_up
|
||||
|
||||
|
||||
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
||||
"""Increment current workfile version."""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 1
|
||||
label = "Increment Workfile Version"
|
||||
optional = True
|
||||
hosts = ["tvpaint"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Publishing not succesfull so version is not increased.")
|
||||
|
||||
path = context.data["currentFile"]
|
||||
workio.save_file(version_up(path))
|
||||
self.log.info('Incrementing workfile version')
|
||||
|
|
@ -303,6 +303,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
key_values = {"families": family, "tasks": task_name}
|
||||
profile = filter_profiles(self.template_name_profiles, key_values,
|
||||
logger=self.log)
|
||||
|
||||
template_name = "publish"
|
||||
if profile:
|
||||
template_name = profile["template_name"]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Asset Name"
|
||||
label = "Validate Editorial Asset Name"
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ def _h264_codec_args(ffprobe_data):
|
|||
|
||||
output.extend(["-codec:v", "h264"])
|
||||
|
||||
bit_rate = ffprobe_data.get("bit_rate")
|
||||
if bit_rate:
|
||||
output.extend(["-b:v", bit_rate])
|
||||
|
||||
pix_fmt = ffprobe_data.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
output.extend(["-pix_fmt", pix_fmt])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"publish": {
|
||||
"ValidateEditorialAssetName": {
|
||||
"enabled": true,
|
||||
"optional": false
|
||||
},
|
||||
"ValidateVersion": {
|
||||
"enabled": true,
|
||||
"optional": false
|
||||
},
|
||||
"IntegrateHeroVersion": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,22 @@
|
|||
"workfile": "ma",
|
||||
"yetiRig": "ma"
|
||||
},
|
||||
"scriptsmenu": {
|
||||
"name": "OpenPype Tools",
|
||||
"definition": [
|
||||
{
|
||||
"type": "action",
|
||||
"command": "import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()",
|
||||
"sourcetype": "python",
|
||||
"title": "Edit shader name definitions",
|
||||
"tooltip": "Edit shader name definitions used in validation and renaming.",
|
||||
"tags": [
|
||||
"pipeline",
|
||||
"shader"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"create": {
|
||||
"CreateLook": {
|
||||
"enabled": true,
|
||||
|
|
@ -148,12 +164,14 @@
|
|||
},
|
||||
"ValidateModelName": {
|
||||
"enabled": false,
|
||||
"database": true,
|
||||
"material_file": {
|
||||
"windows": "",
|
||||
"darwin": "",
|
||||
"linux": ""
|
||||
},
|
||||
"regex": "(.*)_(\\\\d)*_(.*)_(GEO)"
|
||||
"regex": "(.*)_(\\d)*_(?P<shader>.*)_(GEO)",
|
||||
"top_level_regex": ".*_GRP"
|
||||
},
|
||||
"ValidateTransformNamingSuffix": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@
|
|||
},
|
||||
"shot_add_tasks": {}
|
||||
},
|
||||
"shot_add_tasks": {
|
||||
"CollectInstances": {
|
||||
"custom_start_frame": 0,
|
||||
"timeline_frame_start": 900000,
|
||||
"timeline_frame_offset": 0,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_scriptsmenu"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_create"
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "shot_add_tasks",
|
||||
"key": "CollectInstances",
|
||||
"label": "Collect Clip Instances",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
|
|
|
|||
|
|
@ -4,6 +4,46 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateEditorialAssetName",
|
||||
"label": "Validate Editorial Asset Name",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateVersion",
|
||||
"label": "Validate Version",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -147,9 +147,14 @@
|
|||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "database",
|
||||
"label": "Use database shader name definitions"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Path to material file defining list of material names to check. This is material name per line simple text file.<br/>It will be checked against named group <b>shader</b> in your <em>Validation regex</em>.<p>For example: <br/> <code>^.*(?P=<shader>.+)_GEO</code></p>"
|
||||
"label": "Path to material file defining list of material names to check. This is material name per line simple text file.<br/>It will be checked against named group <b>shader</b> in your <em>Validation regex</em>.<p>For example: <br/> <code>^.*(?P=<shader>.+)_GEO</code></p>This is used instead of database definitions if they are disabled."
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
|
|
@ -162,6 +167,15 @@
|
|||
"type": "text",
|
||||
"key": "regex",
|
||||
"label": "Validation regex"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Regex for validating name of top level group name.<br/>You can use named capturing groups:<br/><code>(?P<asset>.*)</code> for Asset name<br/><code>(?P<subset>.*)</code> for Subset<br/><code>(?P<project>.*)</code> for project<br/><p>For example to check for asset in name so <code>*_some_asset_name_GRP</code> is valid, use:<br/><code>.*?_(?P<asset>.*)_GEO</code>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "top_level_regex",
|
||||
"label": "Top level group name regex"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "scriptsmenu",
|
||||
"label": "Scripts Menu Definition",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "name",
|
||||
"label": "Menu Name"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "raw-json",
|
||||
"key": "definition",
|
||||
"label": "Menu definition",
|
||||
"is_list": true
|
||||
}
|
||||
]
|
||||
}
|
||||
5
openpype/vendor/python/common/scriptsmenu/__init__.py
vendored
Normal file
5
openpype/vendor/python/common/scriptsmenu/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .scriptsmenu import ScriptsMenu
|
||||
from . import version
|
||||
|
||||
__all__ = ["ScriptsMenu"]
|
||||
__version__ = version.version
|
||||
207
openpype/vendor/python/common/scriptsmenu/action.py
vendored
Normal file
207
openpype/vendor/python/common/scriptsmenu/action.py
vendored
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import os
|
||||
|
||||
from .vendor.Qt import QtWidgets
|
||||
|
||||
|
||||
class Action(QtWidgets.QAction):
|
||||
"""Custom Action widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
QtWidgets.QAction.__init__(self, parent)
|
||||
|
||||
self._root = None
|
||||
self._tags = list()
|
||||
self._command = None
|
||||
self._sourcetype = None
|
||||
self._iconfile = None
|
||||
self._label = None
|
||||
|
||||
self._COMMAND = """import imp
|
||||
f, filepath, descr = imp.find_module('{module_name}', ['{dirname}'])
|
||||
module = imp.load_module('{module_name}', f, filepath, descr)
|
||||
module.{module_name}()"""
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
return self._root
|
||||
|
||||
@root.setter
|
||||
def root(self, value):
|
||||
self._root = value
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self._tags
|
||||
|
||||
@tags.setter
|
||||
def tags(self, value):
|
||||
self._tags = value
|
||||
|
||||
@property
|
||||
def command(self):
|
||||
return self._command
|
||||
|
||||
@command.setter
|
||||
def command(self, value):
|
||||
"""
|
||||
Store the command in the QAction
|
||||
|
||||
Args:
|
||||
value (str): the full command which will be executed when clicked
|
||||
|
||||
Return:
|
||||
None
|
||||
"""
|
||||
self._command = value
|
||||
|
||||
@property
|
||||
def sourcetype(self):
|
||||
return self._sourcetype
|
||||
|
||||
@sourcetype.setter
|
||||
def sourcetype(self, value):
|
||||
"""
|
||||
Set the command type to get the correct execution of the command given
|
||||
|
||||
Args:
|
||||
value (str): the name of the command type
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
self._sourcetype = value
|
||||
|
||||
@property
|
||||
def iconfile(self):
|
||||
return self._iconfile
|
||||
|
||||
@iconfile.setter
|
||||
def iconfile(self, value):
|
||||
"""Store the path to the image file which needs to be displayed
|
||||
|
||||
Args:
|
||||
value (str): the path to the image
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self._iconfile = value
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._label
|
||||
|
||||
@label.setter
|
||||
def label(self, value):
|
||||
"""
|
||||
Set the abbreviation which will be used as overlay text in the shelf
|
||||
|
||||
Args:
|
||||
value (str): an abbreviation of the name
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
self._label = value
|
||||
|
||||
def run_command(self):
|
||||
"""
|
||||
Run the command of the instance or copy the command to the active shelf
|
||||
based on the current modifiers.
|
||||
|
||||
If callbacks have been registered with fouind modifier integer the
|
||||
function will trigger all callbacks. When a callback function returns a
|
||||
non zero integer it will not execute the action's command
|
||||
|
||||
"""
|
||||
|
||||
# get the current application and its linked keyboard modifiers
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
|
||||
# If the menu has a callback registered for the current modifier
|
||||
# we run the callback instead of the action itself.
|
||||
registered = self._root.registered_callbacks
|
||||
callbacks = registered.get(int(modifiers), [])
|
||||
for callback in callbacks:
|
||||
signal = callback(self)
|
||||
if signal != 0:
|
||||
# Exit function on non-zero return code
|
||||
return
|
||||
|
||||
exec(self.process_command())
|
||||
|
||||
def process_command(self):
|
||||
"""
|
||||
Check if the command is a file which needs to be launched and if it
|
||||
has a relative path, if so return the full path by expanding
|
||||
environment variables. Wrap any mel command in a executable string
|
||||
for Python and return the string if the source type is
|
||||
|
||||
Add your own source type and required process to ensure callback
|
||||
is stored correctly.
|
||||
|
||||
An example of a process is the sourcetype is MEL
|
||||
(Maya Embedded Language) as Python cannot run it on its own so it
|
||||
needs to be wrapped in a string in which we explicitly import mel and
|
||||
run it as a mel.eval. The string is then parsed to python as
|
||||
exec("command").
|
||||
|
||||
Returns:
|
||||
str: a clean command which can be used
|
||||
|
||||
"""
|
||||
if self._sourcetype == "python":
|
||||
return self._command
|
||||
|
||||
if self._sourcetype == "mel":
|
||||
# Escape single quotes
|
||||
conversion = self._command.replace("'", "\\'")
|
||||
return "import maya; maya.mel.eval('{}')".format(conversion)
|
||||
|
||||
if self._sourcetype == "file":
|
||||
if os.path.isabs(self._command):
|
||||
filepath = self._command
|
||||
else:
|
||||
filepath = os.path.normpath(os.path.expandvars(self._command))
|
||||
|
||||
return self._wrap_filepath(filepath)
|
||||
|
||||
def has_tag(self, tag):
|
||||
"""Check whether the tag matches with the action's tags.
|
||||
|
||||
A partial match will also return True, for example tag `a` will match
|
||||
correctly with the `ape` tag on the Action.
|
||||
|
||||
Args:
|
||||
tag (str): The tag
|
||||
|
||||
Returns
|
||||
bool: Whether the action is tagged with given tag
|
||||
|
||||
"""
|
||||
|
||||
for tagitem in self.tags:
|
||||
if tag not in tagitem:
|
||||
continue
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _wrap_filepath(self, file_path):
|
||||
"""Create a wrapped string for the python command
|
||||
|
||||
Args:
|
||||
file_path (str): the filepath of a script
|
||||
|
||||
Returns:
|
||||
str: the wrapped command
|
||||
"""
|
||||
|
||||
dirname = os.path.dirname(r"{}".format(file_path))
|
||||
dirpath = dirname.replace("\\", "/")
|
||||
module_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
|
||||
return self._COMMAND.format(module_name=module_name, dirname=dirpath)
|
||||
54
openpype/vendor/python/common/scriptsmenu/launchformari.py
vendored
Normal file
54
openpype/vendor/python/common/scriptsmenu/launchformari.py
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
# Import third-party modules
|
||||
from vendor.Qt import QtWidgets
|
||||
|
||||
# Import local modules
|
||||
import scriptsmenu
|
||||
|
||||
|
||||
def _mari_main_window():
|
||||
"""Get Mari main window.
|
||||
|
||||
Returns:
|
||||
MriMainWindow: Mari's main window.
|
||||
|
||||
"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if obj.metaObject().className() == 'MriMainWindow':
|
||||
return obj
|
||||
raise RuntimeError('Could not find Mari MainWindow instance')
|
||||
|
||||
|
||||
def _mari_main_menubar():
|
||||
"""Get Mari main menu bar.
|
||||
|
||||
Returns:
|
||||
Retrieve the main menubar of the Mari window.
|
||||
|
||||
"""
|
||||
mari_window = _mari_main_window()
|
||||
menubar = [
|
||||
i for i in mari_window.children() if isinstance(i, QtWidgets.QMenuBar)
|
||||
]
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def main(title="Scripts"):
|
||||
"""Build the main scripts menu in Mari.
|
||||
|
||||
Args:
|
||||
title (str): Name of the menu in the application.
|
||||
|
||||
Returns:
|
||||
scriptsmenu.ScriptsMenu: Instance object.
|
||||
|
||||
"""
|
||||
mari_main_bar = _mari_main_menubar()
|
||||
for mari_bar in mari_main_bar.children():
|
||||
if isinstance(mari_bar, scriptsmenu.ScriptsMenu):
|
||||
if mari_bar.title() == title:
|
||||
menu = mari_bar
|
||||
return menu
|
||||
menu = scriptsmenu.ScriptsMenu(title=title, parent=mari_main_bar)
|
||||
return menu
|
||||
137
openpype/vendor/python/common/scriptsmenu/launchformaya.py
vendored
Normal file
137
openpype/vendor/python/common/scriptsmenu/launchformaya.py
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import logging
|
||||
|
||||
import maya.cmds as cmds
|
||||
import maya.mel as mel
|
||||
|
||||
import scriptsmenu
|
||||
from .vendor.Qt import QtCore, QtWidgets
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_repeat_last(action):
|
||||
"""Register the action in repeatLast to ensure the RepeatLast hotkey works
|
||||
|
||||
Args:
|
||||
action (action.Action): Action wigdet instance
|
||||
|
||||
Returns:
|
||||
int: 0
|
||||
|
||||
"""
|
||||
command = action.process_command()
|
||||
command = command.replace("\n", "; ")
|
||||
# Register command to Maya (mel)
|
||||
cmds.repeatLast(addCommand='python("{}")'.format(command),
|
||||
addCommandLabel=action.label)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def to_shelf(action):
|
||||
"""Copy clicked menu item to the currently active Maya shelf
|
||||
Args:
|
||||
action (action.Action): the action instance which is clicked
|
||||
|
||||
Returns:
|
||||
int: 1
|
||||
|
||||
"""
|
||||
|
||||
shelftoplevel = mel.eval("$gShelfTopLevel = $gShelfTopLevel;")
|
||||
current_active_shelf = cmds.tabLayout(shelftoplevel,
|
||||
query=True,
|
||||
selectTab=True)
|
||||
|
||||
cmds.shelfButton(command=action.process_command(),
|
||||
sourceType="python",
|
||||
parent=current_active_shelf,
|
||||
image=action.iconfile or "pythonFamily.png",
|
||||
annotation=action.statusTip(),
|
||||
imageOverlayLabel=action.label or "")
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def _maya_main_window():
|
||||
"""Return Maya's main window"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if obj.objectName() == 'MayaWindow':
|
||||
return obj
|
||||
raise RuntimeError('Could not find MayaWindow instance')
|
||||
|
||||
|
||||
def _maya_main_menubar():
|
||||
"""Retrieve the main menubar of the Maya window"""
|
||||
mayawindow = _maya_main_window()
|
||||
menubar = [i for i in mayawindow.children()
|
||||
if isinstance(i, QtWidgets.QMenuBar)]
|
||||
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def find_scripts_menu(title, parent):
|
||||
"""
|
||||
Check if the menu exists with the given title in the parent
|
||||
|
||||
Args:
|
||||
title (str): the title name of the scripts menu
|
||||
|
||||
parent (QtWidgets.QMenuBar): the menubar to check
|
||||
|
||||
Returns:
|
||||
QtWidgets.QMenu or None
|
||||
|
||||
"""
|
||||
|
||||
menu = None
|
||||
search = [i for i in parent.children() if
|
||||
isinstance(i, scriptsmenu.ScriptsMenu)
|
||||
and i.title() == title]
|
||||
|
||||
if search:
|
||||
assert len(search) < 2, ("Multiple instances of menu '{}' "
|
||||
"in menu bar".format(title))
|
||||
menu = search[0]
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def main(title="Scripts", parent=None, objectName=None):
|
||||
"""Build the main scripts menu in Maya
|
||||
|
||||
Args:
|
||||
title (str): name of the menu in the application
|
||||
|
||||
parent (QtWidgets.QtMenuBar): the parent object for the menu
|
||||
|
||||
objectName (str): custom objectName for scripts menu
|
||||
|
||||
Returns:
|
||||
scriptsmenu.ScriptsMenu instance
|
||||
|
||||
"""
|
||||
|
||||
mayamainbar = parent or _maya_main_menubar()
|
||||
try:
|
||||
# check menu already exists
|
||||
menu = find_scripts_menu(title, mayamainbar)
|
||||
if not menu:
|
||||
log.info("Attempting to build menu ...")
|
||||
object_name = objectName or title.lower()
|
||||
menu = scriptsmenu.ScriptsMenu(title=title,
|
||||
parent=mayamainbar,
|
||||
objectName=object_name)
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return
|
||||
|
||||
# Register control + shift callback to add to shelf (maya behavior)
|
||||
modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
menu.register_callback(int(modifiers), to_shelf)
|
||||
|
||||
menu.register_callback(0, register_repeat_last)
|
||||
|
||||
return menu
|
||||
36
openpype/vendor/python/common/scriptsmenu/launchfornuke.py
vendored
Normal file
36
openpype/vendor/python/common/scriptsmenu/launchfornuke.py
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import scriptsmenu
|
||||
from .vendor.Qt import QtWidgets
|
||||
|
||||
|
||||
def _nuke_main_window():
|
||||
"""Return Nuke's main window"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if (obj.inherits('QMainWindow') and
|
||||
obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):
|
||||
return obj
|
||||
raise RuntimeError('Could not find Nuke MainWindow instance')
|
||||
|
||||
|
||||
def _nuke_main_menubar():
|
||||
"""Retrieve the main menubar of the Nuke window"""
|
||||
nuke_window = _nuke_main_window()
|
||||
menubar = [i for i in nuke_window.children()
|
||||
if isinstance(i, QtWidgets.QMenuBar)]
|
||||
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def main(title="Scripts"):
|
||||
# Register control + shift callback to add to shelf (Nuke behavior)
|
||||
# modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
# menu.register_callback(modifiers, to_shelf)
|
||||
nuke_main_bar = _nuke_main_menubar()
|
||||
for nuke_bar in nuke_main_bar.children():
|
||||
if isinstance(nuke_bar, scriptsmenu.ScriptsMenu):
|
||||
if nuke_bar.title() == title:
|
||||
menu = nuke_bar
|
||||
return menu
|
||||
|
||||
menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar)
|
||||
return menu
|
||||
316
openpype/vendor/python/common/scriptsmenu/scriptsmenu.py
vendored
Normal file
316
openpype/vendor/python/common/scriptsmenu/scriptsmenu.py
vendored
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import os
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from .vendor.Qt import QtWidgets, QtCore
|
||||
from . import action
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScriptsMenu(QtWidgets.QMenu):
|
||||
"""A Qt menu that displays a list of searchable actions"""
|
||||
|
||||
updated = QtCore.Signal(QtWidgets.QMenu)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize Scripts menu
|
||||
|
||||
Args:
|
||||
title (str): the name of the root menu which will be created
|
||||
|
||||
parent (QtWidgets.QObject) : the QObject to parent the menu to
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
QtWidgets.QMenu.__init__(self, *args, **kwargs)
|
||||
|
||||
self.searchbar = None
|
||||
self.update_action = None
|
||||
|
||||
self._script_actions = []
|
||||
self._callbacks = defaultdict(list)
|
||||
|
||||
# Automatically add it to the parent menu
|
||||
parent = kwargs.get("parent", None)
|
||||
if parent:
|
||||
parent.addMenu(self)
|
||||
|
||||
objectname = kwargs.get("objectName", "scripts")
|
||||
title = kwargs.get("title", "Scripts")
|
||||
self.setObjectName(objectname)
|
||||
self.setTitle(title)
|
||||
|
||||
# add default items in the menu
|
||||
self.create_default_items()
|
||||
|
||||
def on_update(self):
|
||||
self.updated.emit(self)
|
||||
|
||||
@property
|
||||
def registered_callbacks(self):
|
||||
return self._callbacks.copy()
|
||||
|
||||
def create_default_items(self):
|
||||
"""Add a search bar to the top of the menu given"""
|
||||
|
||||
# create widget and link function
|
||||
searchbar = QtWidgets.QLineEdit()
|
||||
searchbar.setFixedWidth(120)
|
||||
searchbar.setPlaceholderText("Search ...")
|
||||
searchbar.textChanged.connect(self._update_search)
|
||||
self.searchbar = searchbar
|
||||
|
||||
# create widget holder
|
||||
searchbar_action = QtWidgets.QWidgetAction(self)
|
||||
|
||||
# add widget to widget holder
|
||||
searchbar_action.setDefaultWidget(self.searchbar)
|
||||
searchbar_action.setObjectName("Searchbar")
|
||||
|
||||
# add update button and link function
|
||||
update_action = QtWidgets.QAction(self)
|
||||
update_action.setObjectName("Update Scripts")
|
||||
update_action.setText("Update Scripts")
|
||||
update_action.setVisible(False)
|
||||
update_action.triggered.connect(self.on_update)
|
||||
self.update_action = update_action
|
||||
|
||||
# add action to menu
|
||||
self.addAction(searchbar_action)
|
||||
self.addAction(update_action)
|
||||
|
||||
# add separator object
|
||||
separator = self.addSeparator()
|
||||
separator.setObjectName("separator")
|
||||
|
||||
def add_menu(self, title, parent=None):
|
||||
"""Create a sub menu for a parent widget
|
||||
|
||||
Args:
|
||||
parent(QtWidgets.QWidget): the object to parent the menu to
|
||||
|
||||
title(str): the title of the menu
|
||||
|
||||
Returns:
|
||||
QtWidget.QMenu instance
|
||||
"""
|
||||
|
||||
if not parent:
|
||||
parent = self
|
||||
|
||||
menu = QtWidgets.QMenu(parent, title)
|
||||
menu.setTitle(title)
|
||||
menu.setObjectName(title)
|
||||
menu.setTearOffEnabled(True)
|
||||
parent.addMenu(menu)
|
||||
|
||||
return menu
|
||||
|
||||
def add_script(self, parent, title, command, sourcetype, icon=None,
|
||||
tags=None, label=None, tooltip=None):
|
||||
"""Create an action item which runs a script when clicked
|
||||
|
||||
Args:
|
||||
parent (QtWidget.QWidget): The widget to parent the item to
|
||||
|
||||
title (str): The text which will be displayed in the menu
|
||||
|
||||
command (str): The command which needs to be run when the item is
|
||||
clicked.
|
||||
|
||||
sourcetype (str): The type of command, the way the command is
|
||||
processed is based on the source type.
|
||||
|
||||
icon (str): The file path of an icon to display with the menu item
|
||||
|
||||
tags (list, tuple): Keywords which describe the action
|
||||
|
||||
label (str): A short description of the script which will be displayed
|
||||
when hovering over the menu item
|
||||
|
||||
tooltip (str): A tip for the user about the usage fo the tool
|
||||
|
||||
Returns:
|
||||
QtWidget.QAction instance
|
||||
|
||||
"""
|
||||
|
||||
assert tags is None or isinstance(tags, (list, tuple))
|
||||
# Ensure tags is a list
|
||||
tags = list() if tags is None else list(tags)
|
||||
tags.append(title.lower())
|
||||
|
||||
assert icon is None or isinstance(icon, str), (
|
||||
"Invalid data type for icon, supported : None, string")
|
||||
|
||||
# create new action
|
||||
script_action = action.Action(parent)
|
||||
script_action.setText(title)
|
||||
script_action.setObjectName(title)
|
||||
script_action.tags = tags
|
||||
|
||||
# link action to root for callback library
|
||||
script_action.root = self
|
||||
|
||||
# Set up the command
|
||||
script_action.sourcetype = sourcetype
|
||||
script_action.command = command
|
||||
|
||||
try:
|
||||
script_action.process_command()
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError("Script action can't be "
|
||||
"processed: {}".format(e))
|
||||
|
||||
if icon:
|
||||
iconfile = os.path.expandvars(icon)
|
||||
script_action.iconfile = iconfile
|
||||
script_action_icon = QtWidgets.QIcon(iconfile)
|
||||
script_action.setIcon(script_action_icon)
|
||||
|
||||
if label:
|
||||
script_action.label = label
|
||||
|
||||
if tooltip:
|
||||
script_action.setStatusTip(tooltip)
|
||||
|
||||
script_action.triggered.connect(script_action.run_command)
|
||||
parent.addAction(script_action)
|
||||
|
||||
# Add to our searchable actions
|
||||
self._script_actions.append(script_action)
|
||||
|
||||
return script_action
|
||||
|
||||
def build_from_configuration(self, parent, configuration):
|
||||
"""Process the configurations and store the configuration
|
||||
|
||||
This creates all submenus from a configuration.json file.
|
||||
|
||||
When the configuration holds the key `main` all scripts under `main` will
|
||||
be added to the main menu first before adding the rest
|
||||
|
||||
Args:
|
||||
parent (ScriptsMenu): script menu instance
|
||||
configuration (list): A ScriptsMenu configuration list
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
for item in configuration:
|
||||
assert isinstance(item, dict), "Configuration is wrong!"
|
||||
|
||||
# skip items which have no `type` key
|
||||
item_type = item.get('type', None)
|
||||
if not item_type:
|
||||
log.warning("Missing 'type' from configuration item")
|
||||
continue
|
||||
|
||||
# add separator
|
||||
# Special behavior for separators
|
||||
if item_type == "separator":
|
||||
parent.addSeparator()
|
||||
|
||||
# add submenu
|
||||
# items should hold a collection of submenu items (dict)
|
||||
elif item_type == "menu":
|
||||
assert "items" in item, "Menu is missing 'items' key"
|
||||
menu = self.add_menu(parent=parent, title=item["title"])
|
||||
self.build_from_configuration(menu, item["items"])
|
||||
|
||||
# add script
|
||||
elif item_type == "action":
|
||||
# filter out `type` from the item dict
|
||||
config = {key: value for key, value in
|
||||
item.items() if key != "type"}
|
||||
|
||||
self.add_script(parent=parent, **config)
|
||||
|
||||
def set_update_visible(self, state):
|
||||
self.update_action.setVisible(state)
|
||||
|
||||
def clear_menu(self):
|
||||
"""Clear all menu items which are not default
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Set up a more robust implementation for this
|
||||
# Delete all except the first three actions
|
||||
for _action in self.actions()[3:]:
|
||||
self.removeAction(_action)
|
||||
|
||||
def register_callback(self, modifiers, callback):
|
||||
self._callbacks[modifiers].append(callback)
|
||||
|
||||
def _update_search(self, search):
|
||||
"""Hide all the samples which do not match the user's import
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
if not search:
|
||||
for action in self._script_actions:
|
||||
action.setVisible(True)
|
||||
else:
|
||||
for action in self._script_actions:
|
||||
if not action.has_tag(search.lower()):
|
||||
action.setVisible(False)
|
||||
|
||||
# Set visibility for all submenus
|
||||
for action in self.actions():
|
||||
if not action.menu():
|
||||
continue
|
||||
|
||||
menu = action.menu()
|
||||
visible = any(action.isVisible() for action in menu.actions())
|
||||
action.setVisible(visible)
|
||||
|
||||
|
||||
def load_configuration(path):
|
||||
"""Load the configuration from a file
|
||||
|
||||
Read out the JSON file which will dictate the structure of the scripts menu
|
||||
|
||||
Args:
|
||||
path (str): file path of the .JSON file
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise AttributeError("Given configuration is not "
|
||||
"a file!\n'{}'".format(path))
|
||||
|
||||
extension = os.path.splitext(path)[-1]
|
||||
if extension != ".json":
|
||||
raise AttributeError("Given configuration file has unsupported "
|
||||
"file type, provide a .json file")
|
||||
|
||||
# retrieve and store config
|
||||
with open(path, "r") as f:
|
||||
configuration = json.load(f)
|
||||
|
||||
return configuration
|
||||
|
||||
|
||||
def application(configuration, parent):
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
scriptsmenu = ScriptsMenu(configuration, parent)
|
||||
scriptsmenu.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
1989
openpype/vendor/python/common/scriptsmenu/vendor/Qt.py
vendored
Normal file
1989
openpype/vendor/python/common/scriptsmenu/vendor/Qt.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
0
openpype/vendor/python/common/scriptsmenu/vendor/__init__.py
vendored
Normal file
0
openpype/vendor/python/common/scriptsmenu/vendor/__init__.py
vendored
Normal file
9
openpype/vendor/python/common/scriptsmenu/version.py
vendored
Normal file
9
openpype/vendor/python/common/scriptsmenu/version.py
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 5
|
||||
VERSION_PATCH = 1
|
||||
|
||||
|
||||
version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
|
||||
__version__ = version
|
||||
|
||||
__all__ = ['version', '__version__']
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.3.0-nightly.6"
|
||||
__version__ = "3.3.0-nightly.7"
|
||||
|
|
|
|||
|
|
@ -80,17 +80,6 @@ function Show-PSWarning() {
|
|||
}
|
||||
}
|
||||
|
||||
function Install-Poetry() {
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor Green
|
||||
Write-Host "Installing Poetry ... "
|
||||
$python = "python"
|
||||
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
|
||||
$python = & pyenv which python
|
||||
}
|
||||
$env:POETRY_HOME="$openpype_root\.poetry"
|
||||
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) -
|
||||
}
|
||||
|
||||
$art = @"
|
||||
|
||||
. . .. . ..
|
||||
|
|
|
|||
|
|
@ -62,9 +62,12 @@ function Test-Python() {
|
|||
Write-Host "Detecting host Python ... " -NoNewline
|
||||
$python = "python"
|
||||
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
|
||||
$python = & pyenv which python
|
||||
$pyenv_python = & pyenv which python
|
||||
if (Test-Path -PathType Leaf -Path "$($pyenv_python)") {
|
||||
$python = $pyenv_python
|
||||
}
|
||||
}
|
||||
if (-not (Get-Command "python3" -ErrorAction SilentlyContinue)) {
|
||||
if (-not (Get-Command $python -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "!!! Python not detected" -ForegroundColor red
|
||||
Set-Location -Path $current_dir
|
||||
Exit-WithCode 1
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ title: Maya
|
|||
sidebar_label: Maya
|
||||
---
|
||||
|
||||
## Maya
|
||||
## Publish Plugins
|
||||
|
||||
### Publish Plugins
|
||||
### Render Settings Validator
|
||||
|
||||
#### Render Settings Validator (`ValidateRenderSettings`)
|
||||
`ValidateRenderSettings`
|
||||
|
||||
Render Settings Validator is here to make sure artists will submit renders
|
||||
we correct settings. Some of these settings are needed by OpenPype but some
|
||||
|
|
@ -49,4 +49,49 @@ Arnolds Camera (AA) samples to 6.
|
|||
Note that `aiOptions` is not the name of node but rather its type. For renderers there is usually
|
||||
just one instance of this node type but if that is not so, validator will go through all its
|
||||
instances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman**
|
||||
it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`.
|
||||
it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`.
|
||||
|
||||
### Model Name Validator
|
||||
|
||||
`ValidateRenderSettings`
|
||||
|
||||
This validator can enforce specific names for model members. It will check them against **Validation Regex**.
|
||||
There is special group in that regex - **shader**. If present, it will take that part of the name as shader name
|
||||
and it will compare it with list of shaders defined either in file name specified in **Material File** or from
|
||||
database file that is per project and can be directly edited from Maya's *OpenPype Tools > Edit Shader name definitions* when
|
||||
**Use database shader name definitions** is on. This list defines simply as one shader name per line.
|
||||
|
||||

|
||||
|
||||
For example - you are using default regex `(.*)_(\d)*_(?P<shader>.*)_(GEO)` and you have two shaders defined
|
||||
in either file or database `foo` and `bar`.
|
||||
|
||||
Object named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too.
|
||||
|
||||
#### Top level group name
|
||||
There is a validation for top level group name too. You can specify whatever regex you'd like to use. Default will
|
||||
pass everything with `_GRP` suffix. You can use *named capturing groups* to validate against specific data. If you
|
||||
put `(?P<asset>.*)` it will try to match everything captured in that group against current asset name. Likewise you can
|
||||
use it for **subset** and **project** - `(?P<subset>.*)` and `(?P<project>.*)`.
|
||||
|
||||
**Example**
|
||||
|
||||
You are working on asset (shot) `0030_OGC_0190`. You have this regex in **Top level group name**:
|
||||
```regexp
|
||||
.*?_(?P<asset>.*)_GRP
|
||||
```
|
||||
|
||||
When you publish your model with top group named like `foo_GRP` it will fail. But with `foo_0030_OGC_0190_GRP` it will pass.
|
||||
|
||||
:::info About regex
|
||||
All regexes used here are in Python variant.
|
||||
:::
|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||

|
||||
|
||||
:::note Work in progress
|
||||
This is still work in progress. Menu definition will be handled more friendly with widgets and not
|
||||
raw json.
|
||||
:::
|
||||
BIN
website/docs/assets/maya-admin_model_name_validator.png
Normal file
BIN
website/docs/assets/maya-admin_model_name_validator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
website/docs/assets/maya-admin_scriptsmenu.png
Normal file
BIN
website/docs/assets/maya-admin_scriptsmenu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Loading…
Add table
Add a link
Reference in a new issue