Merge branch 'develop' into enhancement/OP-1017_houdini-colorspaces

This commit is contained in:
MustafaJafar 2024-04-25 10:59:20 +02:00
commit 5a48baddf4
91 changed files with 1049 additions and 850 deletions

View file

@ -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

View file

@ -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."""

View file

@ -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

View file

@ -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,

View file

@ -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()

View file

@ -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")

View file

@ -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 "

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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}
)

View file

@ -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)

View file

@ -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"] = []

View file

@ -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:

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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
)
)

View file

@ -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)

View file

@ -0,0 +1,3 @@
name = "aftereffects"
title = "AfterEffects"
version = "0.1.3"

View file

@ -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):

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.3"

View file

@ -0,0 +1,3 @@
name = "blender"
title = "Blender"
version = "0.1.8"

View file

@ -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()

View file

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

View file

@ -0,0 +1,3 @@
name = "celaction"
title = "CelAction"
version = "0.1.0"

View file

@ -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()

View file

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

View file

@ -0,0 +1,3 @@
name = "clockify"
title = "Clockify"
version = "0.1.1"

View file

@ -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 = {}

View file

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

View file

@ -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(

View file

@ -0,0 +1,3 @@
name = "deadline"
title = "Deadline"
version = "0.1.10"

View file

@ -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):

View file

@ -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]:

View file

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

View file

@ -0,0 +1,3 @@
name = "flame"
title = "Flame"
version = "0.1.0"

View file

@ -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()

View file

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

View file

@ -0,0 +1,3 @@
name = "fusion"
title = "Fusion"
version = "0.1.5"

View file

@ -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()

View file

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

View file

@ -0,0 +1,3 @@
name = "harmony"
title = "Harmony"
version = "0.1.2"

View file

@ -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):

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.2"

View file

@ -0,0 +1,3 @@
name = "hiero"
title = "Hiero"
version = "0.1.2"

View file

@ -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()

View file

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

View file

@ -0,0 +1,3 @@
name = "houdini"
title = "Houdini"
version = "0.2.12"

View file

@ -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):

View file

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

View file

@ -0,0 +1,3 @@
name = "max"
title = "Max"
version = "0.1.7"

View file

@ -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):

View file

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

View file

@ -0,0 +1,3 @@
name = "maya"
title = "Maya"
version = "0.1.16"

View file

@ -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):

View file

@ -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
},
}

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.15"

View file

@ -0,0 +1,3 @@
name = "nuke"
title = "Nuke"
version = "0.1.10"

View file

@ -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):

View file

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

View file

@ -0,0 +1,3 @@
name = "photoshop"
title = "Photoshop"
version = "0.1.2"

View file

@ -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):

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.2"

View file

@ -0,0 +1,3 @@
name = "resolve"
title = "DaVinci Resolve"
version = "0.1.0"

View file

@ -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()

View file

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

View file

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

View file

@ -0,0 +1,3 @@
name = "royalrender"
title = "Royal Render"
version = "0.1.1"

View file

@ -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):

View file

@ -0,0 +1,3 @@
name = "substancepainter"
title = "Substance Painter"
version = "0.1.1"

View file

@ -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):

View 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
}
]
}
}

View file

@ -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,
}

View file

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

View file

@ -0,0 +1,3 @@
name = "timers_manager"
title = "Timers Manager"
version = "0.1.1"

View file

@ -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

View file

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

View file

@ -0,0 +1,3 @@
name = "traypublisher"
title = "TrayPublisher"
version = "0.1.4"

View file

@ -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):

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.4"

View file

@ -0,0 +1,3 @@
name = "tvpaint"
title = "TVPaint"
version = "0.1.2"

View file

@ -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):

View file

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

View file

@ -0,0 +1,3 @@
name = "unreal"
title = "Unreal"
version = "0.1.0"

View file

@ -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()

View file

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