mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into bugfix/1139-version_up_current_workfile-doesnt-return-the-first-workfile-name-if-there-are-no-workfiles
This commit is contained in:
commit
600c6e6207
49 changed files with 3516 additions and 181 deletions
|
|
@ -26,10 +26,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"photoshop",
|
||||
"tvpaint",
|
||||
"substancepainter",
|
||||
"substancedesigner",
|
||||
"aftereffects",
|
||||
"wrap",
|
||||
"openrv",
|
||||
"cinema4d"
|
||||
"cinema4d",
|
||||
"silhouette",
|
||||
}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
order = 0
|
||||
hosts = {
|
||||
"substancepainter",
|
||||
"substancedesigner",
|
||||
"fusion",
|
||||
"blender",
|
||||
"aftereffects",
|
||||
|
|
@ -20,7 +21,8 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"hiero",
|
||||
"resolve",
|
||||
"openrv",
|
||||
"cinema4d"
|
||||
"cinema4d",
|
||||
"silhouette",
|
||||
}
|
||||
launch_types = set()
|
||||
|
||||
|
|
|
|||
|
|
@ -108,21 +108,29 @@ def run_subprocess(*args, **kwargs):
|
|||
| getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
)
|
||||
|
||||
# Escape parentheses for bash
|
||||
# Escape special characters in certain shells
|
||||
if (
|
||||
kwargs.get("shell") is True
|
||||
and len(args) == 1
|
||||
and isinstance(args[0], str)
|
||||
and os.getenv("SHELL") in ("/bin/bash", "/bin/sh")
|
||||
):
|
||||
new_arg = (
|
||||
args[0]
|
||||
.replace("(", "\\(")
|
||||
.replace(")", "\\)")
|
||||
)
|
||||
args = (new_arg, )
|
||||
# Escape parentheses for bash
|
||||
if os.getenv("SHELL") in ("/bin/bash", "/bin/sh"):
|
||||
new_arg = (
|
||||
args[0]
|
||||
.replace("(", "\\(")
|
||||
.replace(")", "\\)")
|
||||
)
|
||||
args = (new_arg,)
|
||||
# Escape & on Windows in shell with `cmd.exe` using ^&
|
||||
elif (
|
||||
platform.system().lower() == "windows"
|
||||
and os.getenv("COMSPEC").endswith("cmd.exe")
|
||||
):
|
||||
new_arg = args[0].replace("&", "^&")
|
||||
args = (new_arg, )
|
||||
|
||||
# Get environents from kwarg or use current process environments if were
|
||||
# Get environments from kwarg or use current process environments if were
|
||||
# not passed.
|
||||
env = kwargs.get("env") or os.environ
|
||||
# Make sure environment contains only strings
|
||||
|
|
|
|||
|
|
@ -587,8 +587,8 @@ class FormattingPart:
|
|||
if sub_key < 0:
|
||||
sub_key = len(value) + sub_key
|
||||
|
||||
invalid = 0 > sub_key < len(data)
|
||||
if invalid:
|
||||
valid = 0 <= sub_key < len(value)
|
||||
if not valid:
|
||||
used_keys.append(sub_key)
|
||||
missing_key = True
|
||||
break
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from ayon_core.lib.events import QueuedEventSystem
|
|||
from ayon_core.lib.attribute_definitions import get_default_values
|
||||
from ayon_core.host import IPublishHost, IWorkfileHost
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
from ayon_core.pipeline.plugin_discover import DiscoverResult
|
||||
|
||||
from .exceptions import (
|
||||
|
|
@ -480,6 +481,36 @@ class CreateContext:
|
|||
self.get_current_project_name())
|
||||
return self._current_project_settings
|
||||
|
||||
def get_template_data(
|
||||
self, folder_path: Optional[str], task_name: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare template data for given context.
|
||||
|
||||
Method is using cached entities and settings to prepare template data.
|
||||
|
||||
Args:
|
||||
folder_path (Optional[str]): Folder path.
|
||||
task_name (Optional[str]): Task name.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Template data.
|
||||
|
||||
"""
|
||||
project_entity = self.get_current_project_entity()
|
||||
folder_entity = task_entity = None
|
||||
if folder_path:
|
||||
folder_entity = self.get_folder_entity(folder_path)
|
||||
if task_name and folder_entity:
|
||||
task_entity = self.get_task_entity(folder_path, task_name)
|
||||
|
||||
return get_template_data(
|
||||
project_entity,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
host_name=self.host_name,
|
||||
settings=self.get_current_project_settings(),
|
||||
)
|
||||
|
||||
@property
|
||||
def context_has_changed(self):
|
||||
"""Host context has changed.
|
||||
|
|
|
|||
|
|
@ -562,6 +562,10 @@ class BaseCreator(ABC):
|
|||
instance
|
||||
)
|
||||
|
||||
cur_project_name = self.create_context.get_current_project_name()
|
||||
if not project_entity and project_name == cur_project_name:
|
||||
project_entity = self.create_context.get_current_project_entity()
|
||||
|
||||
return get_product_name(
|
||||
project_name,
|
||||
task_name,
|
||||
|
|
@ -858,18 +862,30 @@ class Creator(BaseCreator):
|
|||
["CollectAnatomyInstanceData"]
|
||||
["follow_workfile_version"]
|
||||
)
|
||||
follow_version_hosts = (
|
||||
publish_settings
|
||||
["CollectSceneVersion"]
|
||||
["hosts"]
|
||||
)
|
||||
|
||||
current_host = create_ctx.host.name
|
||||
follow_workfile_version = (
|
||||
follow_workfile_version and
|
||||
current_host in follow_version_hosts
|
||||
)
|
||||
|
||||
# Gather version number provided from the instance.
|
||||
current_workfile = create_ctx.get_current_workfile_path()
|
||||
version = instance.get("version")
|
||||
|
||||
# If follow workfile, gather version from workfile path.
|
||||
if version is None and follow_workfile_version:
|
||||
current_workfile = self.create_context.get_current_workfile_path()
|
||||
if version is None and follow_workfile_version and current_workfile:
|
||||
workfile_version = get_version_from_path(current_workfile)
|
||||
version = int(workfile_version)
|
||||
if workfile_version is not None:
|
||||
version = int(workfile_version)
|
||||
|
||||
# Fill-up version with next version available.
|
||||
elif version is None:
|
||||
if version is None:
|
||||
versions = self.get_next_versions_for_instances(
|
||||
[instance]
|
||||
)
|
||||
|
|
@ -916,6 +932,7 @@ class Creator(BaseCreator):
|
|||
instance.transient_data.update({
|
||||
"stagingDir": staging_dir_path,
|
||||
"stagingDir_persistent": staging_dir_info.is_persistent,
|
||||
"stagingDir_is_custom": staging_dir_info.is_custom,
|
||||
})
|
||||
|
||||
self.log.info(f"Applied staging dir to instance: {staging_dir_path}")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import os
|
|||
import logging
|
||||
import collections
|
||||
|
||||
from ayon_core.pipeline.constants import AVALON_INSTANCE_ID
|
||||
from ayon_core.pipeline.constants import AYON_INSTANCE_ID
|
||||
|
||||
from .product_name import get_product_name
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class LegacyCreator:
|
|||
# Default data
|
||||
self.data = collections.OrderedDict()
|
||||
# TODO use 'AYON_INSTANCE_ID' when all hosts support it
|
||||
self.data["id"] = AVALON_INSTANCE_ID
|
||||
self.data["id"] = AYON_INSTANCE_ID
|
||||
self.data["productType"] = self.product_type
|
||||
self.data["folderPath"] = folder_path
|
||||
self.data["productName"] = name
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
import typing
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
|
|
@ -17,6 +18,9 @@ from ayon_core.pipeline import (
|
|||
from .exceptions import ImmutableKeyError
|
||||
from .changes import TrackChangesItem
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .creator_plugins import BaseCreator
|
||||
|
||||
|
||||
class ConvertorItem:
|
||||
"""Item representing convertor plugin.
|
||||
|
|
@ -444,10 +448,11 @@ class CreatedInstance:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
product_type,
|
||||
product_name,
|
||||
data,
|
||||
creator,
|
||||
product_type: str,
|
||||
product_name: str,
|
||||
data: Dict[str, Any],
|
||||
creator: "BaseCreator",
|
||||
transient_data: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self._creator = creator
|
||||
creator_identifier = creator.identifier
|
||||
|
|
@ -462,7 +467,9 @@ class CreatedInstance:
|
|||
self._members = []
|
||||
|
||||
# Data that can be used for lifetime of object
|
||||
self._transient_data = {}
|
||||
if transient_data is None:
|
||||
transient_data = {}
|
||||
self._transient_data = transient_data
|
||||
|
||||
# Create a copy of passed data to avoid changing them on the fly
|
||||
data = copy.deepcopy(data or {})
|
||||
|
|
@ -492,7 +499,7 @@ class CreatedInstance:
|
|||
item_id = data.get("id")
|
||||
# TODO use only 'AYON_INSTANCE_ID' when all hosts support it
|
||||
if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}:
|
||||
item_id = AVALON_INSTANCE_ID
|
||||
item_id = AYON_INSTANCE_ID
|
||||
self._data["id"] = item_id
|
||||
self._data["productType"] = product_type
|
||||
self._data["productName"] = product_name
|
||||
|
|
@ -787,16 +794,26 @@ class CreatedInstance:
|
|||
self._create_context.instance_create_attr_defs_changed(self.id)
|
||||
|
||||
@classmethod
|
||||
def from_existing(cls, instance_data, creator):
|
||||
def from_existing(
|
||||
cls,
|
||||
instance_data: Dict[str, Any],
|
||||
creator: "BaseCreator",
|
||||
transient_data: Optional[Dict[str, Any]] = None,
|
||||
) -> "CreatedInstance":
|
||||
"""Convert instance data from workfile to CreatedInstance.
|
||||
|
||||
Args:
|
||||
instance_data (Dict[str, Any]): Data in a structure ready for
|
||||
'CreatedInstance' object.
|
||||
creator (BaseCreator): Creator plugin which is creating the
|
||||
instance of for which the instance belong.
|
||||
"""
|
||||
instance of for which the instance belongs.
|
||||
transient_data (Optional[dict[str, Any]]): Instance transient
|
||||
data.
|
||||
|
||||
Returns:
|
||||
CreatedInstance: Instance object.
|
||||
|
||||
"""
|
||||
instance_data = copy.deepcopy(instance_data)
|
||||
|
||||
product_type = instance_data.get("productType")
|
||||
|
|
@ -809,7 +826,11 @@ class CreatedInstance:
|
|||
product_name = instance_data.get("subset")
|
||||
|
||||
return cls(
|
||||
product_type, product_name, instance_data, creator
|
||||
product_type,
|
||||
product_name,
|
||||
instance_data,
|
||||
creator,
|
||||
transient_data=transient_data,
|
||||
)
|
||||
|
||||
def attribute_value_changed(self, key, changes):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import re
|
||||
import clique
|
||||
import math
|
||||
|
||||
import opentimelineio as otio
|
||||
from opentimelineio import opentime as _ot
|
||||
|
|
@ -256,8 +257,14 @@ def remap_range_on_file_sequence(otio_clip, otio_range):
|
|||
rate=available_range_rate,
|
||||
).to_frames()
|
||||
|
||||
# e.g.:
|
||||
# duration = 10 frames at 24fps
|
||||
# if frame_in = 1001 then
|
||||
# frame_out = 1010
|
||||
offset_duration = max(0, otio_range.duration.to_frames() - 1)
|
||||
|
||||
frame_out = otio.opentime.RationalTime.from_frames(
|
||||
frame_in + otio_range.duration.to_frames() - 1,
|
||||
frame_in + offset_duration,
|
||||
rate=available_range_rate,
|
||||
).to_frames()
|
||||
|
||||
|
|
@ -337,8 +344,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
|
||||
# modifiers
|
||||
time_scalar = 1.
|
||||
offset_in = 0
|
||||
offset_out = 0
|
||||
time_warp_nodes = []
|
||||
|
||||
# Check for speed effects and adjust playback speed accordingly
|
||||
|
|
@ -369,24 +374,15 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
tw_node.update(metadata)
|
||||
tw_node["lookup"] = list(lookup)
|
||||
|
||||
# get first and last frame offsets
|
||||
offset_in += lookup[0]
|
||||
offset_out += lookup[-1]
|
||||
|
||||
# add to timewarp nodes
|
||||
time_warp_nodes.append(tw_node)
|
||||
|
||||
# multiply by time scalar
|
||||
offset_in *= time_scalar
|
||||
offset_out *= time_scalar
|
||||
|
||||
# scale handles
|
||||
handle_start *= abs(time_scalar)
|
||||
handle_end *= abs(time_scalar)
|
||||
|
||||
# flip offset and handles if reversed speed
|
||||
if time_scalar < 0:
|
||||
offset_in, offset_out = offset_out, offset_in
|
||||
handle_start, handle_end = handle_end, handle_start
|
||||
|
||||
# If media source is an image sequence, returned
|
||||
|
|
@ -395,17 +391,19 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
if is_input_sequence:
|
||||
|
||||
src_in = conformed_source_range.start_time
|
||||
src_duration = conformed_source_range.duration
|
||||
|
||||
offset_in = otio.opentime.RationalTime(offset_in, rate=src_in.rate)
|
||||
offset_duration = otio.opentime.RationalTime(
|
||||
offset_out,
|
||||
rate=src_duration.rate
|
||||
src_duration = math.ceil(
|
||||
otio_clip.source_range.duration.value
|
||||
* abs(time_scalar)
|
||||
)
|
||||
retimed_duration = otio.opentime.RationalTime(
|
||||
src_duration,
|
||||
otio_clip.source_range.duration.rate
|
||||
)
|
||||
retimed_duration = retimed_duration.rescaled_to(src_in.rate)
|
||||
|
||||
trim_range = otio.opentime.TimeRange(
|
||||
start_time=src_in + offset_in,
|
||||
duration=src_duration + offset_duration
|
||||
start_time=src_in,
|
||||
duration=retimed_duration,
|
||||
)
|
||||
|
||||
# preserve discrete frame numbers
|
||||
|
|
@ -418,18 +416,92 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
|
||||
else:
|
||||
# compute retimed range
|
||||
media_in_trimmed = conformed_source_range.start_time.value + offset_in
|
||||
media_out_trimmed = media_in_trimmed + (
|
||||
(
|
||||
conformed_source_range.duration.value
|
||||
* abs(time_scalar)
|
||||
+ offset_out
|
||||
) - 1
|
||||
media_in_trimmed = conformed_source_range.start_time.value
|
||||
|
||||
offset_duration = (
|
||||
conformed_source_range.duration.value
|
||||
* abs(time_scalar)
|
||||
)
|
||||
|
||||
# Offset duration by 1 for media out frame
|
||||
# - only if duration is not single frame (start frame != end frame)
|
||||
if offset_duration > 0:
|
||||
offset_duration -= 1
|
||||
media_out_trimmed = media_in_trimmed + offset_duration
|
||||
|
||||
media_in = available_range.start_time.value
|
||||
media_out = available_range.end_time_inclusive().value
|
||||
|
||||
if time_warp_nodes:
|
||||
# Naive approach: Resolve consecutive timewarp(s) on range,
|
||||
# then check if plate range has to be extended beyond source range.
|
||||
in_frame = media_in_trimmed
|
||||
frame_range = [in_frame]
|
||||
for _ in range(otio_clip.source_range.duration.to_frames() - 1):
|
||||
in_frame += time_scalar
|
||||
frame_range.append(in_frame)
|
||||
|
||||
# Different editorial DCC might have different TimeWarp logic.
|
||||
# The following logic assumes that the "lookup" list values are
|
||||
# frame offsets relative to the current source frame number.
|
||||
#
|
||||
# media_source_range |______1_____|______2______|______3______|
|
||||
#
|
||||
# media_retimed_range |______2_____|______2______|______3______|
|
||||
#
|
||||
# TimeWarp lookup +1 0 0
|
||||
for tw_idx, tw in enumerate(time_warp_nodes):
|
||||
for idx, frame_number in enumerate(frame_range):
|
||||
# First timewarp, apply on media range
|
||||
if tw_idx == 0:
|
||||
frame_range[idx] = round(
|
||||
frame_number +
|
||||
(tw["lookup"][idx] * time_scalar)
|
||||
)
|
||||
# Consecutive timewarp, apply on the previous result
|
||||
else:
|
||||
new_idx = round(idx + tw["lookup"][idx])
|
||||
|
||||
if 0 <= new_idx < len(frame_range):
|
||||
frame_range[idx] = frame_range[new_idx]
|
||||
continue
|
||||
|
||||
# TODO: implementing this would need to actually have
|
||||
# retiming engine resolve process within AYON,
|
||||
# resolving wraps as curves, then projecting
|
||||
# those into the previous media_range.
|
||||
raise NotImplementedError(
|
||||
"Unsupported consecutive timewarps "
|
||||
"(out of computed range)"
|
||||
)
|
||||
|
||||
# adjust range if needed
|
||||
media_in_trimmed_before_tw = media_in_trimmed
|
||||
media_in_trimmed = max(min(frame_range), media_in)
|
||||
media_out_trimmed = min(max(frame_range), media_out)
|
||||
|
||||
# If TimeWarp changes the first frame of the soure range,
|
||||
# we need to offset the first TimeWarp values accordingly.
|
||||
#
|
||||
# expected_range |______2_____|______2______|______3______|
|
||||
#
|
||||
# EDITORIAL
|
||||
# media_source_range |______1_____|______2______|______3______|
|
||||
#
|
||||
# TimeWarp lookup +1 0 0
|
||||
#
|
||||
# EXTRACTED PLATE
|
||||
# plate_range |______2_____|______3______|_ _ _ _ _ _ _|
|
||||
#
|
||||
# expected TimeWarp 0 -1 -1
|
||||
if media_in_trimmed != media_in_trimmed_before_tw:
|
||||
offset = media_in_trimmed_before_tw - media_in_trimmed
|
||||
offset *= 1.0 / time_scalar
|
||||
time_warp_nodes[0]["lookup"] = [
|
||||
value + offset
|
||||
for value in time_warp_nodes[0]["lookup"]
|
||||
]
|
||||
|
||||
# adjust available handles if needed
|
||||
if (media_in_trimmed - media_in) < handle_start:
|
||||
handle_start = max(0, media_in_trimmed - media_in)
|
||||
|
|
@ -448,16 +520,16 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
"retime": True,
|
||||
"speed": time_scalar,
|
||||
"timewarps": time_warp_nodes,
|
||||
"handleStart": int(handle_start),
|
||||
"handleEnd": int(handle_end)
|
||||
"handleStart": math.ceil(handle_start),
|
||||
"handleEnd": math.ceil(handle_end)
|
||||
}
|
||||
}
|
||||
|
||||
returning_dict = {
|
||||
"mediaIn": media_in_trimmed,
|
||||
"mediaOut": media_out_trimmed,
|
||||
"handleStart": int(handle_start),
|
||||
"handleEnd": int(handle_end),
|
||||
"handleStart": math.ceil(handle_start),
|
||||
"handleEnd": math.ceil(handle_end),
|
||||
"speed": time_scalar
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -464,6 +464,12 @@ def filter_pyblish_plugins(plugins):
|
|||
if getattr(plugin, "enabled", True) is False:
|
||||
plugins.remove(plugin)
|
||||
|
||||
# Pyblish already operated a filter based on host.
|
||||
# But applying settings might have changed "hosts"
|
||||
# value in plugin so re-filter.
|
||||
elif not pyblish.plugin.host_is_compatible(plugin):
|
||||
plugins.remove(plugin)
|
||||
|
||||
|
||||
def get_errored_instances_from_context(context, plugin=None):
|
||||
"""Collect failed instances from pyblish context.
|
||||
|
|
@ -708,6 +714,7 @@ def get_instance_staging_dir(instance):
|
|||
project_settings=context.data["project_settings"],
|
||||
template_data=template_data,
|
||||
always_return_path=True,
|
||||
username=context.data["user"],
|
||||
)
|
||||
|
||||
staging_dir_path = staging_dir_info.directory
|
||||
|
|
|
|||
|
|
@ -292,6 +292,9 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin):
|
|||
```
|
||||
"""
|
||||
|
||||
# Allow exposing tooltip from class with `optional_tooltip` attribute
|
||||
optional_tooltip: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
"""Attribute definitions based on plugin's optional attribute."""
|
||||
|
|
@ -304,8 +307,14 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin):
|
|||
active = getattr(cls, "active", True)
|
||||
# Return boolean stored under 'active' key with label of the class name
|
||||
label = cls.label or cls.__name__
|
||||
|
||||
return [
|
||||
BoolDef("active", default=active, label=label)
|
||||
BoolDef(
|
||||
"active",
|
||||
default=active,
|
||||
label=label,
|
||||
tooltip=cls.optional_tooltip,
|
||||
)
|
||||
]
|
||||
|
||||
def is_active(self, data):
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ def get_staging_dir_info(
|
|||
logger: Optional[logging.Logger] = None,
|
||||
prefix: Optional[str] = None,
|
||||
suffix: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
) -> Optional[StagingDir]:
|
||||
"""Get staging dir info data.
|
||||
|
||||
|
|
@ -157,6 +158,7 @@ def get_staging_dir_info(
|
|||
logger (Optional[logging.Logger]): Logger instance.
|
||||
prefix (Optional[str]) Optional prefix for staging dir name.
|
||||
suffix (Optional[str]): Optional suffix for staging dir name.
|
||||
username (Optional[str]): AYON Username.
|
||||
|
||||
Returns:
|
||||
Optional[StagingDir]: Staging dir info data
|
||||
|
|
@ -183,7 +185,9 @@ def get_staging_dir_info(
|
|||
|
||||
# making few queries to database
|
||||
ctx_data = get_template_data(
|
||||
project_entity, folder_entity, task_entity, host_name
|
||||
project_entity, folder_entity, task_entity, host_name,
|
||||
settings=project_settings,
|
||||
username=username
|
||||
)
|
||||
|
||||
# add additional data
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from ayon_core.settings import get_studio_settings
|
|||
from ayon_core.lib.local_settings import get_ayon_username
|
||||
|
||||
|
||||
def get_general_template_data(settings=None):
|
||||
def get_general_template_data(settings=None, username=None):
|
||||
"""General template data based on system settings or machine.
|
||||
|
||||
Output contains formatting keys:
|
||||
|
|
@ -14,17 +14,22 @@ def get_general_template_data(settings=None):
|
|||
|
||||
Args:
|
||||
settings (Dict[str, Any]): Studio or project settings.
|
||||
username (Optional[str]): AYON Username.
|
||||
"""
|
||||
|
||||
if not settings:
|
||||
settings = get_studio_settings()
|
||||
|
||||
if username is None:
|
||||
username = get_ayon_username()
|
||||
|
||||
core_settings = settings["core"]
|
||||
return {
|
||||
"studio": {
|
||||
"name": core_settings["studio_name"],
|
||||
"code": core_settings["studio_code"]
|
||||
},
|
||||
"user": get_ayon_username()
|
||||
"user": username
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -145,6 +150,7 @@ def get_template_data(
|
|||
task_entity=None,
|
||||
host_name=None,
|
||||
settings=None,
|
||||
username=None
|
||||
):
|
||||
"""Prepare data for templates filling from entered documents and info.
|
||||
|
||||
|
|
@ -167,12 +173,13 @@ def get_template_data(
|
|||
host_name (Optional[str]): Used to fill '{app}' key.
|
||||
settings (Union[Dict, None]): Prepared studio or project settings.
|
||||
They're queried if not passed (may be slower).
|
||||
username (Optional[str]): AYON Username.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Data prepared for filling workdir template.
|
||||
"""
|
||||
|
||||
template_data = get_general_template_data(settings)
|
||||
template_data = get_general_template_data(settings, username=username)
|
||||
template_data.update(get_project_template_data(project_entity))
|
||||
if folder_entity:
|
||||
template_data.update(get_folder_template_data(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ class CollectOtioReview(pyblish.api.InstancePlugin):
|
|||
# optionally get `reviewTrack`
|
||||
review_track_name = instance.data.get("reviewTrack")
|
||||
|
||||
# [clip_media] setting:
|
||||
# Extract current clip source range as reviewable.
|
||||
# Flag review content from otio_clip.
|
||||
if not review_track_name and "review" in instance.data["families"]:
|
||||
otio_review_clips = [otio_clip]
|
||||
|
||||
# skip if no review track available
|
||||
elif not review_track_name:
|
||||
return
|
||||
|
||||
# generate range in parent
|
||||
otio_tl_range = otio_clip.range_in_parent()
|
||||
|
||||
|
|
@ -43,12 +53,14 @@ class CollectOtioReview(pyblish.api.InstancePlugin):
|
|||
clip_frame_end = int(
|
||||
otio_tl_range.start_time.value + otio_tl_range.duration.value)
|
||||
|
||||
# skip if no review track available
|
||||
if not review_track_name:
|
||||
return
|
||||
|
||||
# loop all tracks and match with name in `reviewTrack`
|
||||
for track in otio_timeline.tracks:
|
||||
|
||||
# No review track defined, skip the loop
|
||||
if review_track_name is None:
|
||||
break
|
||||
|
||||
# Not current review track, skip it.
|
||||
if review_track_name != track.name:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Provides:
|
|||
instance -> otioReviewClips
|
||||
"""
|
||||
import os
|
||||
import math
|
||||
|
||||
import clique
|
||||
import pyblish.api
|
||||
|
|
@ -69,9 +70,17 @@ class CollectOtioSubsetResources(
|
|||
self.log.debug(
|
||||
">> retimed_attributes: {}".format(retimed_attributes))
|
||||
|
||||
# break down into variables
|
||||
media_in = int(retimed_attributes["mediaIn"])
|
||||
media_out = int(retimed_attributes["mediaOut"])
|
||||
# break down into variables as rounded frame numbers
|
||||
#
|
||||
# 0 1 2 3 4
|
||||
# |-------------|---------------|--------------|-------------|
|
||||
# |_______________media range_______________|
|
||||
# 0.6 3.2
|
||||
#
|
||||
# As rounded frames, media_in = 0 and media_out = 4
|
||||
media_in = math.floor(retimed_attributes["mediaIn"])
|
||||
media_out = math.ceil(retimed_attributes["mediaOut"])
|
||||
|
||||
handle_start = int(retimed_attributes["handleStart"])
|
||||
handle_end = int(retimed_attributes["handleEnd"])
|
||||
|
||||
|
|
@ -149,7 +158,6 @@ class CollectOtioSubsetResources(
|
|||
|
||||
self.log.info(
|
||||
"frame_start-frame_end: {}-{}".format(frame_start, frame_end))
|
||||
review_repre = None
|
||||
|
||||
if is_sequence:
|
||||
# file sequence way
|
||||
|
|
@ -174,17 +182,18 @@ class CollectOtioSubsetResources(
|
|||
path, trimmed_media_range_h, metadata)
|
||||
self.staging_dir, collection = collection_data
|
||||
|
||||
self.log.debug(collection)
|
||||
repre = self._create_representation(
|
||||
frame_start, frame_end, collection=collection)
|
||||
if len(collection.indexes) > 1:
|
||||
self.log.debug(collection)
|
||||
repre = self._create_representation(
|
||||
frame_start, frame_end, collection=collection)
|
||||
else:
|
||||
filename = tuple(collection)[0]
|
||||
self.log.debug(filename)
|
||||
|
||||
# TODO: discuss this, it erases frame number.
|
||||
repre = self._create_representation(
|
||||
frame_start, frame_end, file=filename)
|
||||
|
||||
if (
|
||||
not instance.data.get("otioReviewClips")
|
||||
and "review" in instance.data["families"]
|
||||
):
|
||||
review_repre = self._create_representation(
|
||||
frame_start, frame_end, collection=collection,
|
||||
delete=True, review=True)
|
||||
|
||||
else:
|
||||
_trim = False
|
||||
|
|
@ -200,13 +209,6 @@ class CollectOtioSubsetResources(
|
|||
repre = self._create_representation(
|
||||
frame_start, frame_end, file=filename, trim=_trim)
|
||||
|
||||
if (
|
||||
not instance.data.get("otioReviewClips")
|
||||
and "review" in instance.data["families"]
|
||||
):
|
||||
review_repre = self._create_representation(
|
||||
frame_start, frame_end,
|
||||
file=filename, delete=True, review=True)
|
||||
|
||||
instance.data["originalDirname"] = self.staging_dir
|
||||
|
||||
|
|
@ -219,9 +221,6 @@ class CollectOtioSubsetResources(
|
|||
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
# add review representation to instance data
|
||||
if review_repre:
|
||||
instance.data["representations"].append(review_repre)
|
||||
|
||||
self.log.debug(instance.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,8 +93,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
|
||||
# now we can just add instances from json file and we are done
|
||||
any_staging_dir_persistent = False
|
||||
for instance_data in data.get("instances"):
|
||||
|
||||
for instance_data in data["instances"]:
|
||||
self.log.debug(" - processing instance for {}".format(
|
||||
instance_data.get("productName")))
|
||||
instance = self._context.create_instance(
|
||||
|
|
@ -105,7 +104,11 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
instance.data.update(instance_data)
|
||||
|
||||
# stash render job id for later validation
|
||||
instance.data["render_job_id"] = data.get("job").get("_id")
|
||||
instance.data["publishJobMetadata"] = data
|
||||
# TODO remove 'render_job_id' here and rather use
|
||||
# 'publishJobMetadata' where is needed.
|
||||
# - this is deadline specific
|
||||
instance.data["render_job_id"] = data.get("job", {}).get("_id")
|
||||
staging_dir_persistent = instance.data.get(
|
||||
"stagingDir_persistent", False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"yeticacheUE",
|
||||
"tycache",
|
||||
"usd",
|
||||
"oxrig"
|
||||
"oxrig",
|
||||
"sbsar",
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -14,23 +14,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder
|
||||
label = 'Collect Scene Version'
|
||||
# configurable in Settings
|
||||
hosts = [
|
||||
"aftereffects",
|
||||
"blender",
|
||||
"celaction",
|
||||
"fusion",
|
||||
"harmony",
|
||||
"hiero",
|
||||
"houdini",
|
||||
"maya",
|
||||
"max",
|
||||
"nuke",
|
||||
"photoshop",
|
||||
"resolve",
|
||||
"tvpaint",
|
||||
"motionbuilder",
|
||||
"substancepainter"
|
||||
]
|
||||
hosts = ["*"]
|
||||
|
||||
# in some cases of headless publishing (for example webpublisher using PS)
|
||||
# you want to ignore version from name and let integrate use next version
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ class ExtractOTIOReview(
|
|||
self.actual_fps = available_range.duration.rate
|
||||
start = src_range.start_time.rescaled_to(self.actual_fps)
|
||||
duration = src_range.duration.rescaled_to(self.actual_fps)
|
||||
src_frame_start = src_range.start_time.to_frames()
|
||||
|
||||
# Temporary.
|
||||
# Some AYON custom OTIO exporter were implemented with
|
||||
|
|
@ -157,7 +156,7 @@ class ExtractOTIOReview(
|
|||
if (
|
||||
is_clip_from_media_sequence(r_otio_cl)
|
||||
and available_range_start_frame == media_ref.start_frame
|
||||
and src_frame_start < media_ref.start_frame
|
||||
and start.to_frames() < media_ref.start_frame
|
||||
):
|
||||
available_range = otio.opentime.TimeRange(
|
||||
otio.opentime.RationalTime(0, rate=self.actual_fps),
|
||||
|
|
@ -321,6 +320,9 @@ class ExtractOTIOReview(
|
|||
end = max(collection.indexes)
|
||||
|
||||
files = [f for f in collection]
|
||||
# single frame sequence
|
||||
if len(files) == 1:
|
||||
files = files[0]
|
||||
ext = collection.format("{tail}")
|
||||
representation_data.update({
|
||||
"name": ext[1:],
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"resolve",
|
||||
"traypublisher",
|
||||
"substancepainter",
|
||||
"substancedesigner",
|
||||
"nuke",
|
||||
"aftereffects",
|
||||
"unreal",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
|
|||
label = "Validate File Saved"
|
||||
order = pyblish.api.ValidatorOrder - 0.1
|
||||
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
|
||||
"cinema4d"]
|
||||
"cinema4d", "silhouette"]
|
||||
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
|
||||
|
||||
def process(self, context):
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ Enabled vs Disabled logic in most of stylesheets
|
|||
font-family: "Noto Sans";
|
||||
font-weight: 450;
|
||||
outline: none;
|
||||
|
||||
/* Define icon size to fix size issues for most of DCCs */
|
||||
icon-size: 16px;
|
||||
}
|
||||
|
||||
QWidget {
|
||||
|
|
@ -1168,6 +1171,8 @@ ValidationArtistMessage QLabel {
|
|||
|
||||
#PublishLogMessage {
|
||||
font-family: "Noto Sans Mono";
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#PublishInstanceLogsLabel {
|
||||
|
|
|
|||
|
|
@ -1117,6 +1117,57 @@ class LogIconFrame(QtWidgets.QFrame):
|
|||
painter.end()
|
||||
|
||||
|
||||
class LogItemMessage(QtWidgets.QTextEdit):
|
||||
def __init__(self, msg, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
# Set as plain text to propagate new line characters
|
||||
self.setPlainText(msg)
|
||||
|
||||
self.setObjectName("PublishLogMessage")
|
||||
self.setReadOnly(True)
|
||||
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
|
||||
self.setLineWidth(0)
|
||||
self.setMidLineWidth(0)
|
||||
pal = self.palette()
|
||||
pal.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent)
|
||||
self.setPalette(pal)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
viewport = self.viewport()
|
||||
viewport.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction)
|
||||
self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
self.setLineWrapMode(QtWidgets.QTextEdit.WidgetWidth)
|
||||
self.setWordWrapMode(
|
||||
QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere
|
||||
)
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Preferred,
|
||||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
document = self.document()
|
||||
document.documentLayout().documentSizeChanged.connect(
|
||||
self._adjust_minimum_size
|
||||
)
|
||||
document.setDocumentMargin(0.0)
|
||||
self._height = None
|
||||
|
||||
def _adjust_minimum_size(self, size):
|
||||
self._height = size.height() + (2 * self.frameWidth())
|
||||
self.updateGeometry()
|
||||
|
||||
def sizeHint(self):
|
||||
size = super().sizeHint()
|
||||
if self._height is not None:
|
||||
size.setHeight(self._height)
|
||||
return size
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
|
||||
class LogItemWidget(QtWidgets.QWidget):
|
||||
log_level_to_flag = {
|
||||
10: LOG_DEBUG_VISIBLE,
|
||||
|
|
@ -1132,12 +1183,7 @@ class LogItemWidget(QtWidgets.QWidget):
|
|||
type_flag, level_n = self._get_log_info(log)
|
||||
icon_label = LogIconFrame(
|
||||
self, log["type"], level_n, log.get("is_validation_error"))
|
||||
message_label = QtWidgets.QLabel(log["msg"].rstrip(), self)
|
||||
message_label.setObjectName("PublishLogMessage")
|
||||
message_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction)
|
||||
message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
message_label.setWordWrap(True)
|
||||
message_label = LogItemMessage(log["msg"].rstrip(), self)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -1290,6 +1336,7 @@ class InstanceLogsWidget(QtWidgets.QWidget):
|
|||
|
||||
label_widget = QtWidgets.QLabel(instance.label, self)
|
||||
label_widget.setObjectName("PublishInstanceLogsLabel")
|
||||
label_widget.setWordWrap(True)
|
||||
logs_grid = LogsWithIconsView(instance.logs, self)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -1329,9 +1376,11 @@ class InstancesLogsView(QtWidgets.QFrame):
|
|||
|
||||
content_wrap_widget = QtWidgets.QWidget(scroll_area)
|
||||
content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
content_wrap_widget.setMinimumWidth(80)
|
||||
|
||||
content_widget = QtWidgets.QWidget(content_wrap_widget)
|
||||
content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(8, 8, 8, 8)
|
||||
content_layout.setSpacing(15)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def find_free_port(
|
|||
exclude_ports (list, tuple, set): List of ports that won't be
|
||||
checked form entered range.
|
||||
host (str): Host where will check for free ports. Set to
|
||||
"localhost" by default.
|
||||
"127.0.0.1" by default.
|
||||
"""
|
||||
if port_from is None:
|
||||
port_from = 8079
|
||||
|
|
@ -42,7 +42,7 @@ def find_free_port(
|
|||
|
||||
# Default host is localhost but it is possible to look for other hosts
|
||||
if host is None:
|
||||
host = "localhost"
|
||||
host = "127.0.0.1"
|
||||
|
||||
found_port = None
|
||||
while True:
|
||||
|
|
@ -78,7 +78,7 @@ class WebServerManager:
|
|||
self._log = None
|
||||
|
||||
self.port = port or 8079
|
||||
self.host = host or "localhost"
|
||||
self.host = host or "127.0.0.1"
|
||||
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from ayon_core.resources import get_image_path
|
||||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui, QtSvg
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
|
|
@ -19,48 +18,6 @@ class DeselectableTreeView(QtWidgets.QTreeView):
|
|||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class TreeViewSpinner(QtWidgets.QTreeView):
|
||||
size = 160
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeViewSpinner, self).__init__(parent=parent)
|
||||
|
||||
loading_image_path = get_image_path("spinner-200.svg")
|
||||
|
||||
self.spinner = QtSvg.QSvgRenderer(loading_image_path)
|
||||
|
||||
self.is_loading = False
|
||||
self.is_empty = True
|
||||
|
||||
def paint_loading(self, event):
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
rect.moveTo(
|
||||
rect.x() + rect.width() / 2 - self.size / 2,
|
||||
rect.y() + rect.height() / 2 - self.size / 2
|
||||
)
|
||||
rect.setSize(QtCore.QSizeF(self.size, self.size))
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
self.spinner.render(painter, rect)
|
||||
|
||||
def paint_empty(self, event):
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
qtext_opt = QtGui.QTextOption(
|
||||
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
painter.drawText(rect, "No Data", qtext_opt)
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self.is_loading:
|
||||
self.paint_loading(event)
|
||||
elif self.is_empty:
|
||||
self.paint_empty(event)
|
||||
else:
|
||||
super(TreeViewSpinner, self).paintEvent(event)
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
"""Ultimate TreeView with flick charm and double click signals.
|
||||
|
||||
|
|
|
|||
|
|
@ -184,9 +184,10 @@ class WorkareaModel:
|
|||
return items
|
||||
|
||||
for filename in os.listdir(workdir):
|
||||
# We want to support both files and folders. e.g. Silhoutte uses
|
||||
# folders as its project files. So we do not check whether it is
|
||||
# a file or not.
|
||||
filepath = os.path.join(workdir, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in self._extensions:
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
# Initial setup
|
||||
workarea_btn_open.setEnabled(False)
|
||||
workarea_btn_browse.setEnabled(False)
|
||||
workarea_btn_save.setEnabled(False)
|
||||
published_btn_copy_n_open.setEnabled(False)
|
||||
published_btn_change_context.setEnabled(False)
|
||||
published_btn_cancel.setVisible(False)
|
||||
|
|
@ -278,8 +280,9 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._published_btn_change_context.setEnabled(enabled)
|
||||
|
||||
def _update_workarea_btns_state(self):
|
||||
enabled = self._is_save_enabled
|
||||
enabled = self._is_save_enabled and self._valid_selected_context
|
||||
self._workarea_btn_save.setEnabled(enabled)
|
||||
self._workarea_btn_browse.setEnabled(self._valid_selected_context)
|
||||
|
||||
def _on_published_repre_changed(self, event):
|
||||
self._valid_representation_id = event["representation_id"] is not None
|
||||
|
|
@ -294,6 +297,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
and self._selected_task_id is not None
|
||||
)
|
||||
self._update_published_btns_state()
|
||||
self._update_workarea_btns_state()
|
||||
|
||||
def _on_published_save_clicked(self):
|
||||
result = self._exec_save_as_dialog()
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.addWidget(pages_widget, 1)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
overlay_messages_widget = MessageOverlayObject(self)
|
||||
overlay_invalid_host = InvalidHostOverlay(self)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.0.12+dev"
|
||||
__version__ = "1.1.0+dev"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue