Merge branch 'develop' into enhancement/AY-4909_Move-AfterEffects-client-code

This commit is contained in:
Jakub Trllo 2024-06-06 11:16:28 +02:00 committed by GitHub
commit 175833de3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
607 changed files with 1703 additions and 1177 deletions

View file

@ -8,7 +8,6 @@ import inspect
import logging
import threading
import collections
from uuid import uuid4
from abc import ABCMeta, abstractmethod
@ -52,9 +51,13 @@ IGNORED_MODULES_IN_AYON = set()
MOVED_ADDON_MILESTONE_VERSIONS = {
"aftereffects": VersionInfo(0, 2, 0),
"applications": VersionInfo(0, 2, 0),
"blender": VersionInfo(0, 2, 0),
"celaction": VersionInfo(0, 2, 0),
"clockify": VersionInfo(0, 2, 0),
"flame": VersionInfo(0, 2, 0),
"fusion": VersionInfo(0, 2, 0),
"harmony": VersionInfo(0, 2, 0),
"hiero": VersionInfo(0, 2, 0),
"max": VersionInfo(0, 2, 0),
"photoshop": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
@ -63,6 +66,7 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
"nuke": VersionInfo(0, 2, 0),
"resolve": VersionInfo(0, 2, 0),
"substancepainter": VersionInfo(0, 2, 0),
"houdini": VersionInfo(0, 3, 0),
}
@ -551,6 +555,9 @@ class AYONAddon(object):
enabled = True
_id = None
# Temporary variable for 'version' property
_missing_version_warned = False
def __init__(self, manager, settings):
self.manager = manager
@ -581,6 +588,26 @@ class AYONAddon(object):
pass
@property
def version(self):
"""Addon version.
Todo:
Should be abstract property (required). Introduced in
ayon-core 0.3.3 .
Returns:
str: Addon version as semver compatible string.
"""
if not self.__class__._missing_version_warned:
self.__class__._missing_version_warned = True
print(
f"DEV WARNING: Addon '{self.name}' does not have"
f" defined version."
)
return "0.0.0"
def initialize(self, settings):
"""Initialization of addon attributes.
@ -696,6 +723,30 @@ class OpenPypeAddOn(OpenPypeModule):
enabled = True
class _AddonReportInfo:
def __init__(
self, class_name, name, version, report_value_by_label
):
self.class_name = class_name
self.name = name
self.version = version
self.report_value_by_label = report_value_by_label
@classmethod
def from_addon(cls, addon, report):
class_name = addon.__class__.__name__
report_value_by_label = {
label: reported.get(class_name)
for label, reported in report.items()
}
return cls(
addon.__class__.__name__,
addon.name,
addon.version,
report_value_by_label
)
class AddonsManager:
"""Manager of addons that helps to load and prepare them to work.
@ -872,10 +923,6 @@ class AddonsManager:
name_alias = getattr(addon, "openpype_alias", None)
if name_alias:
aliased_names.append((name_alias, addon))
enabled_str = "X"
if not addon.enabled:
enabled_str = " "
self.log.debug("[{}] {}".format(enabled_str, name))
now = time.time()
report[addon.__class__.__name__] = now - prev_start_time
@ -887,6 +934,13 @@ class AddonsManager:
exc_info=True
)
for addon_name in sorted(self._addons_by_name.keys()):
addon = self._addons_by_name[addon_name]
enabled_str = "X" if addon.enabled else " "
self.log.debug(
f"[{enabled_str}] {addon.name} ({addon.version})"
)
for item in aliased_names:
name_alias, addon = item
if name_alias not in self._addons_by_name:
@ -1175,39 +1229,55 @@ class AddonsManager:
available_col_names |= set(addon_names.keys())
# Prepare ordered dictionary for columns
cols = collections.OrderedDict()
# Add addon names to first columnt
cols["Addon name"] = list(sorted(
addon.__class__.__name__
addons_info = [
_AddonReportInfo.from_addon(addon, self._report)
for addon in self.addons
if addon.__class__.__name__ in available_col_names
))
]
addons_info.sort(key=lambda x: x.name)
addon_name_rows = [
addon_info.name
for addon_info in addons_info
]
addon_version_rows = [
addon_info.version
for addon_info in addons_info
]
# Add total key (as last addon)
cols["Addon name"].append(self._report_total_key)
addon_name_rows.append(self._report_total_key)
addon_version_rows.append(f"({len(addons_info)})")
cols = collections.OrderedDict()
# Add addon names to first columnt
cols["Addon name"] = addon_name_rows
cols["Version"] = addon_version_rows
# Add columns from report
total_by_addon = {
row: 0
for row in addon_name_rows
}
for label in self._report.keys():
cols[label] = []
total_addon_times = {}
for addon_name in cols["Addon name"]:
total_addon_times[addon_name] = 0
for label, reported in self._report.items():
for addon_name in cols["Addon name"]:
col_time = reported.get(addon_name)
if col_time is None:
cols[label].append("N/A")
rows = []
col_total = 0
for addon_info in addons_info:
value = addon_info.report_value_by_label.get(label)
if value is None:
rows.append("N/A")
continue
cols[label].append("{:.3f}".format(col_time))
total_addon_times[addon_name] += col_time
rows.append("{:.3f}".format(value))
total_by_addon[addon_info.name] += value
col_total += value
total_by_addon[self._report_total_key] += col_total
rows.append("{:.3f}".format(col_total))
cols[label] = rows
# Add to also total column that should sum the row
cols[self._report_total_key] = []
for addon_name in cols["Addon name"]:
cols[self._report_total_key].append(
"{:.3f}".format(total_addon_times[addon_name])
)
cols[self._report_total_key] = [
"{:.3f}".format(total_by_addon[addon_name])
for addon_name in cols["Addon name"]
]
# Prepare column widths and total row count
# - column width is by

View file

@ -1,6 +0,0 @@
from .addon import BlenderAddon
__all__ = (
"BlenderAddon",
)

View file

@ -1,10 +0,0 @@
from .addon import (
HIERO_ROOT_DIR,
HieroAddon,
)
__all__ = (
"HIERO_ROOT_DIR",
"HieroAddon",
)

View file

@ -7,6 +7,8 @@ import six
from ayon_core.lib import Logger
from ayon_core.modules import AYONAddon, IPluginPaths
from .version import __version__
class DeadlineWebserviceError(Exception):
"""
@ -16,6 +18,7 @@ class DeadlineWebserviceError(Exception):
class DeadlineModule(AYONAddon, IPluginPaths):
name = "deadline"
version = __version__
def initialize(self, studio_settings):
# This module is always enabled

View file

@ -79,7 +79,7 @@ class FusionSubmitDeadline(
else:
context.data[key] = True
from ayon_core.hosts.fusion.api.lib import get_frame_path
from ayon_fusion.api.lib import get_frame_path
deadline_url = instance.data["deadline"]["url"]
assert deadline_url, "Requires Deadline Webservice URL"

View file

@ -1,6 +1,9 @@
from .version import __version__
from .addon import JobQueueAddon
__all__ = (
"__version__",
"JobQueueAddon",
)

View file

@ -44,9 +44,12 @@ import platform
from ayon_core.addon import AYONAddon, click_wrap
from ayon_core.settings import get_studio_settings
from .version import __version__
class JobQueueAddon(AYONAddon):
name = "job_queue"
version = __version__
def initialize(self, studio_settings):
addon_settings = studio_settings.get(self.name) or {}

View file

@ -0,0 +1 @@
__version__ = "1.0.0"

View file

@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
class LauncherAction(AYONAddon, ITrayAction):
label = "Launcher"
name = "launcher_tool"
version = "1.0.0"
def initialize(self, settings):

View file

@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon
class LoaderAddon(AYONAddon, ITrayAddon):
name = "loader_tool"
version = "1.0.0"
def initialize(self, settings):
# Tray attributes

View file

@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
class PythonInterpreterAction(AYONAddon, ITrayAction):
label = "Console"
name = "python_interpreter"
version = "1.0.0"
admin_action = True
def initialize(self, settings):

View file

@ -1,6 +1,9 @@
from .version import __version__
from .addon import RoyalRenderAddon
__all__ = (
"__version__",
"RoyalRenderAddon",
)

View file

@ -4,10 +4,13 @@ import os
from ayon_core.addon import AYONAddon, IPluginPaths
from .version import __version__
class RoyalRenderAddon(AYONAddon, IPluginPaths):
"""Class providing basic Royal Render implementation logic."""
name = "royalrender"
version = __version__
# _rr_api = None
# @property

View file

@ -0,0 +1 @@
__version__ = "0.1.1"

View file

@ -1,7 +1,10 @@
from .version import __version__
from .timers_manager import (
TimersManager
)
__all__ = (
"__version__",
"TimersManager",
)

View file

@ -10,6 +10,7 @@ from ayon_core.addon import (
)
from ayon_core.lib.events import register_event_callback
from .version import __version__
from .exceptions import InvalidContextError
TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -96,6 +97,7 @@ class TimersManager(
See `ExampleTimersManagerConnector`.
"""
name = "timers_manager"
version = __version__
label = "Timers Service"
_required_methods = (

View file

@ -0,0 +1 @@
__version__ = "0.1.1"

View file

@ -1,8 +1,13 @@
from .version import __version__
from .structures import HostMsgAction
from .webserver_module import (
WebServerAddon
)
__all__ = (
"__version__",
"HostMsgAction",
"WebServerAddon",
)

View file

@ -9,22 +9,18 @@ from qtpy import QtWidgets
from ayon_core.addon import ITrayService
from ayon_core.tools.stdout_broker.window import ConsoleDialog
from .structures import HostMsgAction
log = logging.getLogger(__name__)
# Host listener icon type
class IconType:
IDLE = "idle"
RUNNING = "running"
FAILED = "failed"
class MsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"
class HostListener:
def __init__(self, webserver, module):
self._window_per_id = {}
@ -96,22 +92,22 @@ class HostListener:
if msg.type == aiohttp.WSMsgType.TEXT:
host_name, action, text = self._parse_message(msg)
if action == MsgAction.CONNECTING:
if action == HostMsgAction.CONNECTING:
self._action_per_id[host_name] = None
# must be sent to main thread, or action wont trigger
self.module.execute_in_main_thread(
lambda: self._host_is_connecting(host_name, text))
elif action == MsgAction.CLOSE:
elif action == HostMsgAction.CLOSE:
# clean close
self._close(host_name)
await ws.close()
elif action == MsgAction.INITIALIZED:
elif action == HostMsgAction.INITIALIZED:
self.module.execute_in_main_thread(
# must be queued as _host_is_connecting might not
# be triggered/finished yet
lambda: self._set_host_icon(host_name,
IconType.RUNNING))
elif action == MsgAction.ADD:
elif action == HostMsgAction.ADD:
self.module.execute_in_main_thread(
lambda: self._add_text(host_name, text))
elif msg.type == aiohttp.WSMsgType.ERROR:

View file

@ -0,0 +1,6 @@
# Host listener message actions
class HostMsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"

View file

@ -0,0 +1 @@
__version__ = "1.0.0"

View file

@ -26,9 +26,12 @@ import socket
from ayon_core import resources
from ayon_core.addon import AYONAddon, ITrayService
from .version import __version__
class WebServerAddon(AYONAddon, ITrayService):
name = "webserver"
version = __version__
label = "WebServer"
webserver_url_env = "AYON_WEBSERVER_URL"

View file

@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler
from ayon_core import AYON_CORE_ROOT
from ayon_core.host import HostBase
from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event
from ayon_core.lib import (
is_in_tests,
initialize_ayon_connection,
emit_event,
version_up
)
from ayon_core.addon import load_addons, AddonsManager
from ayon_core.settings import get_project_settings
@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names
from .workfile import (
get_workdir,
get_custom_workfile_template_by_string_context,
get_workfile_template_key_from_context,
get_last_workfile
)
from . import (
register_loader_plugin_path,
@ -579,3 +586,48 @@ def get_process_id():
if _process_id is None:
_process_id = str(uuid.uuid4())
return _process_id
def version_up_current_workfile():
"""Function to increment and save workfile
"""
host = registered_host()
if not host.has_unsaved_changes():
print("No unsaved changes, skipping file save..")
return
project_name = get_current_project_name()
folder_path = get_current_folder_path()
task_name = get_current_task_name()
host_name = get_current_host_name()
template_key = get_workfile_template_key_from_context(
project_name,
folder_path,
task_name,
host_name,
)
anatomy = Anatomy(project_name)
data = get_template_data_with_names(
project_name, folder_path, task_name, host_name
)
data["root"] = anatomy.roots
work_template = anatomy.get_template_item("work", template_key)
# Define saving file extension
extensions = host.get_workfile_extensions()
current_file = host.get_current_workfile()
if current_file:
extensions = [os.path.splitext(current_file)[-1]]
work_root = work_template["directory"].format_strict(data)
file_template = work_template["file"].template
last_workfile_path = get_last_workfile(
work_root, file_template, data, extensions, True
)
new_workfile_path = version_up(last_workfile_path)
if os.path.exists(new_workfile_path):
new_workfile_path = version_up(new_workfile_path)
host.save_workfile(new_workfile_path)

View file

@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
settings_category = getattr(plugin, "settings_category", None)
if settings_category:
try:
return (
project_settings
[settings_category]
["publish"]
[plugin.__name__]
)
category_settings = project_settings[settings_category]
except KeyError:
log.warning((
"Couldn't find plugin '{}' settings"
" under settings category '{}'"
).format(plugin.__name__, settings_category))
"Couldn't find settings category '{}' in project settings"
).format(settings_category))
return {}
try:
return category_settings["publish"][plugin.__name__]
except KeyError:
return {}
# Use project settings based on a category name

View file

@ -172,12 +172,30 @@ class VersionItem:
def __gt__(self, other):
if not isinstance(other, VersionItem):
return False
if (
other.version == self.version
and self.is_hero
):
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Hero version is greater than non-hero
if version == other_version:
return self.is_hero
return version > other_version
def __lt__(self, other):
if not isinstance(other, VersionItem):
return True
return other.version < self.version
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Non-hero version is lesser than hero
if version == other_version:
return not self.is_hero
return version < other_version
def __ge__(self, other):
return self.__eq__(other) or self.__gt__(other)
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def to_data(self):
return {

View file

@ -348,10 +348,18 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
return set()
if not self._loaded_products_cache.is_valid:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
try:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
except BaseException:
self.log.error(
"Failed to collect loaded products.", exc_info=True
)
containers = []
repre_ids = set()
for container in containers:
repre_id = container.get("representation")

View file

@ -321,6 +321,8 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def set_merged_products_selection(self, items):
"""

View file

@ -199,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel):
product_item = self._product_items_by_id.get(product_id)
if product_item is None:
return None
return list(product_item.version_items.values())
product_items = list(product_item.version_items.values())
product_items.sort(reverse=True)
return product_items
if role == QtCore.Qt.EditRole:
return None

View file

@ -0,0 +1,5 @@
from .broker import StdOutBroker
__all__ = (
"StdOutBroker",
)

View file

@ -1,173 +1,12 @@
import os
import sys
import threading
import collections
import websocket
import json
from datetime import datetime
import warnings
from .broker import StdOutBroker
from ayon_core.lib import Logger
from openpype_modules.webserver.host_console_listener import MsgAction
warnings.warn(
(
"Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'"
" is deprecated. Please use 'ayon_core.tools.stdout_broker' instead."
),
DeprecationWarning
)
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": MsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": MsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": MsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": MsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()
__all__ = ("StdOutBroker", )

View file

@ -0,0 +1,174 @@
import os
import sys
import threading
import collections
import json
from datetime import datetime
import websocket
from ayon_core.lib import Logger
from ayon_core.modules.webserver import HostMsgAction
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": HostMsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": HostMsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": HostMsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": HostMsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()

View file

@ -447,8 +447,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def initialize_addons(self):
self._initializing_addons = True
self.tray_man.initialize_addons()
self._initializing_addons = False
try:
self.tray_man.initialize_addons()
finally:
self._initializing_addons = False
def _click_timer_timeout(self):
self._click_timer.stop()

View file

@ -370,6 +370,8 @@ class FoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def refresh(self):
"""Refresh folders model.

View file

@ -79,11 +79,12 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
exclude = [
"client/ayon_core/scripts/slates/__init__.py",
"client/ayon_core/modules/click_wrap.py",
"client/ayon_core/hosts/unreal/integration/*",
"client/ayon_core/hosts/hiero/api/startup/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/CelAction/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/HarmonyAYON/*",
"client/ayon_core/modules/click_wrap.py",
"client/ayon_core/scripts/slates/__init__.py",
"server_addon/hiero/client/ayon_hiero/api/startup/*",
"server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/*"
]

View file

@ -798,7 +798,7 @@ class PublishPuginsModel(BaseSettingsModel):
)
ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
default_factory=PluginStateByHostModel,
title="Validate Containers"
title="Validate Outdated Containers"
)
ValidateIntent: ValidateIntentModel = SettingsField(
default_factory=ValidateIntentModel,

View file

@ -118,6 +118,15 @@ class WorkfilesLockProfile(BaseSettingsModel):
enabled: bool = SettingsField(True, title="Enabled")
class AYONMenuModel(BaseSettingsModel):
_layout = "expanded"
version_up_current_workfile: bool = SettingsField(
False,
title="Version Up Workfile",
description="Add 'Version Up Workfile' to AYON menu"
)
class WorkfilesToolModel(BaseSettingsModel):
workfile_template_profiles: list[WorkfileTemplateProfile] = SettingsField(
default_factory=list,
@ -268,6 +277,10 @@ class PublishToolModel(BaseSettingsModel):
class GlobalToolsModel(BaseSettingsModel):
ayon_menu: AYONMenuModel = SettingsField(
default_factory=AYONMenuModel,
title="AYON Menu"
)
creator: CreatorToolModel = SettingsField(
default_factory=CreatorToolModel,
title="Creator"
@ -287,6 +300,9 @@ class GlobalToolsModel(BaseSettingsModel):
DEFAULT_TOOLS_VALUES = {
"ayon_menu": {
"version_up_current_workfile": False
},
"creator": {
"product_types_smart_select": [
{

View file

@ -0,0 +1,13 @@
from .version import __version__
from .addon import (
BlenderAddon,
BLENDER_ADDON_ROOT,
)
__all__ = (
"__version__",
"BlenderAddon",
"BLENDER_ADDON_ROOT",
)

View file

@ -1,18 +1,21 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
from .version import __version__
BLENDER_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class BlenderAddon(AYONAddon, IHostAddon):
name = "blender"
version = __version__
host_name = "blender"
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
# Prepare path to implementation script
implementation_user_script_path = os.path.join(
BLENDER_ROOT_DIR,
BLENDER_ADDON_ROOT,
"blender_addon"
)
@ -61,7 +64,7 @@ class BlenderAddon(AYONAddon, IHostAddon):
if app.host_name != self.host_name:
return []
return [
os.path.join(BLENDER_ROOT_DIR, "hooks")
os.path.join(BLENDER_ADDON_ROOT, "hooks")
]
def get_workfile_extensions(self):

View file

@ -15,7 +15,6 @@ from .pipeline import (
from .plugin import (
Creator,
Loader,
)
from .workio import (
@ -51,7 +50,6 @@ __all__ = [
"BlenderHost",
"Creator",
"Loader",
# Workfiles API
"open_file",

View file

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before After
Before After

View file

@ -305,7 +305,7 @@ class LaunchCreator(LaunchQtApp):
class LaunchLoader(LaunchQtApp):
"""Launch Avalon Loader."""
"""Launch AYON Loader."""
bl_idname = "wm.avalon_loader"
bl_label = "Load..."

View file

@ -5,9 +5,6 @@ from typing import Callable, Dict, Iterator, List, Optional
import bpy
from . import lib
from . import ops
import pyblish.api
import ayon_api
@ -33,8 +30,12 @@ from ayon_core.lib import (
register_event_callback,
emit_event
)
import ayon_core.hosts.blender
from ayon_core.settings import get_project_settings
from ayon_blender import BLENDER_ADDON_ROOT
from . import lib
from . import ops
from .workio import (
open_file,
save_file,
@ -44,9 +45,7 @@ from .workio import (
work_root,
)
HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.blender.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(BLENDER_ADDON_ROOT, "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")

View file

@ -4,6 +4,7 @@ import itertools
from pathlib import Path
from typing import Dict, List, Optional
import pyblish.api
import bpy
from ayon_core.pipeline import (
@ -13,6 +14,7 @@ from ayon_core.pipeline import (
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
from ayon_core.pipeline.publish import Extractor
from ayon_core.lib import BoolDef
from .pipeline import (
@ -161,10 +163,23 @@ def deselect_all():
bpy.context.view_layer.objects.active = active
class BaseCreator(Creator):
class BlenderInstancePlugin(pyblish.api.InstancePlugin):
settings_category = "blender"
class BlenderContextPlugin(pyblish.api.ContextPlugin):
settings_category = "blender"
class BlenderExtractor(Extractor):
settings_category = "blender"
class BlenderCreator(Creator):
"""Base class for Blender Creator plug-ins."""
defaults = ['Main']
settings_category = "blender"
create_as_asset_group = False
@staticmethod
@ -265,7 +280,7 @@ class BaseCreator(Creator):
return instance_node
def collect_instances(self):
"""Override abstract method from BaseCreator.
"""Override abstract method from BlenderCreator.
Collect existing instances related to this creator plugin."""
# Cache instances in shared data
@ -292,7 +307,7 @@ class BaseCreator(Creator):
self._add_instance_to_context(instance)
def update_instances(self, update_list):
"""Override abstract method from BaseCreator.
"""Override abstract method from BlenderCreator.
Store changes of existing instances so they can be recollected.
Args:
@ -376,13 +391,7 @@ class BaseCreator(Creator):
]
class Loader(LoaderPlugin):
"""Base class for Loader plug-ins."""
hosts = ["blender"]
class AssetLoader(LoaderPlugin):
class BlenderLoader(LoaderPlugin):
"""A basic AssetLoader for Blender
This will implement the basic logic for linking/appending assets
@ -392,6 +401,7 @@ class AssetLoader(LoaderPlugin):
it's different for different types (e.g. model, rig, animation,
etc.).
"""
settings_category = "blender"
@staticmethod
def _get_instance_empty(instance_name: str, nodes: List) -> Optional[bpy.types.Object]:
@ -496,7 +506,7 @@ class AssetLoader(LoaderPlugin):
# Only containerise if it's not already a collection from a .blend file.
# representation = context["representation"]["name"]
# if representation != "blend":
# from ayon_core.hosts.blender.api.pipeline import containerise
# from ayon_blender.api.pipeline import containerise
# return containerise(
# name=name,
# namespace=namespace,

View file

@ -1,5 +1,5 @@
from ayon_core.pipeline import install_host
from ayon_core.hosts.blender.api import BlenderHost
from ayon_blender.api import BlenderHost
def register():

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Converter for legacy Houdini products."""
from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin
from ayon_core.hosts.blender.api.lib import imprint
from ayon_blender.api.lib import imprint
class BlenderLegacyConvertor(ProductConvertorPlugin):
@ -42,7 +42,7 @@ class BlenderLegacyConvertor(ProductConvertorPlugin):
parameter on them.
This is using cached entries done in
:py:meth:`~BaseCreator.cache_instance_data()`
:py:meth:`~BlenderCreator.cache_instance_data()`
"""
self.legacy_instances = self.collection_shared_data.get(

View file

@ -2,10 +2,10 @@
import bpy
from ayon_core.hosts.blender.api import lib, plugin
from ayon_blender.api import lib, plugin
class CreateAction(plugin.BaseCreator):
class CreateAction(plugin.BlenderCreator):
"""Action output for character rigs."""
identifier = "io.openpype.creators.blender.action"

View file

@ -1,9 +1,9 @@
"""Create an animation asset."""
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateAnimation(plugin.BaseCreator):
class CreateAnimation(plugin.BlenderCreator):
"""Animation output for character rigs."""
identifier = "io.openpype.creators.blender.animation"

View file

@ -2,10 +2,10 @@
import bpy
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateBlendScene(plugin.BaseCreator):
class CreateBlendScene(plugin.BlenderCreator):
"""Generic group of assets."""
identifier = "io.openpype.creators.blender.blendscene"

View file

@ -2,11 +2,11 @@
import bpy
from ayon_core.hosts.blender.api import plugin, lib
from ayon_core.hosts.blender.api.pipeline import AVALON_INSTANCES
from ayon_blender.api import plugin, lib
from ayon_blender.api.pipeline import AVALON_INSTANCES
class CreateCamera(plugin.BaseCreator):
class CreateCamera(plugin.BlenderCreator):
"""Polygonal static geometry."""
identifier = "io.openpype.creators.blender.camera"

View file

@ -2,10 +2,10 @@
import bpy
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateLayout(plugin.BaseCreator):
class CreateLayout(plugin.BlenderCreator):
"""Layout output for character rigs."""
identifier = "io.openpype.creators.blender.layout"

View file

@ -2,10 +2,10 @@
import bpy
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateModel(plugin.BaseCreator):
class CreateModel(plugin.BlenderCreator):
"""Polygonal static geometry."""
identifier = "io.openpype.creators.blender.model"

View file

@ -1,9 +1,9 @@
"""Create a pointcache asset."""
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreatePointcache(plugin.BaseCreator):
class CreatePointcache(plugin.BlenderCreator):
"""Polygonal static geometry."""
identifier = "io.openpype.creators.blender.pointcache"

View file

@ -2,12 +2,12 @@
import bpy
from ayon_core.lib import version_up
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.render_lib import prepare_rendering
from ayon_core.hosts.blender.api.workio import save_file
from ayon_blender.api import plugin
from ayon_blender.api.render_lib import prepare_rendering
from ayon_blender.api.workio import save_file
class CreateRenderlayer(plugin.BaseCreator):
class CreateRenderlayer(plugin.BlenderCreator):
"""Single baked camera."""
identifier = "io.openpype.creators.blender.render"

View file

@ -1,9 +1,9 @@
"""Create review."""
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateReview(plugin.BaseCreator):
class CreateReview(plugin.BlenderCreator):
"""Single baked camera."""
identifier = "io.openpype.creators.blender.review"

View file

@ -2,10 +2,10 @@
import bpy
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateRig(plugin.BaseCreator):
class CreateRig(plugin.BlenderCreator):
"""Artist-friendly rig with controls to direct motion."""
identifier = "io.openpype.creators.blender.rig"

View file

@ -1,9 +1,9 @@
"""Create a USD Export."""
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CreateUSD(plugin.BaseCreator):
class CreateUSD(plugin.BlenderCreator):
"""Create USD Export"""
identifier = "io.openpype.creators.blender.usd"

View file

@ -2,14 +2,14 @@ import bpy
import ayon_api
from ayon_core.pipeline import CreatedInstance, AutoCreator
from ayon_core.hosts.blender.api.plugin import BaseCreator
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api.plugin import BlenderCreator
from ayon_blender.api.pipeline import (
AVALON_PROPERTY,
AVALON_CONTAINERS
)
class CreateWorkfile(BaseCreator, AutoCreator):
class CreateWorkfile(BlenderCreator, AutoCreator):
"""Workfile auto-creator.
The workfile instance stores its data on the `AVALON_CONTAINERS` collection

View file

@ -1,6 +1,6 @@
import bpy
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
def append_workfile(context, fname, do_import):
@ -34,7 +34,7 @@ def append_workfile(context, fname, do_import):
collection.children.link(coll)
class AppendBlendLoader(plugin.AssetLoader):
class AppendBlendLoader(plugin.BlenderLoader):
"""Append workfile in Blender (unmanaged)
Warning:
@ -59,7 +59,7 @@ class AppendBlendLoader(plugin.AssetLoader):
return
class ImportBlendLoader(plugin.AssetLoader):
class ImportBlendLoader(plugin.BlenderLoader):
"""Import workfile in the current Blender scene (unmanaged)
Warning:

View file

@ -7,8 +7,8 @@ from typing import Dict, List, Optional
import bpy
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import (
containerise_existing,
AVALON_PROPERTY,
)
@ -16,7 +16,7 @@ from ayon_core.hosts.blender.api.pipeline import (
logger = logging.getLogger("ayon").getChild("blender").getChild("load_action")
class BlendActionLoader(plugin.AssetLoader):
class BlendActionLoader(plugin.BlenderLoader):
"""Load action from a .blend file.
Warning:

View file

@ -4,11 +4,11 @@ from typing import Dict, List, Optional
import bpy
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import AVALON_PROPERTY
class BlendAnimationLoader(plugin.AssetLoader):
class BlendAnimationLoader(plugin.BlenderLoader):
"""Load animations from a .blend file.
Warning:

View file

@ -10,14 +10,14 @@ from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class AudioLoader(plugin.AssetLoader):
class AudioLoader(plugin.BlenderLoader):
"""Load audio in Blender."""
product_types = {"audio"}

View file

@ -9,15 +9,15 @@ from ayon_core.pipeline import (
registered_host
)
from ayon_core.pipeline.create import CreateContext
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.lib import imprint
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin
from ayon_blender.api.lib import imprint
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class BlendLoader(plugin.AssetLoader):
class BlendLoader(plugin.BlenderLoader):
"""Load assets from a .blend file."""
product_types = {"model", "rig", "layout", "camera"}

View file

@ -7,15 +7,15 @@ from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.lib import imprint
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin
from ayon_blender.api.lib import imprint
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class BlendSceneLoader(plugin.AssetLoader):
class BlendSceneLoader(plugin.BlenderLoader):
"""Load assets from a .blend file."""
product_types = {"blendScene"}

View file

@ -11,14 +11,14 @@ from ayon_core.pipeline import (
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
from ayon_core.hosts.blender.api import plugin, lib
from ayon_blender.api import plugin, lib
class CacheModelLoader(plugin.AssetLoader):
class CacheModelLoader(plugin.BlenderLoader):
"""Load cache models.
Stores the imported asset in a collection named after the asset.

View file

@ -10,14 +10,14 @@ from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api import plugin, lib
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin, lib
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class AbcCameraLoader(plugin.AssetLoader):
class AbcCameraLoader(plugin.BlenderLoader):
"""Load a camera from Alembic file.
Stores the imported asset in an empty named after the asset.

View file

@ -10,14 +10,14 @@ from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api import plugin, lib
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin, lib
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class FbxCameraLoader(plugin.AssetLoader):
class FbxCameraLoader(plugin.BlenderLoader):
"""Load a camera from FBX.
Stores the imported asset in an empty named after the asset.

View file

@ -10,14 +10,14 @@ from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api import plugin, lib
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin, lib
from ayon_blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class FbxModelLoader(plugin.AssetLoader):
class FbxModelLoader(plugin.BlenderLoader):
"""Load FBX models.
Stores the imported asset in an empty named after the asset.

View file

@ -15,15 +15,15 @@ from ayon_core.pipeline import (
loaders_from_representation,
AVALON_CONTAINER_ID,
)
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api.pipeline import (
AVALON_INSTANCES,
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class JsonLayoutLoader(plugin.AssetLoader):
class JsonLayoutLoader(plugin.BlenderLoader):
"""Load layout published from Unreal."""
product_types = {"layout"}

View file

@ -9,14 +9,14 @@ import json
import bpy
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import (
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import (
containerise_existing,
AVALON_PROPERTY
)
class BlendLookLoader(plugin.AssetLoader):
class BlendLookLoader(plugin.BlenderLoader):
"""Load models from a .blend file.
Because they come from a .blend file we can simply link the collection that

View file

@ -1,8 +1,8 @@
import pyblish.api
from ayon_core.hosts.blender.api import workio
from ayon_blender.api import workio, plugin
class CollectBlenderCurrentFile(pyblish.api.ContextPlugin):
class CollectBlenderCurrentFile(plugin.BlenderContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.5

View file

@ -3,10 +3,11 @@ import bpy
import pyblish.api
from ayon_core.pipeline.publish import KnownPublishError
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import AVALON_PROPERTY
class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
class CollectBlenderInstanceData(plugin.BlenderInstancePlugin):
"""Validator to verify that the instance is not empty"""
order = pyblish.api.CollectorOrder

View file

@ -5,12 +5,12 @@ import os
import re
import bpy
from ayon_core.hosts.blender.api import colorspace
import pyblish.api
from ayon_blender.api import colorspace, plugin
class CollectBlenderRender(pyblish.api.InstancePlugin):
class CollectBlenderRender(plugin.BlenderInstancePlugin):
"""Gather all publishable render instances."""
order = pyblish.api.CollectorOrder + 0.01

View file

@ -1,9 +1,9 @@
import bpy
import pyblish.api
from ayon_blender.api import plugin
class CollectReview(pyblish.api.InstancePlugin):
class CollectReview(plugin.BlenderInstancePlugin):
"""Collect Review data
"""

View file

@ -1,9 +1,10 @@
from pathlib import Path
from pyblish.api import InstancePlugin, CollectorOrder
from pyblish.api import CollectorOrder
from ayon_blender.api import plugin
class CollectWorkfile(InstancePlugin):
class CollectWorkfile(plugin.BlenderInstancePlugin):
"""Inject workfile data into its instance."""
order = CollectorOrder

View file

@ -4,10 +4,10 @@ import bpy
from ayon_core.lib import BoolDef
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractABC(plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin):
"""Extract as ABC."""
label = "Extract ABC"

View file

@ -3,12 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class ExtractAnimationABC(
publish.Extractor,
publish.OptionalPyblishPluginMixin,
plugin.BlenderExtractor,
publish.OptionalPyblishPluginMixin,
):
"""Extract as ABC."""

View file

@ -3,9 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_blender.api import plugin
class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractBlend(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""Extract a blend file."""
label = "Extract Blend"

View file

@ -3,11 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_blender.api import plugin
class ExtractBlendAnimation(
publish.Extractor,
publish.OptionalPyblishPluginMixin,
plugin.BlenderExtractor,
publish.OptionalPyblishPluginMixin,
):
"""Extract a blend file."""

View file

@ -3,10 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractCameraABC(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""Extract camera as ABC."""
label = "Extract Camera (ABC)"

View file

@ -3,10 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractCamera(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""Extract as the camera as FBX."""
label = "Extract Camera (FBX)"

View file

@ -3,10 +3,12 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_blender.api import plugin
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractFBX(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""Extract as FBX."""
label = "Extract FBX"

View file

@ -6,8 +6,8 @@ import bpy_extras
import bpy_extras.anim_utils
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import AVALON_PROPERTY
def get_all_parents(obj):
@ -42,8 +42,8 @@ def get_highest_root(objects):
class ExtractAnimationFBX(
publish.Extractor,
publish.OptionalPyblishPluginMixin,
plugin.BlenderExtractor,
publish.OptionalPyblishPluginMixin,
):
"""Extract as animation."""

View file

@ -8,11 +8,13 @@ import bpy_extras.anim_utils
from ayon_api import get_representations
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
from ayon_blender.api import plugin
from ayon_blender.api.pipeline import AVALON_PROPERTY
class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractLayout(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""Extract a layout."""
label = "Extract Layout (JSON)"

View file

@ -7,11 +7,13 @@ import pyblish.api
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import capture
from ayon_core.hosts.blender.api.lib import maintained_time
from ayon_blender.api import capture, plugin
from ayon_blender.api.lib import maintained_time
class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
class ExtractPlayblast(
plugin.BlenderExtractor, publish.OptionalPyblishPluginMixin
):
"""
Extract viewport playblast.

View file

@ -3,14 +3,13 @@ import glob
import json
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import capture
from ayon_core.hosts.blender.api.lib import maintained_time
from ayon_blender.api import capture, plugin
from ayon_blender.api.lib import maintained_time
import bpy
class ExtractThumbnail(publish.Extractor):
class ExtractThumbnail(plugin.BlenderExtractor):
"""Extract viewport thumbnail.
Takes review camera and creates a thumbnail based on viewport

View file

@ -2,11 +2,11 @@ import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin, lib
from ayon_core.pipeline import KnownPublishError
from ayon_blender.api import plugin, lib
class ExtractUSD(publish.Extractor):
class ExtractUSD(plugin.BlenderExtractor):
"""Extract as USD."""
label = "Extract USD"
@ -40,7 +40,7 @@ class ExtractUSD(publish.Extractor):
root = lib.get_highest_root(objects=instance[:])
if not root:
instance_node = instance.data["transientData"]["instance_node"]
raise publish.KnownPublishError(
raise KnownPublishError(
f"No root object found in instance: {instance_node.name}"
)
self.log.debug(f"Exporting using active root: {root.name}")

View file

@ -1,11 +1,12 @@
import pyblish.api
from ayon_core.pipeline.publish import OptionalPyblishPluginMixin
from ayon_core.hosts.blender.api.workio import save_file
from ayon_blender.api.workio import save_file
from ayon_blender.api import plugin
class IncrementWorkfileVersion(
pyblish.api.ContextPlugin,
OptionalPyblishPluginMixin
plugin.BlenderContextPlugin,
OptionalPyblishPluginMixin
):
"""Increment current workfile version."""

View file

@ -2,11 +2,12 @@ import json
import pyblish.api
from ayon_core.pipeline.publish import OptionalPyblishPluginMixin
from ayon_blender.api import plugin
class IntegrateAnimation(
pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin,
plugin.BlenderInstancePlugin,
OptionalPyblishPluginMixin,
):
"""Generate a JSON file for animation."""

View file

@ -2,9 +2,8 @@ from typing import List
import bpy
import pyblish.api
import ayon_core.hosts.blender.api.action
import ayon_blender.api.action
from ayon_blender.api import plugin
from ayon_core.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError,
@ -12,8 +11,10 @@ from ayon_core.pipeline.publish import (
)
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
class ValidateCameraZeroKeyframe(
plugin.BlenderInstancePlugin,
OptionalPyblishPluginMixin
):
"""Camera must have a keyframe at frame 0.
Unreal shifts the first keyframe to frame 0. Forcing the camera to have
@ -25,7 +26,7 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin,
hosts = ["blender"]
families = ["camera"]
label = "Zero Keyframe"
actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction]
actions = [ayon_blender.api.action.SelectInvalidAction]
@staticmethod
def get_invalid(instance) -> List:

View file

@ -2,18 +2,20 @@ import os
import bpy
import pyblish.api
from ayon_core.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.blender.api.render_lib import prepare_rendering
from ayon_blender.api import plugin
from ayon_blender.api.render_lib import prepare_rendering
class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
class ValidateDeadlinePublish(
plugin.BlenderInstancePlugin,
OptionalPyblishPluginMixin
):
"""Validates Render File Directory is
not the same in every submission
"""

View file

@ -6,6 +6,7 @@ from ayon_core.pipeline.publish import (
OptionalPyblishPluginMixin,
PublishValidationError
)
from ayon_blender.api import plugin
class SaveWorkfileAction(pyblish.api.Action):
@ -18,8 +19,10 @@ class SaveWorkfileAction(pyblish.api.Action):
bpy.ops.wm.avalon_workfiles()
class ValidateFileSaved(pyblish.api.ContextPlugin,
OptionalPyblishPluginMixin):
class ValidateFileSaved(
plugin.BlenderContextPlugin,
OptionalPyblishPluginMixin
):
"""Validate that the workfile has been saved."""
order = pyblish.api.ValidatorOrder - 0.01

Some files were not shown because too many files have changed in this diff Show more