mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into houdini_opengl
This commit is contained in:
commit
c0ece932c3
13 changed files with 275 additions and 74 deletions
2
.github/workflows/update_bug_report.yml
vendored
2
.github/workflows/update_bug_report.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.release.target_commitish }}
|
||||
- name: Update version
|
||||
uses: ShaMan123/gha-populate-form-version@v2.0.2
|
||||
uses: ynput/gha-populate-form-version@main
|
||||
with:
|
||||
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
registry: github
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ from openpype.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
Anatomy,
|
||||
)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.settings import get_project_settings
|
||||
from .pipeline import containerise
|
||||
from . import lib
|
||||
|
|
@ -82,6 +84,44 @@ def get_reference_node_parents(ref):
|
|||
return parents
|
||||
|
||||
|
||||
def get_custom_namespace(custom_namespace):
|
||||
"""Return unique namespace.
|
||||
|
||||
The input namespace can contain a single group
|
||||
of '#' number tokens to indicate where the namespace's
|
||||
unique index should go. The amount of tokens defines
|
||||
the zero padding of the number, e.g ### turns into 001.
|
||||
|
||||
Warning: Note that a namespace will always be
|
||||
prefixed with a _ if it starts with a digit
|
||||
|
||||
Example:
|
||||
>>> get_custom_namespace("myspace_##_")
|
||||
# myspace_01_
|
||||
>>> get_custom_namespace("##_myspace")
|
||||
# _01_myspace
|
||||
>>> get_custom_namespace("myspace##")
|
||||
# myspace01
|
||||
|
||||
"""
|
||||
split = re.split("([#]+)", custom_namespace, 1)
|
||||
|
||||
if len(split) == 3:
|
||||
base, padding, suffix = split
|
||||
padding = "%0{}d".format(len(padding))
|
||||
else:
|
||||
base = split[0]
|
||||
padding = "%02d" # default padding
|
||||
suffix = ""
|
||||
|
||||
return lib.unique_namespace(
|
||||
base,
|
||||
format=padding,
|
||||
prefix="_" if not base or base[0].isdigit() else "",
|
||||
suffix=suffix
|
||||
)
|
||||
|
||||
|
||||
class Creator(LegacyCreator):
|
||||
defaults = ['Main']
|
||||
|
||||
|
|
@ -143,15 +183,46 @@ class ReferenceLoader(Loader):
|
|||
assert os.path.exists(self.fname), "%s does not exist." % self.fname
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load']['reference_loader']
|
||||
loaded_containers = []
|
||||
|
||||
count = options.get("count") or 1
|
||||
for c in range(0, count):
|
||||
namespace = namespace or lib.unique_namespace(
|
||||
"{}_{}_".format(asset["name"], context["subset"]["name"]),
|
||||
prefix="_" if asset["name"][0].isdigit() else "",
|
||||
suffix="_",
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
raise LoadError("No group name specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
count = options.get("count") or 1
|
||||
|
||||
for c in range(0, count):
|
||||
namespace = get_custom_namespace(custom_namespace)
|
||||
group_name = "{}:{}".format(
|
||||
namespace,
|
||||
custom_group_name
|
||||
)
|
||||
|
||||
options['group_name'] = group_name
|
||||
|
||||
# Offset loaded subset
|
||||
if "offset" in options:
|
||||
|
|
@ -187,7 +258,7 @@ class ReferenceLoader(Loader):
|
|||
|
||||
return loaded_containers
|
||||
|
||||
def process_reference(self, context, name, namespace, data):
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
"""To be implemented by subclass"""
|
||||
raise NotImplementedError("Must be implemented by subclass")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def process_reference(self, context, name, namespace, data):
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
|
||||
import maya.cmds as cmds
|
||||
from openpype.hosts.maya.api.lib import unique_namespace
|
||||
|
|
@ -41,7 +41,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
groupReference=True,
|
||||
groupName="{}:{}".format(namespace, name),
|
||||
groupName=options['group_name'],
|
||||
reference=True,
|
||||
returnNewNodes=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,14 +125,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
except ValueError:
|
||||
family = "model"
|
||||
|
||||
group_name = "{}:_GRP".format(namespace)
|
||||
# True by default to keep legacy behaviours
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
with maintained_selection():
|
||||
cmds.loadPlugin("AbcImport.mll", quiet=True)
|
||||
file_url = self.prepare_root_value(self.fname,
|
||||
context["project"]["name"])
|
||||
|
||||
nodes = cmds.file(file_url,
|
||||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
def process_reference(
|
||||
self, context, name=None, namespace=None, options=None
|
||||
):
|
||||
|
||||
group_name = "{}:{}".format(namespace, name)
|
||||
group_name = options['group_name']
|
||||
with lib.maintained_selection():
|
||||
file_url = self.prepare_root_value(
|
||||
self.fname, context["project"]["name"]
|
||||
|
|
|
|||
|
|
@ -354,6 +354,61 @@ def publish_plugins_discover(paths=None):
|
|||
return result
|
||||
|
||||
|
||||
def _get_plugin_settings(host_name, project_settings, plugin, log):
|
||||
"""Get plugin settings based on host name and plugin name.
|
||||
|
||||
Args:
|
||||
host_name (str): Name of host.
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
plugin (pyliblish.Plugin): Plugin where settings are applied.
|
||||
log (logging.Logger): Logger to log messages.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Plugin settings {'attribute': 'value'}.
|
||||
"""
|
||||
|
||||
# Use project settings from host name category when available
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[host_name]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Settings category determined from path
|
||||
# - usually path is './<category>/plugins/publish/<plugin file>'
|
||||
# - category can be host name of addon name ('maya', 'deadline', ...)
|
||||
filepath = os.path.normpath(inspect.getsourcefile(plugin))
|
||||
|
||||
split_path = filepath.rsplit(os.path.sep, 5)
|
||||
if len(split_path) < 4:
|
||||
log.warning(
|
||||
'plugin path too short to extract host {}'.format(filepath)
|
||||
)
|
||||
return {}
|
||||
|
||||
category_from_file = split_path[-4]
|
||||
plugin_kind = split_path[-2]
|
||||
|
||||
# TODO: change after all plugins are moved one level up
|
||||
if category_from_file == "openpype":
|
||||
category_from_file = "global"
|
||||
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[category_from_file]
|
||||
[plugin_kind]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def filter_pyblish_plugins(plugins):
|
||||
"""Pyblish plugin filter which applies OpenPype settings.
|
||||
|
||||
|
|
@ -372,21 +427,21 @@ def filter_pyblish_plugins(plugins):
|
|||
# TODO: Don't use host from 'pyblish.api' but from defined host by us.
|
||||
# - kept becau on farm is probably used host 'shell' which propably
|
||||
# affect how settings are applied there
|
||||
host = pyblish.api.current_host()
|
||||
host_name = pyblish.api.current_host()
|
||||
project_name = os.environ.get("AVALON_PROJECT")
|
||||
|
||||
project_setting = get_project_settings(project_name)
|
||||
project_settings = get_project_settings(project_name)
|
||||
system_settings = get_system_settings()
|
||||
|
||||
# iterate over plugins
|
||||
for plugin in plugins[:]:
|
||||
# Apply settings to plugins
|
||||
if hasattr(plugin, "apply_settings"):
|
||||
# Use classmethod 'apply_settings'
|
||||
# - can be used to target settings from custom settings place
|
||||
# - skip default behavior when successful
|
||||
try:
|
||||
# Use classmethod 'apply_settings'
|
||||
# - can be used to target settings from custom settings place
|
||||
# - skip default behavior when successful
|
||||
plugin.apply_settings(project_setting, system_settings)
|
||||
continue
|
||||
plugin.apply_settings(project_settings, system_settings)
|
||||
|
||||
except Exception:
|
||||
log.warning(
|
||||
|
|
@ -395,53 +450,20 @@ def filter_pyblish_plugins(plugins):
|
|||
).format(plugin.__name__),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
try:
|
||||
config_data = (
|
||||
project_setting
|
||||
[host]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
else:
|
||||
# Automated
|
||||
plugin_settins = _get_plugin_settings(
|
||||
host_name, project_settings, plugin, log
|
||||
)
|
||||
except KeyError:
|
||||
# host determined from path
|
||||
file = os.path.normpath(inspect.getsourcefile(plugin))
|
||||
file = os.path.normpath(file)
|
||||
|
||||
split_path = file.split(os.path.sep)
|
||||
if len(split_path) < 4:
|
||||
log.warning(
|
||||
'plugin path too short to extract host {}'.format(file)
|
||||
)
|
||||
continue
|
||||
|
||||
host_from_file = split_path[-4]
|
||||
plugin_kind = split_path[-2]
|
||||
|
||||
# TODO: change after all plugins are moved one level up
|
||||
if host_from_file == "openpype":
|
||||
host_from_file = "global"
|
||||
|
||||
try:
|
||||
config_data = (
|
||||
project_setting
|
||||
[host_from_file]
|
||||
[plugin_kind]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
for option, value in config_data.items():
|
||||
if option == "enabled" and value is False:
|
||||
log.info('removing plugin {}'.format(plugin.__name__))
|
||||
plugins.remove(plugin)
|
||||
else:
|
||||
log.info('setting {}:{} on plugin {}'.format(
|
||||
for option, value in plugin_settins.items():
|
||||
log.info("setting {}:{} on plugin {}".format(
|
||||
option, value, plugin.__name__))
|
||||
|
||||
setattr(plugin, option, value)
|
||||
|
||||
# Remove disabled plugins
|
||||
if getattr(plugin, "enabled", True) is False:
|
||||
plugins.remove(plugin)
|
||||
|
||||
|
||||
def find_close_plugin(close_plugin_name, log):
|
||||
if close_plugin_name:
|
||||
|
|
|
|||
|
|
@ -1047,6 +1047,10 @@
|
|||
125,
|
||||
255
|
||||
]
|
||||
},
|
||||
"reference_loader": {
|
||||
"namespace": "{asset_name}_{subset}_##",
|
||||
"group_name": "_GRP"
|
||||
}
|
||||
},
|
||||
"workfile_build": {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,28 @@
|
|||
"key": "yetiRig"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "reference_loader",
|
||||
"label": "Reference Loader",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Namespace",
|
||||
"key": "namespace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Group name",
|
||||
"key": "group_name"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import collections
|
|||
import uuid
|
||||
import tempfile
|
||||
import shutil
|
||||
import inspect
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
|
|
@ -26,8 +27,8 @@ from openpype.pipeline import (
|
|||
PublishValidationError,
|
||||
KnownPublishError,
|
||||
registered_host,
|
||||
legacy_io,
|
||||
get_process_id,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
from openpype.pipeline.create import (
|
||||
CreateContext,
|
||||
|
|
@ -2307,6 +2308,37 @@ class PublisherController(BasePublisherController):
|
|||
def _process_main_thread_item(self, item):
|
||||
item()
|
||||
|
||||
def _is_publish_plugin_active(self, plugin):
|
||||
"""Decide if publish plugin is active.
|
||||
|
||||
This is hack because 'active' is mis-used in mixin
|
||||
'OptionalPyblishPluginMixin' where 'active' is used for default value
|
||||
of optional plugins. Because of that is 'active' state of plugin
|
||||
which inherit from 'OptionalPyblishPluginMixin' ignored. That affects
|
||||
headless publishing inside host, potentially remote publishing.
|
||||
|
||||
We have to change that to match pyblish base, but we can do that
|
||||
only when all hosts use Publisher because the change requires
|
||||
change of settings schemas.
|
||||
|
||||
Args:
|
||||
plugin (pyblish.Plugin): Plugin which should be checked if is
|
||||
active.
|
||||
|
||||
Returns:
|
||||
bool: Is plugin active.
|
||||
"""
|
||||
|
||||
if plugin.active:
|
||||
return True
|
||||
|
||||
if not plugin.optional:
|
||||
return False
|
||||
|
||||
if OptionalPyblishPluginMixin in inspect.getmro(plugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _publish_iterator(self):
|
||||
"""Main logic center of publishing.
|
||||
|
||||
|
|
@ -2315,11 +2347,9 @@ class PublisherController(BasePublisherController):
|
|||
states of currently processed publish plugin and instance. Also
|
||||
change state of processed orders like validation order has passed etc.
|
||||
|
||||
Also stops publishing if should stop on validation.
|
||||
|
||||
QUESTION:
|
||||
Does validate button still make sense?
|
||||
Also stops publishing, if should stop on validation.
|
||||
"""
|
||||
|
||||
for idx, plugin in enumerate(self._publish_plugins):
|
||||
self._publish_progress = idx
|
||||
|
||||
|
|
@ -2344,6 +2374,11 @@ class PublisherController(BasePublisherController):
|
|||
# Add plugin to publish report
|
||||
self._publish_report.add_plugin_iter(plugin, self._publish_context)
|
||||
|
||||
# WARNING This is hack fix for optional plugins
|
||||
if not self._is_publish_plugin_active(plugin):
|
||||
self._publish_report.set_plugin_skipped()
|
||||
continue
|
||||
|
||||
# Trigger callback that new plugin is going to be processed
|
||||
plugin_label = plugin.__name__
|
||||
if hasattr(plugin, "label") and plugin.label:
|
||||
|
|
@ -2450,7 +2485,11 @@ def collect_families_from_instances(instances, only_active=False):
|
|||
instances(list<pyblish.api.Instance>): List of publish instances from
|
||||
which are families collected.
|
||||
only_active(bool): Return families only for active instances.
|
||||
|
||||
Returns:
|
||||
list[str]: Families available on instances.
|
||||
"""
|
||||
|
||||
all_families = set()
|
||||
for instance in instances:
|
||||
if only_active:
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
|
||||
items = []
|
||||
for plugin_item in plugin_items:
|
||||
item = QtGui.QStandardItem(plugin_item.label)
|
||||
label = plugin_item.label or plugin_item.name
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(False, ITEM_IS_GROUP_ROLE)
|
||||
item.setData(plugin_item.label, ITEM_LABEL_ROLE)
|
||||
item.setData(plugin_item.id, ITEM_ID_ROLE)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
import qtawesome
|
||||
|
||||
from openpype.client import get_projects
|
||||
from openpype.pipeline import AvalonMongoDB
|
||||
from openpype.style import get_objected_colors
|
||||
from openpype.tools.utils.widgets import ImageButton
|
||||
from openpype.tools.utils.lib import paint_image_with_color
|
||||
|
|
@ -97,6 +96,7 @@ class CompleterView(QtWidgets.QListView):
|
|||
|
||||
# Open the widget unactivated
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
|
||||
self.setAttribute(QtCore.Qt.WA_NoMouseReplay)
|
||||
delegate = QtWidgets.QStyledItemDelegate()
|
||||
self.setItemDelegate(delegate)
|
||||
|
||||
|
|
@ -241,6 +241,18 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
if self._completer is not None:
|
||||
self._completer.set_text_filter(text)
|
||||
|
||||
def _completer_should_be_visible(self):
|
||||
return (
|
||||
self.isVisible()
|
||||
and (self.hasFocus() or self._completer.hasFocus())
|
||||
)
|
||||
|
||||
def _show_completer(self):
|
||||
if self._completer_should_be_visible():
|
||||
self._focus_timer.start()
|
||||
self._completer.show()
|
||||
self._update_completer()
|
||||
|
||||
def _update_completer(self):
|
||||
if self._completer is None or not self._completer.isVisible():
|
||||
return
|
||||
|
|
@ -249,7 +261,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
self._completer.move(new_point)
|
||||
|
||||
def _on_focus_timer(self):
|
||||
if not self.hasFocus() and not self._completer.hasFocus():
|
||||
if not self._completer_should_be_visible():
|
||||
self._completer.hide()
|
||||
self._focus_timer.stop()
|
||||
|
||||
|
|
@ -258,9 +270,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
self.focused_in.emit()
|
||||
|
||||
if self._completer is not None:
|
||||
self._focus_timer.start()
|
||||
self._completer.show()
|
||||
self._update_completer()
|
||||
self._show_completer()
|
||||
|
||||
def paintEvent(self, event):
|
||||
super(SettingsLineEdit, self).paintEvent(event)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,37 @@ or Deadlines **Draft Tile Assembler**.
|
|||
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
|
||||
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
|
||||
|
||||
## Load Plugins
|
||||
|
||||
### Reference Loader
|
||||
|
||||
#### Namespace and Group Name
|
||||
Here you can create your own custom naming for the reference loader.
|
||||
|
||||
The custom naming is split into two parts: namespace and group name. If you don't set the namespace or the group name, an error will occur.
|
||||
Here's the different variables you can use:
|
||||
|
||||
<div class="row markdown">
|
||||
<div class="col col--5 markdown">
|
||||
|
||||
| Token | Description |
|
||||
|---|---|
|
||||
|`{asset_name}` | Asset name |
|
||||
|`{asset_type}` | Asset type |
|
||||
|`{subset}` | Subset name |
|
||||
|`{family}` | Subset family |
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The namespace field can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001.
|
||||
|
||||
Warning: Note that a namespace will always be prefixed with a _ if it starts with a digit.
|
||||
|
||||
Example:
|
||||
|
||||

|
||||
|
||||
### Extract GPU Cache
|
||||
|
||||

|
||||
|
|
@ -170,6 +201,7 @@ These options are set on the camera shape when publishing the review. They corre
|
|||
|
||||

|
||||
|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||

|
||||
|
|
|
|||
BIN
website/docs/assets/maya-admin_custom_namespace.png
Normal file
BIN
website/docs/assets/maya-admin_custom_namespace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Loading…
Add table
Add a link
Reference in a new issue