mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
427 lines
14 KiB
Python
427 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pipeline tools for OpenPype Substance Painter integration."""
|
|
import os
|
|
import logging
|
|
from functools import partial
|
|
|
|
# Substance 3D Painter modules
|
|
import substance_painter.ui
|
|
import substance_painter.event
|
|
import substance_painter.project
|
|
|
|
import pyblish.api
|
|
|
|
from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost
|
|
from openpype.settings import (
|
|
get_current_project_settings,
|
|
get_system_settings
|
|
)
|
|
|
|
from openpype.pipeline.template_data import get_template_data_with_names
|
|
from openpype.pipeline import (
|
|
register_creator_plugin_path,
|
|
register_loader_plugin_path,
|
|
AVALON_CONTAINER_ID,
|
|
Anatomy
|
|
)
|
|
from openpype.lib import (
|
|
StringTemplate,
|
|
register_event_callback,
|
|
emit_event,
|
|
)
|
|
from openpype.pipeline.load import any_outdated_containers
|
|
from openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR
|
|
|
|
from . import lib
|
|
|
|
log = logging.getLogger("openpype.hosts.substance")
|
|
|
|
PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins")
|
|
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
|
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
|
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
|
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
|
|
|
OPENPYPE_METADATA_KEY = "OpenPype"
|
|
OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key
|
|
OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key
|
|
OPENPYPE_METADATA_INSTANCES_KEY = "instances" # child key
|
|
|
|
|
|
class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|
name = "substancepainter"
|
|
|
|
def __init__(self):
|
|
super(SubstanceHost, self).__init__()
|
|
self._has_been_setup = False
|
|
self.menu = None
|
|
self.callbacks = []
|
|
self.shelves = []
|
|
|
|
def install(self):
|
|
pyblish.api.register_host("substancepainter")
|
|
|
|
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
|
register_loader_plugin_path(LOAD_PATH)
|
|
register_creator_plugin_path(CREATE_PATH)
|
|
|
|
log.info("Installing callbacks ... ")
|
|
# register_event_callback("init", on_init)
|
|
self._register_callbacks()
|
|
# register_event_callback("before.save", before_save)
|
|
# register_event_callback("save", on_save)
|
|
register_event_callback("open", on_open)
|
|
# register_event_callback("new", on_new)
|
|
|
|
log.info("Installing menu ... ")
|
|
self._install_menu()
|
|
|
|
project_settings = get_current_project_settings()
|
|
self._install_shelves(project_settings)
|
|
|
|
self._has_been_setup = True
|
|
|
|
def uninstall(self):
|
|
self._uninstall_shelves()
|
|
self._uninstall_menu()
|
|
self._deregister_callbacks()
|
|
|
|
def has_unsaved_changes(self):
|
|
|
|
if not substance_painter.project.is_open():
|
|
return False
|
|
|
|
return substance_painter.project.needs_saving()
|
|
|
|
def get_workfile_extensions(self):
|
|
return [".spp", ".toc"]
|
|
|
|
def save_workfile(self, dst_path=None):
|
|
|
|
if not substance_painter.project.is_open():
|
|
return False
|
|
|
|
if not dst_path:
|
|
dst_path = self.get_current_workfile()
|
|
|
|
full_save_mode = substance_painter.project.ProjectSaveMode.Full
|
|
substance_painter.project.save_as(dst_path, full_save_mode)
|
|
|
|
return dst_path
|
|
|
|
def open_workfile(self, filepath):
|
|
|
|
if not os.path.exists(filepath):
|
|
raise RuntimeError("File does not exist: {}".format(filepath))
|
|
|
|
# We must first explicitly close current project before opening another
|
|
if substance_painter.project.is_open():
|
|
substance_painter.project.close()
|
|
|
|
substance_painter.project.open(filepath)
|
|
return filepath
|
|
|
|
def get_current_workfile(self):
|
|
if not substance_painter.project.is_open():
|
|
return None
|
|
|
|
filepath = substance_painter.project.file_path()
|
|
if filepath and filepath.endswith(".spt"):
|
|
# When currently in a Substance Painter template assume our
|
|
# scene isn't saved. This can be the case directly after doing
|
|
# "New project", the path will then be the template used. This
|
|
# avoids Workfiles tool trying to save as .spt extension if the
|
|
# file hasn't been saved before.
|
|
return
|
|
|
|
return filepath
|
|
|
|
def get_containers(self):
|
|
|
|
if not substance_painter.project.is_open():
|
|
return
|
|
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY)
|
|
if containers:
|
|
for key, container in containers.items():
|
|
container["objectName"] = key
|
|
yield container
|
|
|
|
def update_context_data(self, data, changes):
|
|
|
|
if not substance_painter.project.is_open():
|
|
return
|
|
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data)
|
|
|
|
def get_context_data(self):
|
|
|
|
if not substance_painter.project.is_open():
|
|
return
|
|
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {}
|
|
|
|
def _install_menu(self):
|
|
from PySide2 import QtWidgets
|
|
from openpype.tools.utils import host_tools
|
|
|
|
parent = substance_painter.ui.get_main_window()
|
|
|
|
menu = QtWidgets.QMenu("OpenPype")
|
|
|
|
action = menu.addAction("Create...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_publisher(parent=parent,
|
|
tab="create")
|
|
)
|
|
|
|
action = menu.addAction("Load...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_loader(parent=parent, use_context=True)
|
|
)
|
|
|
|
action = menu.addAction("Publish...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_publisher(parent=parent,
|
|
tab="publish")
|
|
)
|
|
|
|
action = menu.addAction("Manage...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_scene_inventory(parent=parent)
|
|
)
|
|
|
|
action = menu.addAction("Library...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_library_loader(parent=parent)
|
|
)
|
|
|
|
menu.addSeparator()
|
|
action = menu.addAction("Work Files...")
|
|
action.triggered.connect(
|
|
lambda: host_tools.show_workfiles(parent=parent)
|
|
)
|
|
|
|
substance_painter.ui.add_menu(menu)
|
|
|
|
def on_menu_destroyed():
|
|
self.menu = None
|
|
|
|
menu.destroyed.connect(on_menu_destroyed)
|
|
|
|
self.menu = menu
|
|
|
|
def _uninstall_menu(self):
|
|
if self.menu:
|
|
self.menu.destroy()
|
|
self.menu = None
|
|
|
|
def _register_callbacks(self):
|
|
# Prepare emit event callbacks
|
|
open_callback = partial(emit_event, "open")
|
|
|
|
# Connect to the Substance Painter events
|
|
dispatcher = substance_painter.event.DISPATCHER
|
|
for event, callback in [
|
|
(substance_painter.event.ProjectOpened, open_callback)
|
|
]:
|
|
dispatcher.connect(event, callback)
|
|
# Keep a reference so we can deregister if needed
|
|
self.callbacks.append((event, callback))
|
|
|
|
def _deregister_callbacks(self):
|
|
for event, callback in self.callbacks:
|
|
substance_painter.event.DISPATCHER.disconnect(event, callback)
|
|
self.callbacks.clear()
|
|
|
|
def _install_shelves(self, project_settings):
|
|
|
|
shelves = project_settings["substancepainter"].get("shelves", {})
|
|
if not shelves:
|
|
return
|
|
|
|
# Prepare formatting data if we detect any path which might have
|
|
# template tokens like {asset} in there.
|
|
formatting_data = {}
|
|
has_formatting_entries = any("{" in path for path in shelves.values())
|
|
if has_formatting_entries:
|
|
project_name = self.get_current_project_name()
|
|
asset_name = self.get_current_asset_name()
|
|
task_name = self.get_current_asset_name()
|
|
system_settings = get_system_settings()
|
|
formatting_data = get_template_data_with_names(project_name,
|
|
asset_name,
|
|
task_name,
|
|
system_settings)
|
|
anatomy = Anatomy(project_name)
|
|
formatting_data["root"] = anatomy.roots
|
|
|
|
for name, path in shelves.items():
|
|
shelf_name = None
|
|
|
|
# Allow formatting with anatomy for the paths
|
|
if "{" in path:
|
|
path = StringTemplate.format_template(path, formatting_data)
|
|
|
|
try:
|
|
shelf_name = lib.load_shelf(path, name=name)
|
|
except ValueError as exc:
|
|
print(f"Failed to load shelf -> {exc}")
|
|
|
|
if shelf_name:
|
|
self.shelves.append(shelf_name)
|
|
|
|
def _uninstall_shelves(self):
|
|
for shelf_name in self.shelves:
|
|
substance_painter.resource.Shelves.remove(shelf_name)
|
|
self.shelves.clear()
|
|
|
|
|
|
def on_open():
|
|
log.info("Running callback on open..")
|
|
|
|
if any_outdated_containers():
|
|
from openpype.widgets import popup
|
|
|
|
log.warning("Scene has outdated content.")
|
|
|
|
# Get main window
|
|
parent = substance_painter.ui.get_main_window()
|
|
if parent is None:
|
|
log.info("Skipping outdated content pop-up "
|
|
"because Substance window can't be found.")
|
|
else:
|
|
|
|
# Show outdated pop-up
|
|
def _on_show_inventory():
|
|
from openpype.tools.utils import host_tools
|
|
host_tools.show_scene_inventory(parent=parent)
|
|
|
|
dialog = popup.Popup(parent=parent)
|
|
dialog.setWindowTitle("Substance scene has outdated content")
|
|
dialog.setMessage("There are outdated containers in "
|
|
"your Substance scene.")
|
|
dialog.on_clicked.connect(_on_show_inventory)
|
|
dialog.show()
|
|
|
|
|
|
def imprint_container(container,
|
|
name,
|
|
namespace,
|
|
context,
|
|
loader):
|
|
"""Imprint a loaded container with metadata.
|
|
|
|
Containerisation enables a tracking of version, author and origin
|
|
for loaded assets.
|
|
|
|
Arguments:
|
|
container (dict): The (substance metadata) dictionary to imprint into.
|
|
name (str): Name of resulting assembly
|
|
namespace (str): Namespace under which to host container
|
|
context (dict): Asset information
|
|
loader (load.LoaderPlugin): loader instance used to produce container.
|
|
|
|
Returns:
|
|
None
|
|
|
|
"""
|
|
|
|
data = [
|
|
("schema", "openpype:container-2.0"),
|
|
("id", AVALON_CONTAINER_ID),
|
|
("name", str(name)),
|
|
("namespace", str(namespace) if namespace else None),
|
|
("loader", str(loader.__class__.__name__)),
|
|
("representation", str(context["representation"]["_id"])),
|
|
]
|
|
for key, value in data:
|
|
container[key] = value
|
|
|
|
|
|
def set_container_metadata(object_name, container_data, update=False):
|
|
"""Helper method to directly set the data for a specific container
|
|
|
|
Args:
|
|
object_name (str): The unique object name identifier for the container
|
|
container_data (dict): The data for the container.
|
|
Note 'objectName' data is derived from `object_name` and key in
|
|
`container_data` will be ignored.
|
|
update (bool): Whether to only update the dict data.
|
|
|
|
"""
|
|
# The objectName is derived from the key in the metadata so won't be stored
|
|
# in the metadata in the container's data.
|
|
container_data.pop("objectName", None)
|
|
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {}
|
|
if update:
|
|
existing_data = containers.setdefault(object_name, {})
|
|
existing_data.update(container_data) # mutable dict, in-place update
|
|
else:
|
|
containers[object_name] = container_data
|
|
metadata.set("containers", containers)
|
|
|
|
|
|
def remove_container_metadata(object_name):
|
|
"""Helper method to remove the data for a specific container"""
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY)
|
|
if containers:
|
|
containers.pop(object_name, None)
|
|
metadata.set("containers", containers)
|
|
|
|
|
|
def set_instance(instance_id, instance_data, update=False):
|
|
"""Helper method to directly set the data for a specific container
|
|
|
|
Args:
|
|
instance_id (str): Unique identifier for the instance
|
|
instance_data (dict): The instance data to store in the metaadata.
|
|
"""
|
|
set_instances({instance_id: instance_data}, update=update)
|
|
|
|
|
|
def set_instances(instance_data_by_id, update=False):
|
|
"""Store data for multiple instances at the same time.
|
|
|
|
This is more optimal than querying and setting them in the metadata one
|
|
by one.
|
|
"""
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}
|
|
|
|
for instance_id, instance_data in instance_data_by_id.items():
|
|
if update:
|
|
existing_data = instances.get(instance_id, {})
|
|
existing_data.update(instance_data)
|
|
else:
|
|
instances[instance_id] = instance_data
|
|
|
|
metadata.set("instances", instances)
|
|
|
|
|
|
def remove_instance(instance_id):
|
|
"""Helper method to remove the data for a specific container"""
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}
|
|
instances.pop(instance_id, None)
|
|
metadata.set("instances", instances)
|
|
|
|
|
|
def get_instances_by_id():
|
|
"""Return all instances stored in the project instances metadata"""
|
|
if not substance_painter.project.is_open():
|
|
return {}
|
|
|
|
metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY)
|
|
return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {}
|
|
|
|
|
|
def get_instances():
|
|
"""Return all instances stored in the project instances as a list"""
|
|
return list(get_instances_by_id().values())
|