mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/OP-1017_houdini-colorspaces
This commit is contained in:
commit
5a48baddf4
91 changed files with 1049 additions and 850 deletions
|
|
@ -50,7 +50,7 @@ IGNORED_MODULES_IN_AYON = set()
|
|||
# When addon was moved from ayon-core codebase
|
||||
# - this is used to log the missing addon
|
||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||
"applications": VersionInfo(2, 0, 0),
|
||||
"applications": VersionInfo(0, 2, 0),
|
||||
}
|
||||
|
||||
# Inherit from `object` for Python 2 hosts
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
|
||||
import bpy
|
||||
|
||||
from ayon_core.lib import BoolDef
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.blender.api import plugin
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
folder_name = instance.data["folderEntity"]["name"]
|
||||
|
|
@ -46,7 +49,8 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
|||
bpy.ops.wm.alembic_export(
|
||||
filepath=filepath,
|
||||
selected=True,
|
||||
flatten=False
|
||||
flatten=False,
|
||||
subdiv_schema=attr_values.get("subdiv_schema", False)
|
||||
)
|
||||
|
||||
plugin.deselect_all()
|
||||
|
|
@ -65,6 +69,21 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
|||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
return [
|
||||
BoolDef(
|
||||
"subdiv_schema",
|
||||
label="Alembic Mesh Subdiv Schema",
|
||||
tooltip="Export Meshes using Alembic's subdivision schema.\n"
|
||||
"Enabling this includes creases with the export but "
|
||||
"excludes the mesh's normals.\n"
|
||||
"Enabling this usually result in smaller file size "
|
||||
"due to lack of normals.",
|
||||
default=False
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class ExtractModelABC(ExtractABC):
|
||||
"""Extract model as ABC."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from ayon_core.lib import PreLaunchHook
|
||||
from ayon_applications import PreLaunchHook
|
||||
from ayon_core.hosts.fusion import FUSION_HOST_DIR
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,33 +45,11 @@ class AbcLoader(load.LoaderPlugin):
|
|||
alembic = container.createNode("alembic", node_name=node_name)
|
||||
alembic.setParms({"fileName": file_path})
|
||||
|
||||
# Add unpack node
|
||||
unpack_name = "unpack_{}".format(name)
|
||||
unpack = container.createNode("unpack", node_name=unpack_name)
|
||||
unpack.setInput(0, alembic)
|
||||
unpack.setParms({"transfer_attributes": "path"})
|
||||
# Position nodes nicely
|
||||
container.moveToGoodPosition()
|
||||
container.layoutChildren()
|
||||
|
||||
# Add normal to points
|
||||
# Order of menu ['point', 'vertex', 'prim', 'detail']
|
||||
normal_name = "normal_{}".format(name)
|
||||
normal_node = container.createNode("normal", node_name=normal_name)
|
||||
normal_node.setParms({"type": 0})
|
||||
|
||||
normal_node.setInput(0, unpack)
|
||||
|
||||
null = container.createNode("null", node_name="OUT")
|
||||
null.setInput(0, normal_node)
|
||||
|
||||
# Ensure display flag is on the Alembic input node and not on the OUT
|
||||
# node to optimize "debug" displaying in the viewport.
|
||||
alembic.setDisplayFlag(True)
|
||||
|
||||
# Set new position for unpack node else it gets cluttered
|
||||
nodes = [container, alembic, unpack, normal_node, null]
|
||||
for nr, node in enumerate(nodes):
|
||||
node.setPosition([0, (0 - nr)])
|
||||
|
||||
self[:] = nodes
|
||||
nodes = [container, alembic]
|
||||
|
||||
return pipeline.containerise(
|
||||
node_name,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ from typing import Any, Dict, Union
|
|||
import six
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import get_current_project_name, colorspace
|
||||
from ayon_core.pipeline import (
|
||||
get_current_project_name,
|
||||
get_current_folder_path,
|
||||
get_current_task_name,
|
||||
colorspace
|
||||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline.context_tools import (
|
||||
get_current_folder_entity,
|
||||
get_current_task_entity
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from pymxs import runtime as rt
|
||||
|
|
@ -221,41 +226,30 @@ def reset_scene_resolution():
|
|||
scene resolution can be overwritten by a folder if the folder.attrib
|
||||
contains any information regarding scene resolution.
|
||||
"""
|
||||
|
||||
folder_entity = get_current_folder_entity(
|
||||
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}
|
||||
)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
width = int(folder_attributes["resolutionWidth"])
|
||||
height = int(folder_attributes["resolutionHeight"])
|
||||
task_attributes = get_current_task_entity(fields={"attrib"})["attrib"]
|
||||
width = int(task_attributes["resolutionWidth"])
|
||||
height = int(task_attributes["resolutionHeight"])
|
||||
|
||||
set_scene_resolution(width, height)
|
||||
|
||||
|
||||
def get_frame_range(folder_entiy=None) -> Union[Dict[str, Any], None]:
|
||||
"""Get the current folder frame range and handles.
|
||||
def get_frame_range(task_entity=None) -> Union[Dict[str, Any], None]:
|
||||
"""Get the current task frame range and handles
|
||||
|
||||
Args:
|
||||
folder_entiy (dict): Folder eneity.
|
||||
task_entity (dict): Task Entity.
|
||||
|
||||
Returns:
|
||||
dict: with frame start, frame end, handle start, handle end.
|
||||
"""
|
||||
# Set frame start/end
|
||||
if folder_entiy is None:
|
||||
folder_entiy = get_current_folder_entity()
|
||||
|
||||
folder_attributes = folder_entiy["attrib"]
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
return {}
|
||||
|
||||
frame_start = int(frame_start)
|
||||
frame_end = int(frame_end)
|
||||
handle_start = int(folder_attributes.get("handleStart", 0))
|
||||
handle_end = int(folder_attributes.get("handleEnd", 0))
|
||||
if task_entity is None:
|
||||
task_entity = get_current_task_entity(fields={"attrib"})
|
||||
task_attributes = task_entity["attrib"]
|
||||
frame_start = int(task_attributes["frameStart"])
|
||||
frame_end = int(task_attributes["frameEnd"])
|
||||
handle_start = int(task_attributes["handleStart"])
|
||||
handle_end = int(task_attributes["handleEnd"])
|
||||
frame_start_handle = frame_start - handle_start
|
||||
frame_end_handle = frame_end + handle_end
|
||||
|
||||
|
|
@ -281,9 +275,9 @@ def reset_frame_range(fps: bool = True):
|
|||
scene frame rate in frames-per-second.
|
||||
"""
|
||||
if fps:
|
||||
project_name = get_current_project_name()
|
||||
project_entity = ayon_api.get_project(project_name)
|
||||
fps_number = float(project_entity["attrib"].get("fps"))
|
||||
task_entity = get_current_task_entity()
|
||||
task_attributes = task_entity["attrib"]
|
||||
fps_number = float(task_attributes["fps"])
|
||||
rt.frameRate = fps_number
|
||||
frame_range = get_frame_range()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin,
|
|||
return
|
||||
|
||||
frame_range = get_frame_range(
|
||||
instance.data["folderEntity"])
|
||||
instance.data["taskEntity"])
|
||||
|
||||
inst_frame_start = instance.data.get("frameStartHandle")
|
||||
inst_frame_end = instance.data.get("frameEndHandle")
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin,
|
|||
context_label = "{} > {}".format(*context)
|
||||
instance_label = "{} > {}".format(folderPath, task)
|
||||
message = (
|
||||
"Instance '{}' publishes to different folder or task "
|
||||
"Instance '{}' publishes to different context(folder or task) "
|
||||
"than current context: {}. Current context: {}".format(
|
||||
instance.name, instance_label, context_label
|
||||
)
|
||||
|
|
@ -46,7 +46,7 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin,
|
|||
raise PublishValidationError(
|
||||
message=message,
|
||||
description=(
|
||||
"## Publishing to a different context folder or task\n"
|
||||
"## Publishing to a different context data(folder or task)\n"
|
||||
"There are publish instances present which are publishing "
|
||||
"into a different folder path or task than your current context.\n\n"
|
||||
"Usually this is not what you want but there can be cases "
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ from ayon_core.pipeline.publish import (
|
|||
RepairAction,
|
||||
PublishValidationError
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib import reset_scene_resolution
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
reset_scene_resolution,
|
||||
imprint
|
||||
)
|
||||
|
||||
|
||||
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||
|
|
@ -25,8 +28,10 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
width, height = self.get_folder_resolution(instance)
|
||||
current_width = rt.renderWidth
|
||||
current_height = rt.renderHeight
|
||||
current_width, current_height = (
|
||||
self.get_current_resolution(instance)
|
||||
)
|
||||
|
||||
if current_width != width and current_height != height:
|
||||
raise PublishValidationError("Resolution Setting "
|
||||
"not matching resolution "
|
||||
|
|
@ -41,12 +46,16 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
def get_folder_resolution(self, instance):
|
||||
folder_entity = instance.data["folderEntity"]
|
||||
if folder_entity:
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
width = folder_attributes["resolutionWidth"]
|
||||
height = folder_attributes["resolutionHeight"]
|
||||
def get_current_resolution(self, instance):
|
||||
return rt.renderWidth, rt.renderHeight
|
||||
|
||||
@classmethod
|
||||
def get_folder_resolution(cls, instance):
|
||||
task_entity = instance.data.get("taskEntity")
|
||||
if task_entity:
|
||||
task_attributes = task_entity["attrib"]
|
||||
width = task_attributes["resolutionWidth"]
|
||||
height = task_attributes["resolutionHeight"]
|
||||
return int(width), int(height)
|
||||
|
||||
# Defaults if not found in folder entity
|
||||
|
|
@ -55,3 +64,29 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
@classmethod
|
||||
def repair(cls, instance):
|
||||
reset_scene_resolution()
|
||||
|
||||
|
||||
class ValidateReviewResolutionSetting(ValidateResolutionSetting):
|
||||
families = ["review"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def get_current_resolution(self, instance):
|
||||
current_width = instance.data["review_width"]
|
||||
current_height = instance.data["review_height"]
|
||||
return current_width, current_height
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
context_width, context_height = (
|
||||
cls.get_folder_resolution(instance)
|
||||
)
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
creator_attrs["review_width"] = context_width
|
||||
creator_attrs["review_height"] = context_height
|
||||
creator_attrs_data = {
|
||||
"creator_attributes": creator_attrs
|
||||
}
|
||||
# update the width and height of review
|
||||
# data in creator_attributes
|
||||
imprint(instance.data["instance_node"], creator_attrs_data)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from .lib import pairwise
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _allow_export_from_render_setup_layer():
|
||||
def allow_export_from_render_setup_layer():
|
||||
"""Context manager to override Maya settings to allow RS layer export"""
|
||||
try:
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ def export_in_rs_layer(path, nodes, export=None):
|
|||
cmds.disconnectAttr(src, dest)
|
||||
|
||||
# Export Selected
|
||||
with _allow_export_from_render_setup_layer():
|
||||
with allow_export_from_render_setup_layer():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
if export:
|
||||
export()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ConnectGeometry(InventoryAction):
|
|||
repre_id = container["representation"]
|
||||
repre_context = repre_contexts_by_id[repre_id]
|
||||
|
||||
product_type = repre_context["prouct"]["productType"]
|
||||
product_type = repre_context["product"]["productType"]
|
||||
|
||||
containers_by_product_type.setdefault(product_type, [])
|
||||
containers_by_product_type[product_type].append(container)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ConnectXgen(InventoryAction):
|
|||
repre_id = container["representation"]
|
||||
repre_context = repre_contexts_by_id[repre_id]
|
||||
|
||||
product_type = repre_context["prouct"]["productType"]
|
||||
product_type = repre_context["product"]["productType"]
|
||||
|
||||
containers_by_product_type.setdefault(product_type, [])
|
||||
containers_by_product_type[product_type].append(container)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class ConnectYetiRig(InventoryAction):
|
|||
repre_id = container["representation"]
|
||||
repre_context = repre_contexts_by_id[repre_id]
|
||||
|
||||
product_type = repre_context["prouct"]["productType"]
|
||||
product_type = repre_context["product"]["productType"]
|
||||
|
||||
containers_by_product_type.setdefault(product_type, [])
|
||||
containers_by_product_type[product_type].append(container)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
from typing import List
|
||||
|
||||
import maya.cmds as cmds
|
||||
|
||||
from ayon_core.hosts.maya.api import plugin
|
||||
from ayon_core.hosts.maya.api import lib
|
||||
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
|
||||
|
||||
class YetiRigLoader(plugin.ReferenceLoader):
|
||||
"""This loader will load Yeti rig."""
|
||||
|
|
@ -15,6 +20,9 @@ class YetiRigLoader(plugin.ReferenceLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
# From settings
|
||||
create_cache_instance_on_load = True
|
||||
|
||||
def process_reference(
|
||||
self, context, name=None, namespace=None, options=None
|
||||
):
|
||||
|
|
@ -49,4 +57,41 @@ class YetiRigLoader(plugin.ReferenceLoader):
|
|||
)
|
||||
self[:] = nodes
|
||||
|
||||
if self.create_cache_instance_on_load:
|
||||
# Automatically create in instance to allow publishing the loaded
|
||||
# yeti rig into a yeti cache
|
||||
self._create_yeti_cache_instance(nodes, variant=namespace)
|
||||
|
||||
return nodes
|
||||
|
||||
def _create_yeti_cache_instance(self, nodes: List[str], variant: str):
|
||||
"""Create a yeticache product type instance to publish the output.
|
||||
|
||||
This is similar to how loading animation rig will automatically create
|
||||
an animation instance for publishing any loaded character rigs, but
|
||||
then for yeti rigs.
|
||||
|
||||
Args:
|
||||
nodes (List[str]): Nodes generated on load.
|
||||
variant (str): Variant for the yeti cache instance to create.
|
||||
|
||||
"""
|
||||
|
||||
# Find the roots amongst the loaded nodes
|
||||
yeti_nodes = cmds.ls(nodes, type="pgYetiMaya", long=True)
|
||||
assert yeti_nodes, "No pgYetiMaya nodes in rig, this is a bug."
|
||||
|
||||
self.log.info("Creating variant: {}".format(variant))
|
||||
|
||||
creator_identifier = "io.openpype.creators.maya.yeticache"
|
||||
|
||||
host = registered_host()
|
||||
create_context = CreateContext(host)
|
||||
|
||||
with lib.maintained_selection():
|
||||
cmds.select(yeti_nodes, noExpand=True)
|
||||
create_context.create(
|
||||
creator_identifier=creator_identifier,
|
||||
variant=variant,
|
||||
pre_create_data={"use_selection": True}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CollectFileDependencies(pyblish.api.ContextPlugin):
|
|||
families = ["renderlayer"]
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
def apply_settings(cls, project_settings):
|
||||
# Disable plug-in if not used for deadline submission anyway
|
||||
settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa
|
||||
cls.enabled = settings.get("asset_dependencies", True)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ import os
|
|||
from maya import cmds
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.maya.api.lib import maintained_selection
|
||||
from ayon_core.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
renderlayer
|
||||
)
|
||||
from ayon_core.hosts.maya.api.render_setup_tools import (
|
||||
allow_export_from_render_setup_layer
|
||||
)
|
||||
|
||||
|
||||
class ExtractRedshiftProxy(publish.Extractor):
|
||||
|
|
@ -18,6 +24,9 @@ class ExtractRedshiftProxy(publish.Extractor):
|
|||
def process(self, instance):
|
||||
"""Extractor entry point."""
|
||||
|
||||
# Make sure Redshift is loaded
|
||||
cmds.loadPlugin("redshift4maya", quiet=True)
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
file_name = "{}.rs".format(instance.name)
|
||||
file_path = os.path.join(staging_dir, file_name)
|
||||
|
|
@ -60,14 +69,22 @@ class ExtractRedshiftProxy(publish.Extractor):
|
|||
|
||||
# Write out rs file
|
||||
self.log.debug("Writing: '%s'" % file_path)
|
||||
|
||||
# Allow overriding what renderlayer to export from. By default force
|
||||
# it to the default render layer. (Note that the renderlayer isn't
|
||||
# currently exposed as an attribute to artists)
|
||||
layer = instance.data.get("renderLayer", "defaultRenderLayer")
|
||||
|
||||
with maintained_selection():
|
||||
cmds.select(instance.data["setMembers"], noExpand=True)
|
||||
cmds.file(file_path,
|
||||
pr=False,
|
||||
force=True,
|
||||
type="Redshift Proxy",
|
||||
exportSelected=True,
|
||||
options=rs_options)
|
||||
with renderlayer(layer):
|
||||
with allow_export_from_render_setup_layer():
|
||||
cmds.select(instance.data["setMembers"], noExpand=True)
|
||||
cmds.file(file_path,
|
||||
preserveReferences=False,
|
||||
force=True,
|
||||
type="Redshift Proxy",
|
||||
exportSelected=True,
|
||||
options=rs_options)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -586,7 +586,6 @@ def prompt_new_file_with_mesh(mesh_filepath):
|
|||
# TODO: find a way to improve the process event to
|
||||
# load more complicated mesh
|
||||
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 3000)
|
||||
|
||||
file_dialog.done(file_dialog.Accepted)
|
||||
app.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
|
|
@ -606,7 +605,7 @@ def prompt_new_file_with_mesh(mesh_filepath):
|
|||
mesh_select.setVisible(False)
|
||||
|
||||
# Ensure UI is visually up-to-date
|
||||
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents)
|
||||
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000)
|
||||
|
||||
# Trigger the 'select file' dialog to set the path and have the
|
||||
# new file dialog to use the path.
|
||||
|
|
@ -623,8 +622,6 @@ def prompt_new_file_with_mesh(mesh_filepath):
|
|||
"Failed to set mesh path with the prompt dialog:"
|
||||
f"{mesh_filepath}\n\n"
|
||||
"Creating new project directly with the mesh path instead.")
|
||||
else:
|
||||
dialog.done(dialog.Accepted)
|
||||
|
||||
new_action = _get_new_project_action()
|
||||
if not new_action:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import copy
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from ayon_core.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
|
|
@ -8,10 +10,133 @@ from ayon_core.hosts.substancepainter.api.pipeline import (
|
|||
set_container_metadata,
|
||||
remove_container_metadata
|
||||
)
|
||||
from ayon_core.hosts.substancepainter.api.lib import prompt_new_file_with_mesh
|
||||
|
||||
import substance_painter.project
|
||||
import qargparse
|
||||
|
||||
|
||||
def _convert(substance_attr):
|
||||
"""Return Substance Painter Python API Project attribute from string.
|
||||
|
||||
This converts a string like "ProjectWorkflow.Default" to for example
|
||||
the Substance Painter Python API equivalent object, like:
|
||||
`substance_painter.project.ProjectWorkflow.Default`
|
||||
|
||||
Args:
|
||||
substance_attr (str): The `substance_painter.project` attribute,
|
||||
for example "ProjectWorkflow.Default"
|
||||
|
||||
Returns:
|
||||
Any: Substance Python API object of the project attribute.
|
||||
|
||||
Raises:
|
||||
ValueError: If attribute does not exist on the
|
||||
`substance_painter.project` python api.
|
||||
"""
|
||||
root = substance_painter.project
|
||||
for attr in substance_attr.split("."):
|
||||
root = getattr(root, attr, None)
|
||||
if root is None:
|
||||
raise ValueError(
|
||||
"Substance Painter project attribute"
|
||||
f" does not exist: {substance_attr}")
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def get_template_by_name(name: str, templates: list[dict]) -> dict:
|
||||
return next(
|
||||
template for template in templates
|
||||
if template["name"] == name
|
||||
)
|
||||
|
||||
|
||||
class SubstanceProjectConfigurationWindow(QtWidgets.QDialog):
|
||||
"""The pop-up dialog allows users to choose material
|
||||
duplicate options for importing Max objects when updating
|
||||
or switching assets.
|
||||
"""
|
||||
def __init__(self, project_templates):
|
||||
super(SubstanceProjectConfigurationWindow, self).__init__()
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
|
||||
self.configuration = None
|
||||
self.template_names = [template["name"] for template
|
||||
in project_templates]
|
||||
self.project_templates = project_templates
|
||||
|
||||
self.widgets = {
|
||||
"label": QtWidgets.QLabel(
|
||||
"Select your template for project configuration"),
|
||||
"template_options": QtWidgets.QComboBox(),
|
||||
"import_cameras": QtWidgets.QCheckBox("Import Cameras"),
|
||||
"preserve_strokes": QtWidgets.QCheckBox("Preserve Strokes"),
|
||||
"clickbox": QtWidgets.QWidget(),
|
||||
"combobox": QtWidgets.QWidget(),
|
||||
"buttons": QtWidgets.QDialogButtonBox(
|
||||
QtWidgets.QDialogButtonBox.Ok
|
||||
| QtWidgets.QDialogButtonBox.Cancel)
|
||||
}
|
||||
|
||||
self.widgets["template_options"].addItems(self.template_names)
|
||||
|
||||
template_name = self.widgets["template_options"].currentText()
|
||||
self._update_to_match_template(template_name)
|
||||
# Build clickboxes
|
||||
layout = QtWidgets.QHBoxLayout(self.widgets["clickbox"])
|
||||
layout.addWidget(self.widgets["import_cameras"])
|
||||
layout.addWidget(self.widgets["preserve_strokes"])
|
||||
# Build combobox
|
||||
layout = QtWidgets.QHBoxLayout(self.widgets["combobox"])
|
||||
layout.addWidget(self.widgets["template_options"])
|
||||
# Build buttons
|
||||
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
|
||||
# Build layout.
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(self.widgets["label"])
|
||||
layout.addWidget(self.widgets["combobox"])
|
||||
layout.addWidget(self.widgets["clickbox"])
|
||||
layout.addWidget(self.widgets["buttons"])
|
||||
|
||||
self.widgets["template_options"].currentTextChanged.connect(
|
||||
self._update_to_match_template)
|
||||
self.widgets["buttons"].accepted.connect(self.on_accept)
|
||||
self.widgets["buttons"].rejected.connect(self.on_reject)
|
||||
|
||||
def on_accept(self):
|
||||
self.configuration = self.get_project_configuration()
|
||||
self.close()
|
||||
|
||||
def on_reject(self):
|
||||
self.close()
|
||||
|
||||
def _update_to_match_template(self, template_name):
|
||||
template = get_template_by_name(template_name, self.project_templates)
|
||||
self.widgets["import_cameras"].setChecked(template["import_cameras"])
|
||||
self.widgets["preserve_strokes"].setChecked(
|
||||
template["preserve_strokes"])
|
||||
|
||||
def get_project_configuration(self):
|
||||
templates = self.project_templates
|
||||
template_name = self.widgets["template_options"].currentText()
|
||||
template = get_template_by_name(template_name, templates)
|
||||
template = copy.deepcopy(template) # do not edit the original
|
||||
template["import_cameras"] = self.widgets["import_cameras"].isChecked()
|
||||
template["preserve_strokes"] = (
|
||||
self.widgets["preserve_strokes"].isChecked()
|
||||
)
|
||||
for key in ["normal_map_format",
|
||||
"project_workflow",
|
||||
"tangent_space_mode"]:
|
||||
template[key] = _convert(template[key])
|
||||
return template
|
||||
|
||||
@classmethod
|
||||
def prompt(cls, templates):
|
||||
dialog = cls(templates)
|
||||
dialog.exec_()
|
||||
configuration = dialog.configuration
|
||||
dialog.deleteLater()
|
||||
return configuration
|
||||
|
||||
|
||||
class SubstanceLoadProjectMesh(load.LoaderPlugin):
|
||||
|
|
@ -25,48 +150,35 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
options = [
|
||||
qargparse.Boolean(
|
||||
"preserve_strokes",
|
||||
default=True,
|
||||
help="Preserve strokes positions on mesh.\n"
|
||||
"(only relevant when loading into existing project)"
|
||||
),
|
||||
qargparse.Boolean(
|
||||
"import_cameras",
|
||||
default=True,
|
||||
help="Import cameras from the mesh file."
|
||||
)
|
||||
]
|
||||
# Defined via settings
|
||||
project_templates = []
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
def load(self, context, name, namespace, options=None):
|
||||
|
||||
# Get user inputs
|
||||
import_cameras = data.get("import_cameras", True)
|
||||
preserve_strokes = data.get("preserve_strokes", True)
|
||||
sp_settings = substance_painter.project.Settings(
|
||||
import_cameras=import_cameras
|
||||
)
|
||||
result = SubstanceProjectConfigurationWindow.prompt(
|
||||
self.project_templates)
|
||||
if not result:
|
||||
# cancelling loader action
|
||||
return
|
||||
if not substance_painter.project.is_open():
|
||||
# Allow to 'initialize' a new project
|
||||
path = self.filepath_from_context(context)
|
||||
# TODO: improve the prompt dialog function to not
|
||||
# only works for simple polygon scene
|
||||
result = prompt_new_file_with_mesh(mesh_filepath=path)
|
||||
if not result:
|
||||
self.log.info("User cancelled new project prompt."
|
||||
"Creating new project directly from"
|
||||
" Substance Painter API Instead.")
|
||||
settings = substance_painter.project.create(
|
||||
mesh_file_path=path, settings=sp_settings
|
||||
)
|
||||
|
||||
sp_settings = substance_painter.project.Settings(
|
||||
import_cameras=result["import_cameras"],
|
||||
normal_map_format=result["normal_map_format"],
|
||||
project_workflow=result["project_workflow"],
|
||||
tangent_space_mode=result["tangent_space_mode"],
|
||||
default_texture_resolution=result["default_texture_resolution"]
|
||||
)
|
||||
settings = substance_painter.project.create(
|
||||
mesh_file_path=path, settings=sp_settings
|
||||
)
|
||||
else:
|
||||
# Reload the mesh
|
||||
settings = substance_painter.project.MeshReloadingSettings(
|
||||
import_cameras=import_cameras,
|
||||
preserve_strokes=preserve_strokes
|
||||
)
|
||||
import_cameras=result["import_cameras"],
|
||||
preserve_strokes=result["preserve_strokes"])
|
||||
|
||||
def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa
|
||||
if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa
|
||||
|
|
@ -92,7 +204,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
|
|||
# from the user's original choice. We don't store 'preserve_strokes'
|
||||
# as we always preserve strokes on updates.
|
||||
container["options"] = {
|
||||
"import_cameras": import_cameras,
|
||||
"import_cameras": result["import_cameras"],
|
||||
}
|
||||
|
||||
set_container_metadata(project_mesh_object_name, container)
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ class AbstractTemplateBuilder(object):
|
|||
is good practice to check if the same value is not already stored under
|
||||
different key or if the key is not already used for something else.
|
||||
|
||||
Key should be self explanatory to content.
|
||||
Key should be self-explanatory to content.
|
||||
- wrong: 'folder'
|
||||
- good: 'folder_name'
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ class AbstractTemplateBuilder(object):
|
|||
is good practice to check if the same value is not already stored under
|
||||
different key or if the key is not already used for something else.
|
||||
|
||||
Key should be self explanatory to content.
|
||||
Key should be self-explanatory to content.
|
||||
- wrong: 'folder'
|
||||
- good: 'folder_path'
|
||||
|
||||
|
|
@ -395,7 +395,7 @@ class AbstractTemplateBuilder(object):
|
|||
is good practice to check if the same value is not already stored under
|
||||
different key or if the key is not already used for something else.
|
||||
|
||||
Key should be self explanatory to content.
|
||||
Key should be self-explanatory to content.
|
||||
- wrong: 'folder'
|
||||
- good: 'folder_path'
|
||||
|
||||
|
|
@ -466,7 +466,7 @@ class AbstractTemplateBuilder(object):
|
|||
|
||||
return list(sorted(
|
||||
placeholders,
|
||||
key=lambda i: i.order
|
||||
key=lambda placeholder: placeholder.order
|
||||
))
|
||||
|
||||
def build_template(
|
||||
|
|
@ -685,7 +685,7 @@ class AbstractTemplateBuilder(object):
|
|||
for placeholder in placeholders
|
||||
}
|
||||
all_processed = len(placeholders) == 0
|
||||
# Counter is checked at the ned of a loop so the loop happens at least
|
||||
# Counter is checked at the end of a loop so the loop happens at least
|
||||
# once.
|
||||
iter_counter = 0
|
||||
while not all_processed:
|
||||
|
|
@ -1045,7 +1045,7 @@ class PlaceholderPlugin(object):
|
|||
|
||||
Using shared data from builder but stored under plugin identifier.
|
||||
|
||||
Key should be self explanatory to content.
|
||||
Key should be self-explanatory to content.
|
||||
- wrong: 'folder'
|
||||
- good: 'folder_path'
|
||||
|
||||
|
|
@ -1085,7 +1085,7 @@ class PlaceholderPlugin(object):
|
|||
|
||||
Using shared data from builder but stored under plugin identifier.
|
||||
|
||||
Key should be self explanatory to content.
|
||||
Key should be self-explanatory to content.
|
||||
- wrong: 'folder'
|
||||
- good: 'folder_path'
|
||||
|
||||
|
|
@ -1107,10 +1107,10 @@ class PlaceholderItem(object):
|
|||
"""Item representing single item in scene that is a placeholder to process.
|
||||
|
||||
Items are always created and updated by their plugins. Each plugin can use
|
||||
modified class of 'PlacehoderItem' but only to add more options instead of
|
||||
modified class of 'PlaceholderItem' but only to add more options instead of
|
||||
new other.
|
||||
|
||||
Scene identifier is used to avoid processing of the palceholder item
|
||||
Scene identifier is used to avoid processing of the placeholder item
|
||||
multiple times so must be unique across whole workfile builder.
|
||||
|
||||
Args:
|
||||
|
|
@ -1162,7 +1162,7 @@ class PlaceholderItem(object):
|
|||
"""Placeholder data which can modify how placeholder is processed.
|
||||
|
||||
Possible general keys
|
||||
- order: Can define the order in which is palceholder processed.
|
||||
- order: Can define the order in which is placeholder processed.
|
||||
Lower == earlier.
|
||||
|
||||
Other keys are defined by placeholder and should validate them on item
|
||||
|
|
@ -1264,11 +1264,9 @@ class PlaceholderLoadMixin(object):
|
|||
"""Unified attribute definitions for load placeholder.
|
||||
|
||||
Common function for placeholder plugins used for loading of
|
||||
repsentations. Use it in 'get_placeholder_options'.
|
||||
representations. Use it in 'get_placeholder_options'.
|
||||
|
||||
Args:
|
||||
plugin (PlaceholderPlugin): Plugin used for loading of
|
||||
representations.
|
||||
options (Dict[str, Any]): Already available options which are used
|
||||
as defaults for attributes.
|
||||
|
||||
|
|
@ -1468,7 +1466,9 @@ class PlaceholderLoadMixin(object):
|
|||
product_name_regex = None
|
||||
if product_name_regex_value:
|
||||
product_name_regex = re.compile(product_name_regex_value)
|
||||
product_type = placeholder.data["family"]
|
||||
product_type = placeholder.data.get("product_type")
|
||||
if product_type is None:
|
||||
product_type = placeholder.data["family"]
|
||||
|
||||
builder_type = placeholder.data["builder_type"]
|
||||
folder_ids = []
|
||||
|
|
@ -1529,35 +1529,22 @@ class PlaceholderLoadMixin(object):
|
|||
|
||||
pass
|
||||
|
||||
def _reduce_last_version_repre_entities(self, representations):
|
||||
"""Reduce representations to last verison."""
|
||||
def _reduce_last_version_repre_entities(self, repre_contexts):
|
||||
"""Reduce representations to last version."""
|
||||
|
||||
mapping = {}
|
||||
# TODO use representation context with entities
|
||||
# - using 'folder', 'subset' and 'version' from context on
|
||||
# representation is danger
|
||||
for repre_entity in representations:
|
||||
repre_context = repre_entity["context"]
|
||||
|
||||
folder_name = repre_context["asset"]
|
||||
product_name = repre_context["subset"]
|
||||
version = repre_context.get("version", -1)
|
||||
|
||||
if folder_name not in mapping:
|
||||
mapping[folder_name] = {}
|
||||
|
||||
product_mapping = mapping[folder_name]
|
||||
if product_name not in product_mapping:
|
||||
product_mapping[product_name] = collections.defaultdict(list)
|
||||
|
||||
version_mapping = product_mapping[product_name]
|
||||
version_mapping[version].append(repre_entity)
|
||||
version_mapping_by_product_id = {}
|
||||
for repre_context in repre_contexts:
|
||||
product_id = repre_context["product"]["id"]
|
||||
version = repre_context["version"]["version"]
|
||||
version_mapping = version_mapping_by_product_id.setdefault(
|
||||
product_id, {}
|
||||
)
|
||||
version_mapping.setdefault(version, []).append(repre_context)
|
||||
|
||||
output = []
|
||||
for product_mapping in mapping.values():
|
||||
for version_mapping in product_mapping.values():
|
||||
last_version = tuple(sorted(version_mapping.keys()))[-1]
|
||||
output.extend(version_mapping[last_version])
|
||||
for version_mapping in version_mapping_by_product_id.values():
|
||||
last_version = max(version_mapping.keys())
|
||||
output.extend(version_mapping[last_version])
|
||||
return output
|
||||
|
||||
def populate_load_placeholder(self, placeholder, ignore_repre_ids=None):
|
||||
|
|
@ -1585,32 +1572,33 @@ class PlaceholderLoadMixin(object):
|
|||
loader_name = placeholder.data["loader"]
|
||||
loader_args = self.parse_loader_args(placeholder.data["loader_args"])
|
||||
|
||||
placeholder_representations = self._get_representations(placeholder)
|
||||
placeholder_representations = [
|
||||
repre_entity
|
||||
for repre_entity in self._get_representations(placeholder)
|
||||
if repre_entity["id"] not in ignore_repre_ids
|
||||
]
|
||||
|
||||
filtered_representations = []
|
||||
for representation in self._reduce_last_version_repre_entities(
|
||||
placeholder_representations
|
||||
):
|
||||
repre_id = representation["id"]
|
||||
if repre_id not in ignore_repre_ids:
|
||||
filtered_representations.append(representation)
|
||||
|
||||
if not filtered_representations:
|
||||
repre_load_contexts = get_representation_contexts(
|
||||
self.project_name, placeholder_representations
|
||||
)
|
||||
filtered_repre_contexts = self._reduce_last_version_repre_entities(
|
||||
repre_load_contexts.values()
|
||||
)
|
||||
if not filtered_repre_contexts:
|
||||
self.log.info((
|
||||
"There's no representation for this placeholder: {}"
|
||||
).format(placeholder.scene_identifier))
|
||||
if not placeholder.data.get("keep_placeholder", True):
|
||||
self.delete_placeholder(placeholder)
|
||||
return
|
||||
|
||||
repre_load_contexts = get_representation_contexts(
|
||||
self.project_name, filtered_representations
|
||||
)
|
||||
loaders_by_name = self.builder.get_loaders_by_name()
|
||||
self._before_placeholder_load(
|
||||
placeholder
|
||||
)
|
||||
|
||||
failed = False
|
||||
for repre_load_context in repre_load_contexts.values():
|
||||
for repre_load_context in filtered_repre_contexts:
|
||||
folder_path = repre_load_context["folder"]["path"]
|
||||
product_name = repre_load_context["product"]["name"]
|
||||
representation = repre_load_context["representation"]
|
||||
|
|
@ -1695,8 +1683,6 @@ class PlaceholderCreateMixin(object):
|
|||
publishable instances. Use it with 'get_placeholder_options'.
|
||||
|
||||
Args:
|
||||
plugin (PlaceholderPlugin): Plugin used for creating of
|
||||
publish instances.
|
||||
options (Dict[str, Any]): Already available options which are used
|
||||
as defaults for attributes.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,501 +1,426 @@
|
|||
# TODO This plugin is not converted for AYON
|
||||
#
|
||||
# import collections
|
||||
# import os
|
||||
# import uuid
|
||||
#
|
||||
# import clique
|
||||
# import ayon_api
|
||||
# from pymongo import UpdateOne
|
||||
# import qargparse
|
||||
# from qtpy import QtWidgets, QtCore
|
||||
#
|
||||
# from ayon_core import style
|
||||
# from ayon_core.addon import AddonsManager
|
||||
# from ayon_core.lib import format_file_size
|
||||
# from ayon_core.pipeline import load, Anatomy
|
||||
# from ayon_core.pipeline.load import (
|
||||
# get_representation_path_with_anatomy,
|
||||
# InvalidRepresentationContext,
|
||||
# )
|
||||
#
|
||||
#
|
||||
# class DeleteOldVersions(load.ProductLoaderPlugin):
|
||||
# """Deletes specific number of old version"""
|
||||
#
|
||||
# is_multiple_contexts_compatible = True
|
||||
# sequence_splitter = "__sequence_splitter__"
|
||||
#
|
||||
# representations = {"*"}
|
||||
# product_types = {"*"}
|
||||
# tool_names = ["library_loader"]
|
||||
#
|
||||
# label = "Delete Old Versions"
|
||||
# order = 35
|
||||
# icon = "trash"
|
||||
# color = "#d8d8d8"
|
||||
#
|
||||
# options = [
|
||||
# qargparse.Integer(
|
||||
# "versions_to_keep", default=2, min=0, help="Versions to keep:"
|
||||
# ),
|
||||
# qargparse.Boolean(
|
||||
# "remove_publish_folder", help="Remove publish folder:"
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
# def delete_whole_dir_paths(self, dir_paths, delete=True):
|
||||
# size = 0
|
||||
#
|
||||
# for dir_path in dir_paths:
|
||||
# # Delete all files and fodlers in dir path
|
||||
# for root, dirs, files in os.walk(dir_path, topdown=False):
|
||||
# for name in files:
|
||||
# file_path = os.path.join(root, name)
|
||||
# size += os.path.getsize(file_path)
|
||||
# if delete:
|
||||
# os.remove(file_path)
|
||||
# self.log.debug("Removed file: {}".format(file_path))
|
||||
#
|
||||
# for name in dirs:
|
||||
# if delete:
|
||||
# os.rmdir(os.path.join(root, name))
|
||||
#
|
||||
# if not delete:
|
||||
# continue
|
||||
#
|
||||
# # Delete even the folder and it's parents folders if they are empty
|
||||
# while True:
|
||||
# if not os.path.exists(dir_path):
|
||||
# dir_path = os.path.dirname(dir_path)
|
||||
# continue
|
||||
#
|
||||
# if len(os.listdir(dir_path)) != 0:
|
||||
# break
|
||||
#
|
||||
# os.rmdir(os.path.join(dir_path))
|
||||
#
|
||||
# return size
|
||||
#
|
||||
# def path_from_representation(self, representation, anatomy):
|
||||
# try:
|
||||
# context = representation["context"]
|
||||
# except KeyError:
|
||||
# return (None, None)
|
||||
#
|
||||
# try:
|
||||
# path = get_representation_path_with_anatomy(
|
||||
# representation, anatomy
|
||||
# )
|
||||
# except InvalidRepresentationContext:
|
||||
# return (None, None)
|
||||
#
|
||||
# sequence_path = None
|
||||
# if "frame" in context:
|
||||
# context["frame"] = self.sequence_splitter
|
||||
# sequence_path = get_representation_path_with_anatomy(
|
||||
# representation, anatomy
|
||||
# )
|
||||
#
|
||||
# if sequence_path:
|
||||
# sequence_path = sequence_path.normalized()
|
||||
#
|
||||
# return (path.normalized(), sequence_path)
|
||||
#
|
||||
# def delete_only_repre_files(self, dir_paths, file_paths, delete=True):
|
||||
# size = 0
|
||||
#
|
||||
# for dir_id, dir_path in dir_paths.items():
|
||||
# dir_files = os.listdir(dir_path)
|
||||
# collections, remainders = clique.assemble(dir_files)
|
||||
# for file_path, seq_path in file_paths[dir_id]:
|
||||
# file_path_base = os.path.split(file_path)[1]
|
||||
# # Just remove file if `frame` key was not in context or
|
||||
# # filled path is in remainders (single file sequence)
|
||||
# if not seq_path or file_path_base in remainders:
|
||||
# if not os.path.exists(file_path):
|
||||
# self.log.debug(
|
||||
# "File was not found: {}".format(file_path)
|
||||
# )
|
||||
# continue
|
||||
#
|
||||
# size += os.path.getsize(file_path)
|
||||
#
|
||||
# if delete:
|
||||
# os.remove(file_path)
|
||||
# self.log.debug("Removed file: {}".format(file_path))
|
||||
#
|
||||
# if file_path_base in remainders:
|
||||
# remainders.remove(file_path_base)
|
||||
# continue
|
||||
#
|
||||
# seq_path_base = os.path.split(seq_path)[1]
|
||||
# head, tail = seq_path_base.split(self.sequence_splitter)
|
||||
#
|
||||
# final_col = None
|
||||
# for collection in collections:
|
||||
# if head != collection.head or tail != collection.tail:
|
||||
# continue
|
||||
# final_col = collection
|
||||
# break
|
||||
#
|
||||
# if final_col is not None:
|
||||
# # Fill full path to head
|
||||
# final_col.head = os.path.join(dir_path, final_col.head)
|
||||
# for _file_path in final_col:
|
||||
# if os.path.exists(_file_path):
|
||||
#
|
||||
# size += os.path.getsize(_file_path)
|
||||
#
|
||||
# if delete:
|
||||
# os.remove(_file_path)
|
||||
# self.log.debug(
|
||||
# "Removed file: {}".format(_file_path)
|
||||
# )
|
||||
#
|
||||
# _seq_path = final_col.format("{head}{padding}{tail}")
|
||||
# self.log.debug("Removed files: {}".format(_seq_path))
|
||||
# collections.remove(final_col)
|
||||
#
|
||||
# elif os.path.exists(file_path):
|
||||
# size += os.path.getsize(file_path)
|
||||
#
|
||||
# if delete:
|
||||
# os.remove(file_path)
|
||||
# self.log.debug("Removed file: {}".format(file_path))
|
||||
# else:
|
||||
# self.log.debug(
|
||||
# "File was not found: {}".format(file_path)
|
||||
# )
|
||||
#
|
||||
# # Delete as much as possible parent folders
|
||||
# if not delete:
|
||||
# return size
|
||||
#
|
||||
# for dir_path in dir_paths.values():
|
||||
# while True:
|
||||
# if not os.path.exists(dir_path):
|
||||
# dir_path = os.path.dirname(dir_path)
|
||||
# continue
|
||||
#
|
||||
# if len(os.listdir(dir_path)) != 0:
|
||||
# break
|
||||
#
|
||||
# self.log.debug("Removed folder: {}".format(dir_path))
|
||||
# os.rmdir(dir_path)
|
||||
#
|
||||
# return size
|
||||
#
|
||||
# def message(self, text):
|
||||
# msgBox = QtWidgets.QMessageBox()
|
||||
# msgBox.setText(text)
|
||||
# msgBox.setStyleSheet(style.load_stylesheet())
|
||||
# msgBox.setWindowFlags(
|
||||
# msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint
|
||||
# )
|
||||
# msgBox.exec_()
|
||||
#
|
||||
# def get_data(self, context, versions_count):
|
||||
# product_entity = context["product"]
|
||||
# folder_entity = context["folder"]
|
||||
# project_name = context["project"]["name"]
|
||||
# anatomy = Anatomy(project_name)
|
||||
#
|
||||
# versions = list(ayon_api.get_versions(
|
||||
# project_name, product_ids=[product_entity["id"]]
|
||||
# ))
|
||||
#
|
||||
# versions_by_parent = collections.defaultdict(list)
|
||||
# for ent in versions:
|
||||
# versions_by_parent[ent["productId"]].append(ent)
|
||||
#
|
||||
# def sort_func(ent):
|
||||
# return int(ent["version"])
|
||||
#
|
||||
# all_last_versions = []
|
||||
# for _parent_id, _versions in versions_by_parent.items():
|
||||
# for idx, version in enumerate(
|
||||
# sorted(_versions, key=sort_func, reverse=True)
|
||||
# ):
|
||||
# if idx >= versions_count:
|
||||
# break
|
||||
# all_last_versions.append(version)
|
||||
#
|
||||
# self.log.debug("Collected versions ({})".format(len(versions)))
|
||||
#
|
||||
# # Filter latest versions
|
||||
# for version in all_last_versions:
|
||||
# versions.remove(version)
|
||||
#
|
||||
# # Update versions_by_parent without filtered versions
|
||||
# versions_by_parent = collections.defaultdict(list)
|
||||
# for ent in versions:
|
||||
# versions_by_parent[ent["productId"]].append(ent)
|
||||
#
|
||||
# # Filter already deleted versions
|
||||
# versions_to_pop = []
|
||||
# for version in versions:
|
||||
# version_tags = version["data"].get("tags")
|
||||
# if version_tags and "deleted" in version_tags:
|
||||
# versions_to_pop.append(version)
|
||||
#
|
||||
# for version in versions_to_pop:
|
||||
# msg = "Folder: \"{}\" | Product: \"{}\" | Version: \"{}\"".format(
|
||||
# folder_entity["path"],
|
||||
# product_entity["name"],
|
||||
# version["version"]
|
||||
# )
|
||||
# self.log.debug((
|
||||
# "Skipping version. Already tagged as `deleted`. < {} >"
|
||||
# ).format(msg))
|
||||
# versions.remove(version)
|
||||
#
|
||||
# version_ids = [ent["id"] for ent in versions]
|
||||
#
|
||||
# self.log.debug(
|
||||
# "Filtered versions to delete ({})".format(len(version_ids))
|
||||
# )
|
||||
#
|
||||
# if not version_ids:
|
||||
# msg = "Skipping processing. Nothing to delete on {}/{}".format(
|
||||
# folder_entity["path"], product_entity["name"]
|
||||
# )
|
||||
# self.log.info(msg)
|
||||
# print(msg)
|
||||
# return
|
||||
#
|
||||
# repres = list(ayon_api.get_representations(
|
||||
# project_name, version_ids=version_ids
|
||||
# ))
|
||||
#
|
||||
# self.log.debug(
|
||||
# "Collected representations to remove ({})".format(len(repres))
|
||||
# )
|
||||
#
|
||||
# dir_paths = {}
|
||||
# file_paths_by_dir = collections.defaultdict(list)
|
||||
# for repre in repres:
|
||||
# file_path, seq_path = self.path_from_representation(
|
||||
# repre, anatomy
|
||||
# )
|
||||
# if file_path is None:
|
||||
# self.log.debug((
|
||||
# "Could not format path for represenation \"{}\""
|
||||
# ).format(str(repre)))
|
||||
# continue
|
||||
#
|
||||
# dir_path = os.path.dirname(file_path)
|
||||
# dir_id = None
|
||||
# for _dir_id, _dir_path in dir_paths.items():
|
||||
# if _dir_path == dir_path:
|
||||
# dir_id = _dir_id
|
||||
# break
|
||||
#
|
||||
# if dir_id is None:
|
||||
# dir_id = uuid.uuid4()
|
||||
# dir_paths[dir_id] = dir_path
|
||||
#
|
||||
# file_paths_by_dir[dir_id].append([file_path, seq_path])
|
||||
#
|
||||
# dir_ids_to_pop = []
|
||||
# for dir_id, dir_path in dir_paths.items():
|
||||
# if os.path.exists(dir_path):
|
||||
# continue
|
||||
#
|
||||
# dir_ids_to_pop.append(dir_id)
|
||||
#
|
||||
# # Pop dirs from both dictionaries
|
||||
# for dir_id in dir_ids_to_pop:
|
||||
# dir_paths.pop(dir_id)
|
||||
# paths = file_paths_by_dir.pop(dir_id)
|
||||
# # TODO report of missing directories?
|
||||
# paths_msg = ", ".join([
|
||||
# "'{}'".format(path[0].replace("\\", "/")) for path in paths
|
||||
# ])
|
||||
# self.log.debug((
|
||||
# "Folder does not exist. Deleting it's files skipped: {}"
|
||||
# ).format(paths_msg))
|
||||
#
|
||||
# return {
|
||||
# "dir_paths": dir_paths,
|
||||
# "file_paths_by_dir": file_paths_by_dir,
|
||||
# "versions": versions,
|
||||
# "folder": folder_entity,
|
||||
# "product": product_entity,
|
||||
# "archive_product": versions_count == 0
|
||||
# }
|
||||
#
|
||||
# def main(self, project_name, data, remove_publish_folder):
|
||||
# # Size of files.
|
||||
# size = 0
|
||||
# if not data:
|
||||
# return size
|
||||
#
|
||||
# if remove_publish_folder:
|
||||
# size = self.delete_whole_dir_paths(data["dir_paths"].values())
|
||||
# else:
|
||||
# size = self.delete_only_repre_files(
|
||||
# data["dir_paths"], data["file_paths_by_dir"]
|
||||
# )
|
||||
#
|
||||
# mongo_changes_bulk = []
|
||||
# for version in data["versions"]:
|
||||
# orig_version_tags = version["data"].get("tags") or []
|
||||
# version_tags = [tag for tag in orig_version_tags]
|
||||
# if "deleted" not in version_tags:
|
||||
# version_tags.append("deleted")
|
||||
#
|
||||
# if version_tags == orig_version_tags:
|
||||
# continue
|
||||
#
|
||||
# update_query = {"id": version["id"]}
|
||||
# update_data = {"$set": {"data.tags": version_tags}}
|
||||
# mongo_changes_bulk.append(UpdateOne(update_query, update_data))
|
||||
#
|
||||
# if data["archive_product"]:
|
||||
# mongo_changes_bulk.append(UpdateOne(
|
||||
# {
|
||||
# "id": data["product"]["id"],
|
||||
# "type": "subset"
|
||||
# },
|
||||
# {"$set": {"type": "archived_subset"}}
|
||||
# ))
|
||||
#
|
||||
# if mongo_changes_bulk:
|
||||
# dbcon = AvalonMongoDB()
|
||||
# dbcon.Session["AYON_PROJECT_NAME"] = project_name
|
||||
# dbcon.install()
|
||||
# dbcon.bulk_write(mongo_changes_bulk)
|
||||
# dbcon.uninstall()
|
||||
#
|
||||
# self._ftrack_delete_versions(data)
|
||||
#
|
||||
# return size
|
||||
#
|
||||
# def _ftrack_delete_versions(self, data):
|
||||
# """Delete version on ftrack.
|
||||
#
|
||||
# Handling of ftrack logic in this plugin is not ideal. But in OP3 it is
|
||||
# almost impossible to solve the issue other way.
|
||||
#
|
||||
# Note:
|
||||
# Asset versions on ftrack are not deleted but marked as
|
||||
# "not published" which cause that they're invisible.
|
||||
#
|
||||
# Args:
|
||||
# data (dict): Data sent to product loader with full context.
|
||||
# """
|
||||
#
|
||||
# # First check for ftrack id on folder entity
|
||||
# # - skip if ther is none
|
||||
# ftrack_id = data["folder"]["attrib"].get("ftrackId")
|
||||
# if not ftrack_id:
|
||||
# self.log.info((
|
||||
# "Folder does not have filled ftrack id. Skipped delete"
|
||||
# " of ftrack version."
|
||||
# ))
|
||||
# return
|
||||
#
|
||||
# # Check if ftrack module is enabled
|
||||
# addons_manager = AddonsManager()
|
||||
# ftrack_addon = addons_manager.get("ftrack")
|
||||
# if not ftrack_addon or not ftrack_addon.enabled:
|
||||
# return
|
||||
#
|
||||
# import ftrack_api
|
||||
#
|
||||
# session = ftrack_api.Session()
|
||||
# product_name = data["product"]["name"]
|
||||
# versions = {
|
||||
# '"{}"'.format(version_doc["name"])
|
||||
# for version_doc in data["versions"]
|
||||
# }
|
||||
# asset_versions = session.query(
|
||||
# (
|
||||
# "select id, is_published from AssetVersion where"
|
||||
# " asset.parent.id is \"{}\""
|
||||
# " and asset.name is \"{}\""
|
||||
# " and version in ({})"
|
||||
# ).format(
|
||||
# ftrack_id,
|
||||
# product_name,
|
||||
# ",".join(versions)
|
||||
# )
|
||||
# ).all()
|
||||
#
|
||||
# # Set attribute `is_published` to `False` on ftrack AssetVersions
|
||||
# for asset_version in asset_versions:
|
||||
# asset_version["is_published"] = False
|
||||
#
|
||||
# try:
|
||||
# session.commit()
|
||||
#
|
||||
# except Exception:
|
||||
# msg = (
|
||||
# "Could not set `is_published` attribute to `False`"
|
||||
# " for selected AssetVersions."
|
||||
# )
|
||||
# self.log.error(msg)
|
||||
# self.message(msg)
|
||||
#
|
||||
# def load(self, contexts, name=None, namespace=None, options=None):
|
||||
# try:
|
||||
# size = 0
|
||||
# for count, context in enumerate(contexts):
|
||||
# versions_to_keep = 2
|
||||
# remove_publish_folder = False
|
||||
# if options:
|
||||
# versions_to_keep = options.get(
|
||||
# "versions_to_keep", versions_to_keep
|
||||
# )
|
||||
# remove_publish_folder = options.get(
|
||||
# "remove_publish_folder", remove_publish_folder
|
||||
# )
|
||||
#
|
||||
# data = self.get_data(context, versions_to_keep)
|
||||
# if not data:
|
||||
# continue
|
||||
#
|
||||
# project_name = context["project"]["name"]
|
||||
# size += self.main(project_name, data, remove_publish_folder)
|
||||
# print("Progressing {}/{}".format(count + 1, len(contexts)))
|
||||
#
|
||||
# msg = "Total size of files: {}".format(format_file_size(size))
|
||||
# self.log.info(msg)
|
||||
# self.message(msg)
|
||||
#
|
||||
# except Exception:
|
||||
# self.log.error("Failed to delete versions.", exc_info=True)
|
||||
#
|
||||
#
|
||||
# class CalculateOldVersions(DeleteOldVersions):
|
||||
# """Calculate file size of old versions"""
|
||||
# label = "Calculate Old Versions"
|
||||
# order = 30
|
||||
# tool_names = ["library_loader"]
|
||||
#
|
||||
# options = [
|
||||
# qargparse.Integer(
|
||||
# "versions_to_keep", default=2, min=0, help="Versions to keep:"
|
||||
# ),
|
||||
# qargparse.Boolean(
|
||||
# "remove_publish_folder", help="Remove publish folder:"
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
# def main(self, project_name, data, remove_publish_folder):
|
||||
# size = 0
|
||||
#
|
||||
# if not data:
|
||||
# return size
|
||||
#
|
||||
# if remove_publish_folder:
|
||||
# size = self.delete_whole_dir_paths(
|
||||
# data["dir_paths"].values(), delete=False
|
||||
# )
|
||||
# else:
|
||||
# size = self.delete_only_repre_files(
|
||||
# data["dir_paths"], data["file_paths_by_dir"], delete=False
|
||||
# )
|
||||
#
|
||||
# return size
|
||||
import collections
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import clique
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
import qargparse
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core import style
|
||||
from ayon_core.lib import format_file_size
|
||||
from ayon_core.pipeline import load, Anatomy
|
||||
from ayon_core.pipeline.load import (
|
||||
get_representation_path_with_anatomy,
|
||||
InvalidRepresentationContext,
|
||||
)
|
||||
|
||||
|
||||
class DeleteOldVersions(load.ProductLoaderPlugin):
|
||||
"""Deletes specific number of old version"""
|
||||
|
||||
is_multiple_contexts_compatible = True
|
||||
sequence_splitter = "__sequence_splitter__"
|
||||
|
||||
representations = ["*"]
|
||||
product_types = {"*"}
|
||||
tool_names = ["library_loader"]
|
||||
|
||||
label = "Delete Old Versions"
|
||||
order = 35
|
||||
icon = "trash"
|
||||
color = "#d8d8d8"
|
||||
|
||||
options = [
|
||||
qargparse.Integer(
|
||||
"versions_to_keep", default=2, min=0, help="Versions to keep:"
|
||||
),
|
||||
qargparse.Boolean(
|
||||
"remove_publish_folder", help="Remove publish folder:"
|
||||
)
|
||||
]
|
||||
|
||||
def delete_whole_dir_paths(self, dir_paths, delete=True):
|
||||
size = 0
|
||||
|
||||
for dir_path in dir_paths:
|
||||
# Delete all files and fodlers in dir path
|
||||
for root, dirs, files in os.walk(dir_path, topdown=False):
|
||||
for name in files:
|
||||
file_path = os.path.join(root, name)
|
||||
size += os.path.getsize(file_path)
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
|
||||
for name in dirs:
|
||||
if delete:
|
||||
os.rmdir(os.path.join(root, name))
|
||||
|
||||
if not delete:
|
||||
continue
|
||||
|
||||
# Delete even the folder and it's parents folders if they are empty
|
||||
while True:
|
||||
if not os.path.exists(dir_path):
|
||||
dir_path = os.path.dirname(dir_path)
|
||||
continue
|
||||
|
||||
if len(os.listdir(dir_path)) != 0:
|
||||
break
|
||||
|
||||
os.rmdir(os.path.join(dir_path))
|
||||
|
||||
return size
|
||||
|
||||
def path_from_representation(self, representation, anatomy):
|
||||
try:
|
||||
context = representation["context"]
|
||||
except KeyError:
|
||||
return (None, None)
|
||||
|
||||
try:
|
||||
path = get_representation_path_with_anatomy(
|
||||
representation, anatomy
|
||||
)
|
||||
except InvalidRepresentationContext:
|
||||
return (None, None)
|
||||
|
||||
sequence_path = None
|
||||
if "frame" in context:
|
||||
context["frame"] = self.sequence_splitter
|
||||
sequence_path = get_representation_path_with_anatomy(
|
||||
representation, anatomy
|
||||
)
|
||||
|
||||
if sequence_path:
|
||||
sequence_path = sequence_path.normalized()
|
||||
|
||||
return (path.normalized(), sequence_path)
|
||||
|
||||
def delete_only_repre_files(self, dir_paths, file_paths, delete=True):
|
||||
size = 0
|
||||
|
||||
for dir_id, dir_path in dir_paths.items():
|
||||
dir_files = os.listdir(dir_path)
|
||||
collections, remainders = clique.assemble(dir_files)
|
||||
for file_path, seq_path in file_paths[dir_id]:
|
||||
file_path_base = os.path.split(file_path)[1]
|
||||
# Just remove file if `frame` key was not in context or
|
||||
# filled path is in remainders (single file sequence)
|
||||
if not seq_path or file_path_base in remainders:
|
||||
if not os.path.exists(file_path):
|
||||
self.log.debug(
|
||||
"File was not found: {}".format(file_path)
|
||||
)
|
||||
continue
|
||||
|
||||
size += os.path.getsize(file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
|
||||
if file_path_base in remainders:
|
||||
remainders.remove(file_path_base)
|
||||
continue
|
||||
|
||||
seq_path_base = os.path.split(seq_path)[1]
|
||||
head, tail = seq_path_base.split(self.sequence_splitter)
|
||||
|
||||
final_col = None
|
||||
for collection in collections:
|
||||
if head != collection.head or tail != collection.tail:
|
||||
continue
|
||||
final_col = collection
|
||||
break
|
||||
|
||||
if final_col is not None:
|
||||
# Fill full path to head
|
||||
final_col.head = os.path.join(dir_path, final_col.head)
|
||||
for _file_path in final_col:
|
||||
if os.path.exists(_file_path):
|
||||
|
||||
size += os.path.getsize(_file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(_file_path)
|
||||
self.log.debug(
|
||||
"Removed file: {}".format(_file_path)
|
||||
)
|
||||
|
||||
_seq_path = final_col.format("{head}{padding}{tail}")
|
||||
self.log.debug("Removed files: {}".format(_seq_path))
|
||||
collections.remove(final_col)
|
||||
|
||||
elif os.path.exists(file_path):
|
||||
size += os.path.getsize(file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
else:
|
||||
self.log.debug(
|
||||
"File was not found: {}".format(file_path)
|
||||
)
|
||||
|
||||
# Delete as much as possible parent folders
|
||||
if not delete:
|
||||
return size
|
||||
|
||||
for dir_path in dir_paths.values():
|
||||
while True:
|
||||
if not os.path.exists(dir_path):
|
||||
dir_path = os.path.dirname(dir_path)
|
||||
continue
|
||||
|
||||
if len(os.listdir(dir_path)) != 0:
|
||||
break
|
||||
|
||||
self.log.debug("Removed folder: {}".format(dir_path))
|
||||
os.rmdir(dir_path)
|
||||
|
||||
return size
|
||||
|
||||
def message(self, text):
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setText(text)
|
||||
msgBox.setStyleSheet(style.load_stylesheet())
|
||||
msgBox.setWindowFlags(
|
||||
msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint
|
||||
)
|
||||
msgBox.exec_()
|
||||
|
||||
def get_data(self, context, versions_count):
|
||||
product_entity = context["product"]
|
||||
folder_entity = context["folder"]
|
||||
project_name = context["project"]["name"]
|
||||
anatomy = Anatomy(project_name, project_entity=context["project"])
|
||||
|
||||
version_fields = ayon_api.get_default_fields_for_type("version")
|
||||
version_fields.add("tags")
|
||||
versions = list(ayon_api.get_versions(
|
||||
project_name,
|
||||
product_ids=[product_entity["id"]],
|
||||
active=None,
|
||||
hero=False,
|
||||
fields=version_fields
|
||||
))
|
||||
self.log.debug(
|
||||
"Version Number ({})".format(len(versions))
|
||||
)
|
||||
versions_by_parent = collections.defaultdict(list)
|
||||
for ent in versions:
|
||||
versions_by_parent[ent["productId"]].append(ent)
|
||||
|
||||
def sort_func(ent):
|
||||
return int(ent["version"])
|
||||
|
||||
all_last_versions = []
|
||||
for _parent_id, _versions in versions_by_parent.items():
|
||||
for idx, version in enumerate(
|
||||
sorted(_versions, key=sort_func, reverse=True)
|
||||
):
|
||||
if idx >= versions_count:
|
||||
break
|
||||
all_last_versions.append(version)
|
||||
|
||||
self.log.debug("Collected versions ({})".format(len(versions)))
|
||||
|
||||
# Filter latest versions
|
||||
for version in all_last_versions:
|
||||
versions.remove(version)
|
||||
|
||||
# Update versions_by_parent without filtered versions
|
||||
versions_by_parent = collections.defaultdict(list)
|
||||
for ent in versions:
|
||||
versions_by_parent[ent["productId"]].append(ent)
|
||||
|
||||
# Filter already deleted versions
|
||||
versions_to_pop = []
|
||||
for version in versions:
|
||||
if "deleted" in version["tags"]:
|
||||
versions_to_pop.append(version)
|
||||
|
||||
for version in versions_to_pop:
|
||||
msg = "Folder: \"{}\" | Product: \"{}\" | Version: \"{}\"".format(
|
||||
folder_entity["path"],
|
||||
product_entity["name"],
|
||||
version["version"]
|
||||
)
|
||||
self.log.debug((
|
||||
"Skipping version. Already tagged as inactive. < {} >"
|
||||
).format(msg))
|
||||
versions.remove(version)
|
||||
|
||||
version_ids = [ent["id"] for ent in versions]
|
||||
|
||||
self.log.debug(
|
||||
"Filtered versions to delete ({})".format(len(version_ids))
|
||||
)
|
||||
|
||||
if not version_ids:
|
||||
msg = "Skipping processing. Nothing to delete on {}/{}".format(
|
||||
folder_entity["path"], product_entity["name"]
|
||||
)
|
||||
self.log.info(msg)
|
||||
print(msg)
|
||||
return
|
||||
|
||||
repres = list(ayon_api.get_representations(
|
||||
project_name, version_ids=version_ids
|
||||
))
|
||||
|
||||
self.log.debug(
|
||||
"Collected representations to remove ({})".format(len(repres))
|
||||
)
|
||||
|
||||
dir_paths = {}
|
||||
file_paths_by_dir = collections.defaultdict(list)
|
||||
for repre in repres:
|
||||
file_path, seq_path = self.path_from_representation(
|
||||
repre, anatomy
|
||||
)
|
||||
if file_path is None:
|
||||
self.log.debug((
|
||||
"Could not format path for represenation \"{}\""
|
||||
).format(str(repre)))
|
||||
continue
|
||||
|
||||
dir_path = os.path.dirname(file_path)
|
||||
dir_id = None
|
||||
for _dir_id, _dir_path in dir_paths.items():
|
||||
if _dir_path == dir_path:
|
||||
dir_id = _dir_id
|
||||
break
|
||||
|
||||
if dir_id is None:
|
||||
dir_id = uuid.uuid4()
|
||||
dir_paths[dir_id] = dir_path
|
||||
|
||||
file_paths_by_dir[dir_id].append([file_path, seq_path])
|
||||
|
||||
dir_ids_to_pop = []
|
||||
for dir_id, dir_path in dir_paths.items():
|
||||
if os.path.exists(dir_path):
|
||||
continue
|
||||
|
||||
dir_ids_to_pop.append(dir_id)
|
||||
|
||||
# Pop dirs from both dictionaries
|
||||
for dir_id in dir_ids_to_pop:
|
||||
dir_paths.pop(dir_id)
|
||||
paths = file_paths_by_dir.pop(dir_id)
|
||||
# TODO report of missing directories?
|
||||
paths_msg = ", ".join([
|
||||
"'{}'".format(path[0].replace("\\", "/")) for path in paths
|
||||
])
|
||||
self.log.debug((
|
||||
"Folder does not exist. Deleting its files skipped: {}"
|
||||
).format(paths_msg))
|
||||
|
||||
return {
|
||||
"dir_paths": dir_paths,
|
||||
"file_paths_by_dir": file_paths_by_dir,
|
||||
"versions": versions,
|
||||
"folder": folder_entity,
|
||||
"product": product_entity,
|
||||
"archive_product": versions_count == 0
|
||||
}
|
||||
|
||||
def main(self, project_name, data, remove_publish_folder):
|
||||
# Size of files.
|
||||
size = 0
|
||||
if not data:
|
||||
return size
|
||||
|
||||
if remove_publish_folder:
|
||||
size = self.delete_whole_dir_paths(data["dir_paths"].values())
|
||||
else:
|
||||
size = self.delete_only_repre_files(
|
||||
data["dir_paths"], data["file_paths_by_dir"]
|
||||
)
|
||||
|
||||
op_session = OperationsSession()
|
||||
for version in data["versions"]:
|
||||
orig_version_tags = version["tags"]
|
||||
version_tags = list(orig_version_tags)
|
||||
changes = {}
|
||||
if "deleted" not in version_tags:
|
||||
version_tags.append("deleted")
|
||||
changes["tags"] = version_tags
|
||||
|
||||
if version["active"]:
|
||||
changes["active"] = False
|
||||
|
||||
if not changes:
|
||||
continue
|
||||
op_session.update_entity(
|
||||
project_name, "version", version["id"], changes
|
||||
)
|
||||
|
||||
op_session.commit()
|
||||
|
||||
return size
|
||||
|
||||
def load(self, contexts, name=None, namespace=None, options=None):
|
||||
try:
|
||||
size = 0
|
||||
for count, context in enumerate(contexts):
|
||||
versions_to_keep = 2
|
||||
remove_publish_folder = False
|
||||
if options:
|
||||
versions_to_keep = options.get(
|
||||
"versions_to_keep", versions_to_keep
|
||||
)
|
||||
remove_publish_folder = options.get(
|
||||
"remove_publish_folder", remove_publish_folder
|
||||
)
|
||||
|
||||
data = self.get_data(context, versions_to_keep)
|
||||
if not data:
|
||||
continue
|
||||
project_name = context["project"]["name"]
|
||||
size += self.main(project_name, data, remove_publish_folder)
|
||||
print("Progressing {}/{}".format(count + 1, len(contexts)))
|
||||
|
||||
msg = "Total size of files: {}".format(format_file_size(size))
|
||||
self.log.info(msg)
|
||||
self.message(msg)
|
||||
|
||||
except Exception:
|
||||
self.log.error("Failed to delete versions.", exc_info=True)
|
||||
|
||||
|
||||
class CalculateOldVersions(DeleteOldVersions):
|
||||
"""Calculate file size of old versions"""
|
||||
label = "Calculate Old Versions"
|
||||
order = 30
|
||||
tool_names = ["library_loader"]
|
||||
|
||||
options = [
|
||||
qargparse.Integer(
|
||||
"versions_to_keep", default=2, min=0, help="Versions to keep:"
|
||||
),
|
||||
qargparse.Boolean(
|
||||
"remove_publish_folder", help="Remove publish folder:"
|
||||
)
|
||||
]
|
||||
|
||||
def main(self, project_name, data, remove_publish_folder):
|
||||
size = 0
|
||||
|
||||
if not data:
|
||||
return size
|
||||
|
||||
if remove_publish_folder:
|
||||
size = self.delete_whole_dir_paths(
|
||||
data["dir_paths"].values(), delete=False
|
||||
)
|
||||
else:
|
||||
size = self.delete_only_repre_files(
|
||||
data["dir_paths"], data["file_paths_by_dir"], delete=False
|
||||
)
|
||||
|
||||
return size
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ from ayon_core.tools.utils.dialogs import show_message_dialog
|
|||
def open_template_ui(builder, main_window):
|
||||
"""Open template from `builder`
|
||||
|
||||
Asks user about overwriting current scene and feedsback exceptions.
|
||||
Asks user about overwriting current scene and feedback exceptions.
|
||||
"""
|
||||
result = QtWidgets.QMessageBox.question(
|
||||
main_window,
|
||||
"Opening template",
|
||||
"Caution! You will loose unsaved changes.\nDo you want to continue?",
|
||||
"Caution! You will lose unsaved changes.\nDo you want to continue?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
|
||||
)
|
||||
if result == QtWidgets.QMessageBox.Yes:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(WorkAreaFilesModel, self).__init__()
|
||||
|
||||
|
|
@ -163,6 +165,12 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
self._fill_items()
|
||||
|
||||
def _fill_items(self):
|
||||
try:
|
||||
self._fill_items_impl()
|
||||
finally:
|
||||
self.refreshed.emit()
|
||||
|
||||
def _fill_items_impl(self):
|
||||
folder_id = self._selected_folder_id
|
||||
task_id = self._selected_task_id
|
||||
if not folder_id or not task_id:
|
||||
|
|
@ -285,6 +293,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
view.double_clicked.connect(self._on_mouse_double_click)
|
||||
view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
|
|
@ -298,6 +307,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
self._controller = controller
|
||||
|
||||
self._published_mode = False
|
||||
self._change_selection_on_refresh = True
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
"""Set the published mode.
|
||||
|
|
@ -379,7 +389,9 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
if not workfile_info["current"]:
|
||||
return
|
||||
|
||||
self._change_selection_on_refresh = False
|
||||
self._model.refresh()
|
||||
self._change_selection_on_refresh = True
|
||||
|
||||
workfile_name = workfile_info["name"]
|
||||
if (
|
||||
|
|
@ -394,3 +406,30 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
self._controller.expected_workfile_selected(
|
||||
event["folder"]["id"], event["task"]["name"], workfile_name
|
||||
)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
if (
|
||||
not self._change_selection_on_refresh
|
||||
or self._proxy_model.rowCount() < 1
|
||||
):
|
||||
return
|
||||
|
||||
# Find the row with latest date modified
|
||||
latest_index = max(
|
||||
(
|
||||
self._proxy_model.index(idx, 0)
|
||||
for idx in range(self._proxy_model.rowCount())
|
||||
),
|
||||
key=lambda model_index: model_index.data(DATE_MODIFIED_ROLE)
|
||||
)
|
||||
|
||||
# Select row of latest modified
|
||||
selection_model = self._view.selectionModel()
|
||||
selection_model.select(
|
||||
latest_index,
|
||||
(
|
||||
QtCore.QItemSelectionModel.ClearAndSelect
|
||||
| QtCore.QItemSelectionModel.Current
|
||||
| QtCore.QItemSelectionModel.Rows
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -118,11 +118,11 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
overlay_invalid_host = InvalidHostOverlay(self)
|
||||
overlay_invalid_host.setVisible(False)
|
||||
|
||||
first_show_timer = QtCore.QTimer()
|
||||
first_show_timer.setSingleShot(True)
|
||||
first_show_timer.setInterval(50)
|
||||
show_timer = QtCore.QTimer()
|
||||
show_timer.setSingleShot(True)
|
||||
show_timer.setInterval(50)
|
||||
|
||||
first_show_timer.timeout.connect(self._on_first_show)
|
||||
show_timer.timeout.connect(self._on_show)
|
||||
|
||||
controller.register_event_callback(
|
||||
"save_as.finished",
|
||||
|
|
@ -159,7 +159,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
self._tasks_widget = tasks_widget
|
||||
self._side_panel = side_panel
|
||||
|
||||
self._first_show_timer = first_show_timer
|
||||
self._show_timer = show_timer
|
||||
|
||||
self._post_init()
|
||||
|
||||
|
|
@ -287,9 +287,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
|
||||
def showEvent(self, event):
|
||||
super(WorkfilesToolWindow, self).showEvent(event)
|
||||
self._show_timer.start()
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._first_show_timer.start()
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
|
|
@ -303,9 +303,8 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
|
||||
pass
|
||||
|
||||
def _on_first_show(self):
|
||||
if not self._controller_refreshed:
|
||||
self.refresh()
|
||||
def _on_show(self):
|
||||
self.refresh()
|
||||
|
||||
def _on_file_text_filter_change(self, text):
|
||||
self._files_widget.set_text_filter(text)
|
||||
|
|
|
|||
3
server_addon/aftereffects/package.py
Normal file
3
server_addon/aftereffects/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "aftereffects"
|
||||
title = "AfterEffects"
|
||||
version = "0.1.3"
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import AfterEffectsSettings, DEFAULT_AFTEREFFECTS_SETTING
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class AfterEffects(BaseServerAddon):
|
||||
name = "aftereffects"
|
||||
title = "AfterEffects"
|
||||
version = __version__
|
||||
|
||||
settings_model = AfterEffectsSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.3"
|
||||
3
server_addon/blender/package.py
Normal file
3
server_addon/blender/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "blender"
|
||||
title = "Blender"
|
||||
version = "0.1.8"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import BlenderSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class BlenderAddon(BaseServerAddon):
|
||||
name = "blender"
|
||||
title = "Blender"
|
||||
version = __version__
|
||||
settings_model: Type[BlenderSettings] = BlenderSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.8"
|
||||
3
server_addon/celaction/package.py
Normal file
3
server_addon/celaction/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "celaction"
|
||||
title = "CelAction"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import CelActionSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class CelActionAddon(BaseServerAddon):
|
||||
name = "celaction"
|
||||
title = "CelAction"
|
||||
version = __version__
|
||||
settings_model: Type[CelActionSettings] = CelActionSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
||||
3
server_addon/clockify/package.py
Normal file
3
server_addon/clockify/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "clockify"
|
||||
title = "Clockify"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2,14 +2,8 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import ClockifySettings
|
||||
|
||||
|
||||
class ClockifyAddon(BaseServerAddon):
|
||||
name = "clockify"
|
||||
title = "Clockify"
|
||||
version = __version__
|
||||
settings_model: Type[ClockifySettings] = ClockifySettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
|
|
@ -245,12 +245,8 @@ def create_addon_package(
|
|||
keep_source: bool,
|
||||
):
|
||||
src_package_py = addon_dir / "package.py"
|
||||
package = None
|
||||
if src_package_py.exists():
|
||||
package = import_filepath(src_package_py)
|
||||
addon_version = package.version
|
||||
else:
|
||||
addon_version = get_addon_version(addon_dir)
|
||||
package = import_filepath(src_package_py)
|
||||
addon_version = package.version
|
||||
|
||||
addon_output_dir = output_dir / addon_dir.name / addon_version
|
||||
if addon_output_dir.exists():
|
||||
|
|
@ -259,18 +255,7 @@ def create_addon_package(
|
|||
|
||||
# Copy server content
|
||||
dst_package_py = addon_output_dir / "package.py"
|
||||
if package is not None:
|
||||
shutil.copy(src_package_py, dst_package_py)
|
||||
else:
|
||||
addon_name = addon_dir.name
|
||||
if addon_name == "royal_render":
|
||||
addon_name = "royalrender"
|
||||
package_py_content = PACKAGE_PY_TEMPLATE.format(
|
||||
addon_name=addon_name, addon_version=addon_version
|
||||
)
|
||||
|
||||
with open(dst_package_py, "w+") as pkg_py:
|
||||
pkg_py.write(package_py_content)
|
||||
shutil.copy(src_package_py, dst_package_py)
|
||||
|
||||
server_dir = addon_dir / "server"
|
||||
shutil.copytree(
|
||||
|
|
|
|||
3
server_addon/deadline/package.py
Normal file
3
server_addon/deadline/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "deadline"
|
||||
title = "Deadline"
|
||||
version = "0.1.10"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import DeadlineSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class Deadline(BaseServerAddon):
|
||||
name = "deadline"
|
||||
title = "Deadline"
|
||||
version = __version__
|
||||
settings_model: Type[DeadlineSettings] = DeadlineSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class ServerListSubmodel(BaseSettingsModel):
|
|||
|
||||
|
||||
async def defined_deadline_ws_name_enum_resolver(
|
||||
addon: BaseServerAddon,
|
||||
addon: "BaseServerAddon",
|
||||
settings_variant: str = "production",
|
||||
project_name: str | None = None,
|
||||
) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.10"
|
||||
3
server_addon/flame/package.py
Normal file
3
server_addon/flame/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "flame"
|
||||
title = "Flame"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import FlameSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class FlameAddon(BaseServerAddon):
|
||||
name = "flame"
|
||||
title = "Flame"
|
||||
version = __version__
|
||||
settings_model: Type[FlameSettings] = FlameSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
||||
3
server_addon/fusion/package.py
Normal file
3
server_addon/fusion/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "fusion"
|
||||
title = "Fusion"
|
||||
version = "0.1.5"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import FusionSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class FusionAddon(BaseServerAddon):
|
||||
name = "fusion"
|
||||
title = "Fusion"
|
||||
version = __version__
|
||||
settings_model: Type[FusionSettings] = FusionSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.5"
|
||||
3
server_addon/harmony/package.py
Normal file
3
server_addon/harmony/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "harmony"
|
||||
title = "Harmony"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import HarmonySettings, DEFAULT_HARMONY_SETTING
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class Harmony(BaseServerAddon):
|
||||
name = "harmony"
|
||||
title = "Harmony"
|
||||
version = __version__
|
||||
|
||||
settings_model = HarmonySettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.2"
|
||||
3
server_addon/hiero/package.py
Normal file
3
server_addon/hiero/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "hiero"
|
||||
title = "Hiero"
|
||||
version = "0.1.2"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import HieroSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class HieroAddon(BaseServerAddon):
|
||||
name = "hiero"
|
||||
title = "Hiero"
|
||||
version = __version__
|
||||
settings_model: Type[HieroSettings] = HieroSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.2"
|
||||
3
server_addon/houdini/package.py
Normal file
3
server_addon/houdini/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "houdini"
|
||||
title = "Houdini"
|
||||
version = "0.2.12"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import HoudiniSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class Houdini(BaseServerAddon):
|
||||
name = "houdini"
|
||||
title = "Houdini"
|
||||
version = __version__
|
||||
settings_model: Type[HoudiniSettings] = HoudiniSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.2.13"
|
||||
3
server_addon/max/package.py
Normal file
3
server_addon/max/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "max"
|
||||
title = "Max"
|
||||
version = "0.1.7"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import MaxSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class MaxAddon(BaseServerAddon):
|
||||
name = "max"
|
||||
title = "Max"
|
||||
version = __version__
|
||||
settings_model: Type[MaxSettings] = MaxSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.7"
|
||||
3
server_addon/maya/package.py
Normal file
3
server_addon/maya/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "maya"
|
||||
title = "Maya"
|
||||
version = "0.1.16"
|
||||
|
|
@ -2,13 +2,9 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings.main import MayaSettings, DEFAULT_MAYA_SETTING
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class MayaAddon(BaseServerAddon):
|
||||
name = "maya"
|
||||
title = "Maya"
|
||||
version = __version__
|
||||
settings_model = MayaSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@ class ImportLoaderModel(BaseSettingsModel):
|
|||
group_name: str = SettingsField(title="Group name")
|
||||
|
||||
|
||||
class YetiRigLoaderModel(LoaderEnabledModel):
|
||||
create_cache_instance_on_load: bool = SettingsField(
|
||||
title="Create Yeti Cache instance on load",
|
||||
description=(
|
||||
"When enabled, upon loading a Yeti Rig product a new Yeti cache "
|
||||
"instance is automatically created as preparation to publishing "
|
||||
"the output directly."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LoadersModel(BaseSettingsModel):
|
||||
colors: ColorsSetting = SettingsField(
|
||||
default_factory=ColorsSetting,
|
||||
|
|
@ -195,8 +206,8 @@ class LoadersModel(BaseSettingsModel):
|
|||
default_factory=LoaderEnabledModel,
|
||||
title="Yeti Cache Loader"
|
||||
)
|
||||
YetiRigLoader: LoaderEnabledModel = SettingsField(
|
||||
default_factory=LoaderEnabledModel,
|
||||
YetiRigLoader: YetiRigLoaderModel = SettingsField(
|
||||
default_factory=YetiRigLoaderModel,
|
||||
title="Yeti Rig Loader"
|
||||
)
|
||||
|
||||
|
|
@ -266,5 +277,8 @@ DEFAULT_LOADERS_SETTING = {
|
|||
"VRaySceneLoader": {"enabled": True},
|
||||
"XgenLoader": {"enabled": True},
|
||||
"YetiCacheLoader": {"enabled": True},
|
||||
"YetiRigLoader": {"enabled": True},
|
||||
"YetiRigLoader": {
|
||||
"enabled": True,
|
||||
"create_cache_instance_on_load": True
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.15"
|
||||
3
server_addon/nuke/package.py
Normal file
3
server_addon/nuke/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "nuke"
|
||||
title = "Nuke"
|
||||
version = "0.1.10"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import NukeSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class NukeAddon(BaseServerAddon):
|
||||
name = "nuke"
|
||||
title = "Nuke"
|
||||
version = __version__
|
||||
settings_model: Type[NukeSettings] = NukeSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.10"
|
||||
3
server_addon/photoshop/package.py
Normal file
3
server_addon/photoshop/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "photoshop"
|
||||
title = "Photoshop"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import PhotoshopSettings, DEFAULT_PHOTOSHOP_SETTING
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class Photoshop(BaseServerAddon):
|
||||
name = "photoshop"
|
||||
title = "Photoshop"
|
||||
version = __version__
|
||||
|
||||
settings_model = PhotoshopSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.2"
|
||||
3
server_addon/resolve/package.py
Normal file
3
server_addon/resolve/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "resolve"
|
||||
title = "DaVinci Resolve"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import ResolveSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class ResolveAddon(BaseServerAddon):
|
||||
name = "resolve"
|
||||
title = "DaVinci Resolve"
|
||||
version = __version__
|
||||
settings_model: Type[ResolveSettings] = ResolveSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
||||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
3
server_addon/royalrender/package.py
Normal file
3
server_addon/royalrender/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "royalrender"
|
||||
title = "Royal Render"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import RoyalRenderSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class RoyalRenderAddon(BaseServerAddon):
|
||||
name = "royalrender"
|
||||
version = __version__
|
||||
title = "Royal Render"
|
||||
settings_model: Type[RoyalRenderSettings] = RoyalRenderSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
3
server_addon/substancepainter/package.py
Normal file
3
server_addon/substancepainter/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "substancepainter"
|
||||
title = "Substance Painter"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import SubstancePainterSettings, DEFAULT_SPAINTER_SETTINGS
|
||||
|
||||
|
||||
class SubstancePainterAddon(BaseServerAddon):
|
||||
name = "substancepainter"
|
||||
title = "Substance Painter"
|
||||
version = __version__
|
||||
settings_model: Type[SubstancePainterSettings] = SubstancePainterSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
122
server_addon/substancepainter/server/settings/load_plugins.py
Normal file
122
server_addon/substancepainter/server/settings/load_plugins.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
|
||||
|
||||
def normal_map_format_enum():
|
||||
return [
|
||||
{"label": "DirectX", "value": "NormalMapFormat.DirectX"},
|
||||
{"label": "OpenGL", "value": "NormalMapFormat.OpenGL"},
|
||||
]
|
||||
|
||||
|
||||
def tangent_space_enum():
|
||||
return [
|
||||
{"label": "Per Fragment", "value": "TangentSpace.PerFragment"},
|
||||
{"label": "Per Vertex", "value": "TangentSpace.PerVertex"},
|
||||
]
|
||||
|
||||
|
||||
def uv_workflow_enum():
|
||||
return [
|
||||
{"label": "Default", "value": "ProjectWorkflow.Default"},
|
||||
{"label": "UV Tile", "value": "ProjectWorkflow.UVTile"},
|
||||
{"label": "Texture Set Per UV Tile",
|
||||
"value": "ProjectWorkflow.TextureSetPerUVTile"}
|
||||
]
|
||||
|
||||
|
||||
def document_resolution_enum():
|
||||
return [
|
||||
{"label": "128", "value": 128},
|
||||
{"label": "256", "value": 256},
|
||||
{"label": "512", "value": 512},
|
||||
{"label": "1024", "value": 1024},
|
||||
{"label": "2048", "value": 2048},
|
||||
{"label": "4096", "value": 4096}
|
||||
]
|
||||
|
||||
|
||||
class ProjectTemplatesModel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
name: str = SettingsField("default", title="Template Name")
|
||||
default_texture_resolution: int = SettingsField(
|
||||
1024, enum_resolver=document_resolution_enum,
|
||||
title="Document Resolution",
|
||||
description=("Set texture resolution when "
|
||||
"creating new project.")
|
||||
)
|
||||
import_cameras: bool = SettingsField(
|
||||
True, title="Import Cameras",
|
||||
description="Import cameras from the mesh file.")
|
||||
normal_map_format: str = SettingsField(
|
||||
"DirectX", enum_resolver=normal_map_format_enum,
|
||||
title="Normal Map Format",
|
||||
description=("Set normal map format when "
|
||||
"creating new project.")
|
||||
)
|
||||
project_workflow: str = SettingsField(
|
||||
"Default", enum_resolver=uv_workflow_enum,
|
||||
title="UV Tile Settings",
|
||||
description=("Set UV workflow when "
|
||||
"creating new project.")
|
||||
)
|
||||
tangent_space_mode: str = SettingsField(
|
||||
"PerFragment", enum_resolver=tangent_space_enum,
|
||||
title="Tangent Space",
|
||||
description=("An option to compute tangent space "
|
||||
"when creating new project.")
|
||||
)
|
||||
preserve_strokes: bool = SettingsField(
|
||||
True, title="Preserve Strokes",
|
||||
description=("Preserve strokes positions on mesh.\n"
|
||||
"(only relevant when loading into "
|
||||
"existing project)")
|
||||
)
|
||||
|
||||
|
||||
class ProjectTemplateSettingModel(BaseSettingsModel):
|
||||
project_templates: list[ProjectTemplatesModel] = SettingsField(
|
||||
default_factory=ProjectTemplatesModel,
|
||||
title="Project Templates"
|
||||
)
|
||||
|
||||
|
||||
class LoadersModel(BaseSettingsModel):
|
||||
SubstanceLoadProjectMesh: ProjectTemplateSettingModel = SettingsField(
|
||||
default_factory=ProjectTemplateSettingModel,
|
||||
title="Load Mesh"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_LOADER_SETTINGS = {
|
||||
"SubstanceLoadProjectMesh": {
|
||||
"project_templates": [
|
||||
{
|
||||
"name": "2K(Default)",
|
||||
"default_texture_resolution": 2048,
|
||||
"import_cameras": True,
|
||||
"normal_map_format": "NormalMapFormat.DirectX",
|
||||
"project_workflow": "ProjectWorkflow.Default",
|
||||
"tangent_space_mode": "TangentSpace.PerFragment",
|
||||
"preserve_strokes": True
|
||||
},
|
||||
{
|
||||
"name": "2K(UV tile)",
|
||||
"default_texture_resolution": 2048,
|
||||
"import_cameras": True,
|
||||
"normal_map_format": "NormalMapFormat.DirectX",
|
||||
"project_workflow": "ProjectWorkflow.UVTile",
|
||||
"tangent_space_mode": "TangentSpace.PerFragment",
|
||||
"preserve_strokes": True
|
||||
},
|
||||
{
|
||||
"name": "4K(Custom)",
|
||||
"default_texture_resolution": 4096,
|
||||
"import_cameras": True,
|
||||
"normal_map_format": "NormalMapFormat.OpenGL",
|
||||
"project_workflow": "ProjectWorkflow.UVTile",
|
||||
"tangent_space_mode": "TangentSpace.PerFragment",
|
||||
"preserve_strokes": True
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
from .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS
|
||||
from .load_plugins import LoadersModel, DEFAULT_LOADER_SETTINGS
|
||||
|
||||
|
||||
class ShelvesSettingsModel(BaseSettingsModel):
|
||||
|
|
@ -17,9 +18,12 @@ class SubstancePainterSettings(BaseSettingsModel):
|
|||
default_factory=list,
|
||||
title="Shelves"
|
||||
)
|
||||
load: LoadersModel = SettingsField(
|
||||
default_factory=DEFAULT_LOADER_SETTINGS, title="Loaders")
|
||||
|
||||
|
||||
DEFAULT_SPAINTER_SETTINGS = {
|
||||
"imageio": DEFAULT_IMAGEIO_SETTINGS,
|
||||
"shelves": []
|
||||
"shelves": [],
|
||||
"load": DEFAULT_LOADER_SETTINGS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
||||
3
server_addon/timers_manager/package.py
Normal file
3
server_addon/timers_manager/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "timers_manager"
|
||||
title = "Timers Manager"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2,12 +2,8 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import TimersManagerSettings
|
||||
|
||||
|
||||
class TimersManagerAddon(BaseServerAddon):
|
||||
name = "timers_manager"
|
||||
version = __version__
|
||||
title = "Timers Manager"
|
||||
settings_model: Type[TimersManagerSettings] = TimersManagerSettings
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
3
server_addon/traypublisher/package.py
Normal file
3
server_addon/traypublisher/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "traypublisher"
|
||||
title = "TrayPublisher"
|
||||
version = "0.1.4"
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import TraypublisherSettings, DEFAULT_TRAYPUBLISHER_SETTING
|
||||
|
||||
|
||||
class Traypublisher(BaseServerAddon):
|
||||
name = "traypublisher"
|
||||
title = "TrayPublisher"
|
||||
version = __version__
|
||||
|
||||
settings_model = TraypublisherSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.4"
|
||||
3
server_addon/tvpaint/package.py
Normal file
3
server_addon/tvpaint/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "tvpaint"
|
||||
title = "TVPaint"
|
||||
version = "0.1.2"
|
||||
|
|
@ -2,14 +2,10 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import TvpaintSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class TvpaintAddon(BaseServerAddon):
|
||||
name = "tvpaint"
|
||||
title = "TVPaint"
|
||||
version = __version__
|
||||
settings_model: Type[TvpaintSettings] = TvpaintSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.2"
|
||||
3
server_addon/unreal/package.py
Normal file
3
server_addon/unreal/package.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name = "unreal"
|
||||
title = "Unreal"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2,17 +2,11 @@ from typing import Type
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import UnrealSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class UnrealAddon(BaseServerAddon):
|
||||
name = "unreal"
|
||||
title = "Unreal"
|
||||
version = __version__
|
||||
settings_model: Type[UnrealSettings] = UnrealSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
||||
Loading…
Add table
Add a link
Reference in a new issue