Merge branch 'develop' into enhancement/OP-5808_Enhancement-3dsmax-rendering-time-data-from-instance

This commit is contained in:
Ondřej Samohel 2023-05-15 18:22:30 +02:00 committed by GitHub
commit f90dea5e26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 264 additions and 162 deletions

View file

@ -35,6 +35,7 @@ body:
label: Version label: Version
description: What version are you running? Look to OpenPype Tray description: What version are you running? Look to OpenPype Tray
options: options:
- 3.15.7-nightly.3
- 3.15.7-nightly.2 - 3.15.7-nightly.2
- 3.15.7-nightly.1 - 3.15.7-nightly.1
- 3.15.6 - 3.15.6
@ -134,7 +135,6 @@ body:
- 3.14.1-nightly.4 - 3.14.1-nightly.4
- 3.14.1-nightly.3 - 3.14.1-nightly.3
- 3.14.1-nightly.2 - 3.14.1-nightly.2
- 3.14.1-nightly.1
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

6
.gitignore vendored
View file

@ -112,3 +112,9 @@ tools/run_eventserver.*
tools/dev_* tools/dev_*
.github_changelog_generator .github_changelog_generator
# Addons
########
/openpype/addons/*
!/openpype/addons/README.md

View file

@ -0,0 +1,3 @@
This directory is for storing external addons that needs to be included in the pipeline when distributed.
The directory is ignored by Git, but included in the zip and installation files.

View file

@ -1,29 +1,39 @@
import pyblish.api import pyblish.api
from openpype.pipeline import OptionalPyblishPluginMixin
from openpype.pipeline import KnownPublishError
class FusionIncrementCurrentFile(pyblish.api.ContextPlugin):
class FusionIncrementCurrentFile(
pyblish.api.ContextPlugin, OptionalPyblishPluginMixin
):
"""Increment the current file. """Increment the current file.
Saves the current file with an increased version number. Saves the current file with an increased version number.
""" """
label = "Increment current file" label = "Increment workfile version"
order = pyblish.api.IntegratorOrder + 9.0 order = pyblish.api.IntegratorOrder + 9.0
hosts = ["fusion"] hosts = ["fusion"]
families = ["workfile"]
optional = True optional = True
def process(self, context): def process(self, context):
if not self.is_active(context.data):
return
from openpype.lib import version_up from openpype.lib import version_up
from openpype.pipeline.publish import get_errored_plugins_from_context from openpype.pipeline.publish import get_errored_plugins_from_context
errored_plugins = get_errored_plugins_from_context(context) errored_plugins = get_errored_plugins_from_context(context)
if any(plugin.__name__ == "FusionSubmitDeadline" if any(
for plugin in errored_plugins): plugin.__name__ == "FusionSubmitDeadline"
raise RuntimeError("Skipping incrementing current file because " for plugin in errored_plugins
"submission to render farm failed.") ):
raise KnownPublishError(
"Skipping incrementing current file because "
"submission to render farm failed."
)
comp = context.data.get("currentComp") comp = context.data.get("currentComp")
assert comp, "Must have comp" assert comp, "Must have comp"

View file

@ -1,12 +1,17 @@
import pyblish.api import pyblish.api
from openpype.pipeline.publish import RepairAction from openpype.pipeline import (
from openpype.pipeline import PublishValidationError publish,
OptionalPyblishPluginMixin,
PublishValidationError,
)
from openpype.hosts.fusion.api.action import SelectInvalidAction from openpype.hosts.fusion.api.action import SelectInvalidAction
class ValidateBackgroundDepth(pyblish.api.InstancePlugin): class ValidateBackgroundDepth(
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
):
"""Validate if all Background tool are set to float32 bit""" """Validate if all Background tool are set to float32 bit"""
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
@ -15,11 +20,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
families = ["render"] families = ["render"]
optional = True optional = True
actions = [SelectInvalidAction, RepairAction] actions = [SelectInvalidAction, publish.RepairAction]
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):
context = instance.context context = instance.context
comp = context.data.get("currentComp") comp = context.data.get("currentComp")
assert comp, "Must have Comp object" assert comp, "Must have Comp object"
@ -31,12 +35,16 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
return [i for i in backgrounds if i.GetInput("Depth") != 4.0] return [i for i in backgrounds if i.GetInput("Depth") != 4.0]
def process(self, instance): def process(self, instance):
if not self.is_active(instance.data):
return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError( raise PublishValidationError(
"Found {} Backgrounds tools which" "Found {} Backgrounds tools which"
" are not set to float32".format(len(invalid)), " are not set to float32".format(len(invalid)),
title=self.label) title=self.label,
)
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -81,7 +81,13 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
# TODO: make sure this doesn't trigger when # TODO: make sure this doesn't trigger when
# opening with last workfile. # opening with last workfile.
_set_context_settings() _set_context_settings()
shelves.generate_shelves()
if not IS_HEADLESS:
import hdefereval # noqa, hdefereval is only available in ui mode
# Defer generation of shelves due to issue on Windows where shelf
# initialization during start up delays Houdini UI by minutes
# making it extremely slow to launch.
hdefereval.executeDeferred(shelves.generate_shelves)
if not IS_HEADLESS: if not IS_HEADLESS:
import hdefereval # noqa, hdefereval is only available in ui mode import hdefereval # noqa, hdefereval is only available in ui mode

View file

@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int):
None None
""" """
# make sure the render dialog is closed
# for the update of resolution
# Changing the Render Setup dialog settingsshould be done
# with the actual Render Setup dialog in a closed state.
if rt.renderSceneDialog.isOpen():
rt.renderSceneDialog.close()
rt.renderWidth = width rt.renderWidth = width
rt.renderHeight = height rt.renderHeight = height
def reset_scene_resolution(): def reset_scene_resolution():
"""Apply the scene resolution from the project definition """Apply the scene resolution from the project definition

View file

@ -105,6 +105,9 @@ class RenderSettings(object):
rt.rendSaveFile = True rt.rendSaveFile = True
if rt.renderSceneDialog.isOpen():
rt.renderSceneDialog.close()
def arnold_setup(self): def arnold_setup(self):
# get Arnold RenderView run in the background # get Arnold RenderView run in the background
# for setting up renderable camera # for setting up renderable camera

View file

@ -52,6 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
def context_setting(): def context_setting():
return lib.set_context_setting() return lib.set_context_setting()
rt.callbacks.addScript(rt.Name('systemPostNew'), rt.callbacks.addScript(rt.Name('systemPostNew'),
context_setting) context_setting)

View file

@ -27,6 +27,11 @@ class CreateRender(plugin.MaxCreator):
# for additional work on the node: # for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node")) # instance_node = rt.getNodeByName(instance.get("instance_node"))
# make sure the render dialog is closed
# for the update of resolution
# Changing the Render Setup dialog settings should be done
# with the actual Render Setup dialog in a closed state.
# set viewport camera for rendering(mandatory for deadline) # set viewport camera for rendering(mandatory for deadline)
RenderSettings().set_render_camera(sel_obj) RenderSettings().set_render_camera(sel_obj)
# set output paths for rendering(mandatory for deadline) # set output paths for rendering(mandatory for deadline)

View file

@ -0,0 +1,65 @@
import pyblish.api
from openpype.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api.lib import reset_scene_resolution
from openpype.pipeline.context_tools import (
get_current_project_asset,
get_current_project
)
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate the resolution setting aligned with DB"""
order = pyblish.api.ValidatorOrder - 0.01
families = ["maxrender"]
hosts = ["max"]
label = "Validate Resolution Setting"
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
width, height = self.get_db_resolution(instance)
current_width = rt.renderwidth
current_height = rt.renderHeight
if current_width != width and current_height != height:
raise PublishValidationError("Resolution Setting "
"not matching resolution "
"set on asset or shot.")
if current_width != width:
raise PublishValidationError("Width in Resolution Setting "
"not matching resolution set "
"on asset or shot.")
if current_height != height:
raise PublishValidationError("Height in Resolution Setting "
"not matching resolution set "
"on asset or shot.")
def get_db_resolution(self, instance):
data = ["data.resolutionWidth", "data.resolutionHeight"]
project_resolution = get_current_project(fields=data)
project_resolution_data = project_resolution["data"]
asset_resolution = get_current_project_asset(fields=data)
asset_resolution_data = asset_resolution["data"]
# Set project resolution
project_width = int(
project_resolution_data.get("resolutionWidth", 1920))
project_height = int(
project_resolution_data.get("resolutionHeight", 1080))
width = int(
asset_resolution_data.get("resolutionWidth", project_width))
height = int(
asset_resolution_data.get("resolutionHeight", project_height))
return width, height
@classmethod
def repair(cls, instance):
reset_scene_resolution()

View file

@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
asset_name = instance.data.get("asset", None) asset_name = instance.data.get("asset", None)
# Check the number of connected shadingEngines per shape # Check the number of connected shadingEngines per shape
r = re.compile(cls.regex) regex_compile = re.compile(cls.regex)
error_message = "object {0} has invalid shader name {1}"
for shape in shapes: for shape in shapes:
shading_engines = cmds.listConnections(shape, shading_engines = cmds.listConnections(shape,
destination=True, destination=True,
@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
) )
for shader in shaders: for shader in shaders:
m = r.match(cls.regex, shader) m = regex_compile.match(shader)
if m is None: if m is None:
invalid.append(shape) invalid.append(shape)
cls.log.error( cls.log.error(error_message.format(shape, shader))
"object {0} has invalid shader name {1}".format(shape,
shader)
)
else: else:
if 'asset' in r.groupindex: if 'asset' in regex_compile.groupindex:
if m.group('asset') != asset_name: if m.group('asset') != asset_name:
invalid.append(shape) invalid.append(shape)
cls.log.error(("object {0} has invalid " message = error_message
"shader name {1}").format(shape, message += " with missing asset name \"{2}\""
shader)) cls.log.error(
message.format(shape, shader, asset_name)
)
return invalid return invalid

View file

@ -23,9 +23,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon):
UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon"
) )
if not Path(unreal_plugin_path).exists(): if not Path(unreal_plugin_path).exists():
if compatible_versions := get_compatible_integration( compatible_versions = get_compatible_integration(
ue_version, Path(UNREAL_ROOT_DIR) / "integration" ue_version, Path(UNREAL_ROOT_DIR) / "integration"
): )
if compatible_versions:
unreal_plugin_path = compatible_versions[-1] / "Ayon" unreal_plugin_path = compatible_versions[-1] / "Ayon"
unreal_plugin_path = unreal_plugin_path.as_posix() unreal_plugin_path = unreal_plugin_path.as_posix()

View file

@ -135,7 +135,7 @@ class StaticMeshAlembicLoader(plugin.Loader):
source_path = get_representation_path(representation) source_path = get_representation_path(representation)
destination_path = container["namespace"] destination_path = container["namespace"]
task = self.get_task(source_path, destination_path, name, True) task = self.get_task(source_path, destination_path, name, True, False)
# do import fbx and replace existing data # do import fbx and replace existing data
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

View file

@ -73,8 +73,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin):
new_data["level"] = data.get("level") new_data["level"] = data.get("level")
new_data["output"] = s.get('output') new_data["output"] = s.get('output')
new_data["fps"] = seq.get_display_rate().numerator new_data["fps"] = seq.get_display_rate().numerator
new_data["frameStart"] = s.get('frame_range')[0] new_data["frameStart"] = int(s.get('frame_range')[0])
new_data["frameEnd"] = s.get('frame_range')[1] new_data["frameEnd"] = int(s.get('frame_range')[1])
new_data["sequence"] = seq.get_path_name() new_data["sequence"] = seq.get_path_name()
new_data["master_sequence"] = data["master_sequence"] new_data["master_sequence"] = data["master_sequence"]
new_data["master_level"] = data["master_level"] new_data["master_level"] = data["master_level"]

View file

@ -1,48 +0,0 @@
from pathlib import Path
import unreal
from openpype.pipeline import publish
class ExtractRender(publish.Extractor):
"""Extract render."""
label = "Extract Render"
hosts = ["unreal"]
families = ["render"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
# Perform extraction
self.log.info("Performing extraction..")
# Get the render output directory
project_dir = unreal.Paths.project_dir()
render_dir = (f"{project_dir}/Saved/MovieRenders/"
f"{instance.data['subset']}")
assert unreal.Paths.directory_exists(render_dir), \
"Render directory does not exist"
render_path = Path(render_dir)
frames = []
for x in render_path.iterdir():
if x.is_file() and x.suffix == '.png':
frames.append(str(x))
if "representations" not in instance.data:
instance.data["representations"] = []
render_representation = {
'name': 'png',
'ext': 'png',
'files': frames,
"stagingDir": stagingdir,
}
instance.data["representations"].append(render_representation)

View file

@ -311,6 +311,7 @@ def _load_modules():
# Look for OpenPype modules in paths defined with `get_module_dirs` # Look for OpenPype modules in paths defined with `get_module_dirs`
# - dynamically imported OpenPype modules and addons # - dynamically imported OpenPype modules and addons
module_dirs = get_module_dirs() module_dirs = get_module_dirs()
# Add current directory at first place # Add current directory at first place
# - has small differences in import logic # - has small differences in import logic
current_dir = os.path.abspath(os.path.dirname(__file__)) current_dir = os.path.abspath(os.path.dirname(__file__))
@ -318,8 +319,11 @@ def _load_modules():
module_dirs.insert(0, hosts_dir) module_dirs.insert(0, hosts_dir)
module_dirs.insert(0, current_dir) module_dirs.insert(0, current_dir)
addons_dir = os.path.join(os.path.dirname(current_dir), "addons")
module_dirs.append(addons_dir)
processed_paths = set() processed_paths = set()
for dirpath in module_dirs: for dirpath in frozenset(module_dirs):
# Skip already processed paths # Skip already processed paths
if dirpath in processed_paths: if dirpath in processed_paths:
continue continue

View file

@ -378,7 +378,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
existing_tasks.append(task_name_low) existing_tasks.append(task_name_low)
for instance in instances_by_task_name[task_name_low]: for instance in instances_by_task_name[task_name_low]:
instance["ftrackTask"] = child instance.data["ftrackTask"] = child
for task_name in tasks: for task_name in tasks:
task_type = tasks[task_name]["type"] task_type = tasks[task_name]["type"]

View file

@ -9,7 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder order = pyblish.api.IntegratorOrder
label = "Kitsu Note and Status" label = "Kitsu Note and Status"
families = ["render", "kitsu"] families = ["render", "image", "online", "plate", "kitsu"]
# status settings # status settings
set_status_note = False set_status_note = False
@ -52,8 +52,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
for instance in context: for instance in context:
# Check if instance is a review by checking its family # Check if instance is a review by checking its family
# Allow a match to primary family or any of families # Allow a match to primary family or any of families
families = set([instance.data["family"]] + families = set(
instance.data.get("families", [])) [instance.data["family"]] + instance.data.get("families", [])
)
if "review" not in families: if "review" not in families:
continue continue

View file

@ -8,11 +8,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin):
order = pyblish.api.IntegratorOrder + 0.01 order = pyblish.api.IntegratorOrder + 0.01
label = "Kitsu Review" label = "Kitsu Review"
families = ["render", "kitsu"] families = ["render", "image", "online", "plate", "kitsu"]
optional = True optional = True
def process(self, instance): def process(self, instance):
# Check comment has been created # Check comment has been created
comment_id = instance.data.get("kitsu_comment", {}).get("id") comment_id = instance.data.get("kitsu_comment", {}).get("id")
if not comment_id: if not comment_id:

View file

@ -35,6 +35,7 @@ from . import (
register_inventory_action_path, register_inventory_action_path,
register_creator_plugin_path, register_creator_plugin_path,
deregister_loader_plugin_path, deregister_loader_plugin_path,
deregister_inventory_action_path,
) )
@ -54,6 +55,7 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
# Global plugin paths # Global plugin paths
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load") LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
def _get_modules_manager(): def _get_modules_manager():
@ -158,6 +160,7 @@ def install_openpype_plugins(project_name=None, host_name=None):
pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_discovery_filter(filter_pyblish_plugins) pyblish.api.register_discovery_filter(filter_pyblish_plugins)
register_loader_plugin_path(LOAD_PATH) register_loader_plugin_path(LOAD_PATH)
register_inventory_action_path(INVENTORY_PATH)
if host_name is None: if host_name is None:
host_name = os.environ.get("AVALON_APP") host_name = os.environ.get("AVALON_APP")
@ -223,6 +226,7 @@ def uninstall_host():
pyblish.api.deregister_plugin_path(PUBLISH_PATH) pyblish.api.deregister_plugin_path(PUBLISH_PATH)
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
deregister_loader_plugin_path(LOAD_PATH) deregister_loader_plugin_path(LOAD_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
log.info("Global plug-ins unregistred") log.info("Global plug-ins unregistred")
deregister_host() deregister_host()

View file

@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import (
get_default_values, get_default_values,
) )
from openpype.host import IPublishHost, IWorkfileHost from openpype.host import IPublishHost, IWorkfileHost
from openpype.pipeline import legacy_io from openpype.pipeline import legacy_io, Anatomy
from openpype.pipeline.plugin_discover import DiscoverResult from openpype.pipeline.plugin_discover import DiscoverResult
from .creator_plugins import ( from .creator_plugins import (
@ -1383,6 +1383,8 @@ class CreateContext:
self._current_task_name = None self._current_task_name = None
self._current_workfile_path = None self._current_workfile_path = None
self._current_project_anatomy = None
self._host_is_valid = host_is_valid self._host_is_valid = host_is_valid
# Currently unused variable # Currently unused variable
self.headless = headless self.headless = headless
@ -1546,6 +1548,18 @@ class CreateContext:
return self._current_workfile_path return self._current_workfile_path
def get_current_project_anatomy(self):
"""Project anatomy for current project.
Returns:
Anatomy: Anatomy object ready to be used.
"""
if self._current_project_anatomy is None:
self._current_project_anatomy = Anatomy(
self._current_project_name)
return self._current_project_anatomy
@property @property
def context_has_changed(self): def context_has_changed(self):
"""Host context has changed. """Host context has changed.
@ -1568,6 +1582,7 @@ class CreateContext:
) )
project_name = property(get_current_project_name) project_name = property(get_current_project_name)
project_anatomy = property(get_current_project_anatomy)
@property @property
def log(self): def log(self):
@ -1680,6 +1695,8 @@ class CreateContext:
self._current_task_name = task_name self._current_task_name = task_name
self._current_workfile_path = workfile_path self._current_workfile_path = workfile_path
self._current_project_anatomy = None
def reset_plugins(self, discover_publish_plugins=True): def reset_plugins(self, discover_publish_plugins=True):
"""Reload plugins. """Reload plugins.

View file

@ -231,10 +231,24 @@ class BaseCreator:
@property @property
def project_name(self): def project_name(self):
"""Family that plugin represents.""" """Current project name.
Returns:
str: Name of a project.
"""
return self.create_context.project_name return self.create_context.project_name
@property
def project_anatomy(self):
"""Current project anatomy.
Returns:
Anatomy: Project anatomy object.
"""
return self.create_context.project_anatomy
@property @property
def host(self): def host(self):
return self.create_context.host return self.create_context.host

View file

@ -186,7 +186,7 @@ class BuildWorkfile:
if link_context_profiles: if link_context_profiles:
# Find and append linked assets if preset has set linked mapping # Find and append linked assets if preset has set linked mapping
link_assets = get_linked_assets(current_asset_entity) link_assets = get_linked_assets(project_name, current_asset_entity)
if link_assets: if link_assets:
assets.extend(link_assets) assets.extend(link_assets)

View file

@ -0,0 +1,51 @@
from openpype.pipeline import InventoryAction
from openpype.pipeline import get_current_project_name
from openpype.pipeline.load.plugins import discover_loader_plugins
from openpype.pipeline.load.utils import (
get_loader_identifier,
remove_container,
load_container,
)
from openpype.client import get_representation_by_id
class RemoveAndLoad(InventoryAction):
"""Delete inventory item and reload it."""
label = "Remove and load"
icon = "refresh"
def process(self, containers):
project_name = get_current_project_name()
loaders_by_name = {
get_loader_identifier(plugin): plugin
for plugin in discover_loader_plugins(project_name=project_name)
}
for container in containers:
# Get loader
loader_name = container["loader"]
loader = loaders_by_name.get(loader_name, None)
if not loader:
raise RuntimeError(
"Failed to get loader '{}', can't remove "
"and load container".format(loader_name)
)
# Get representation
representation = get_representation_by_id(
project_name, container["representation"]
)
if not representation:
self.log.warning(
"Skipping remove and load because representation id is not"
" found in database: '{}'".format(
container["representation"]
)
)
continue
# Remove container
remove_container(container)
# Load container
load_container(loader, representation)

View file

@ -1,66 +0,0 @@
import os
import re
import clique
import pyblish.api
class ValidateSequenceFrames(pyblish.api.InstancePlugin):
"""Ensure the sequence of frames is complete
The files found in the folder are checked against the startFrame and
endFrame of the instance. If the first or last file is not
corresponding with the first or last frame it is flagged as invalid.
Used regular expression pattern handles numbers in the file names
(eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr",
"Main_beauty.1001.1001.exr") but not numbers behind frames (eg.
"Main_beauty.1001.v001.exr")
"""
order = pyblish.api.ValidatorOrder
label = "Validate Sequence Frames"
families = ["imagesequence", "render"]
hosts = ["shell", "unreal"]
def process(self, instance):
representations = instance.data.get("representations")
if not representations:
return
for repr in representations:
repr_files = repr["files"]
if isinstance(repr_files, str):
continue
ext = repr.get("ext")
if not ext:
_, ext = os.path.splitext(repr_files[0])
elif not ext.startswith("."):
ext = ".{}".format(ext)
pattern = r"\D?(?P<index>(?P<padding>0*)\d+){}$".format(
re.escape(ext))
patterns = [pattern]
collections, remainder = clique.assemble(
repr_files, minimum_items=1, patterns=patterns)
assert not remainder, "Must not have remainder"
assert len(collections) == 1, "Must detect single collection"
collection = collections[0]
frames = list(collection.indexes)
if instance.data.get("slate"):
# Slate is not part of the frame range
frames = frames[1:]
current_range = (frames[0], frames[-1])
required_range = (instance.data["frameStart"],
instance.data["frameEnd"])
if current_range != required_range:
raise ValueError(f"Invalid frame range: {current_range} - "
f"expected: {required_range}")
missing = collection.holes().indexes
assert not missing, "Missing frames: %s" % (missing,)

View file

@ -82,7 +82,8 @@
"png": { "png": {
"ext": "png", "ext": "png",
"tags": [ "tags": [
"ftrackreview" "ftrackreview",
"kitsureview"
], ],
"burnins": [], "burnins": [],
"ffmpeg_args": { "ffmpeg_args": {

View file

@ -734,6 +734,7 @@
"ValidateShaderName": { "ValidateShaderName": {
"enabled": false, "enabled": false,
"optional": true, "optional": true,
"active": true,
"regex": "(?P<asset>.*)_(.*)_SHD" "regex": "(?P<asset>.*)_(.*)_SHD"
}, },
"ValidateShadingEngine": { "ValidateShadingEngine": {

View file

@ -126,6 +126,11 @@
"key": "optional", "key": "optional",
"label": "Optional" "label": "Optional"
}, },
{
"type": "boolean",
"key": "active",
"label": "Active"
},
{ {
"type": "label", "type": "label",
"label": "Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=&lt;asset&gt;.+)_SHD</code></p>" "label": "Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=&lt;asset&gt;.+)_SHD</code></p>"

View file

@ -19,9 +19,9 @@ class ScriptsMenu(QtWidgets.QMenu):
Args: Args:
title (str): the name of the root menu which will be created title (str): the name of the root menu which will be created
parent (QtWidgets.QObject) : the QObject to parent the menu to parent (QtWidgets.QObject) : the QObject to parent the menu to
Returns: Returns:
None None
@ -94,7 +94,7 @@ class ScriptsMenu(QtWidgets.QMenu):
parent(QtWidgets.QWidget): the object to parent the menu to parent(QtWidgets.QWidget): the object to parent the menu to
title(str): the title of the menu title(str): the title of the menu
Returns: Returns:
QtWidget.QMenu instance QtWidget.QMenu instance
""" """
@ -111,7 +111,7 @@ class ScriptsMenu(QtWidgets.QMenu):
return menu return menu
def add_script(self, parent, title, command, sourcetype, icon=None, def add_script(self, parent, title, command, sourcetype, icon=None,
tags=None, label=None, tooltip=None): tags=None, label=None, tooltip=None, shortcut=None):
"""Create an action item which runs a script when clicked """Create an action item which runs a script when clicked
Args: Args:
@ -134,6 +134,8 @@ class ScriptsMenu(QtWidgets.QMenu):
tooltip (str): A tip for the user about the usage fo the tool tooltip (str): A tip for the user about the usage fo the tool
shortcut (str): A shortcut to run the command
Returns: Returns:
QtWidget.QAction instance QtWidget.QAction instance
@ -166,6 +168,9 @@ class ScriptsMenu(QtWidgets.QMenu):
raise RuntimeError("Script action can't be " raise RuntimeError("Script action can't be "
"processed: {}".format(e)) "processed: {}".format(e))
if shortcut:
script_action.setShortcut(shortcut)
if icon: if icon:
iconfile = os.path.expandvars(icon) iconfile = os.path.expandvars(icon)
script_action.iconfile = iconfile script_action.iconfile = iconfile
@ -253,7 +258,7 @@ class ScriptsMenu(QtWidgets.QMenu):
def _update_search(self, search): def _update_search(self, search):
"""Hide all the samples which do not match the user's import """Hide all the samples which do not match the user's import
Returns: Returns:
None None

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring Pype version.""" """Package declaring Pype version."""
__version__ = "3.15.7-nightly.2" __version__ = "3.15.7-nightly.3"