[Automated] Merged develop into main

This commit is contained in:
ynbot 2023-05-16 10:44:09 +02:00 committed by GitHub
commit 74b1ff580a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 432 additions and 149 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

View file

@ -1,3 +1,4 @@
from copy import deepcopy
import os import os
from openpype.hosts.fusion.api import ( from openpype.hosts.fusion.api import (
@ -11,15 +12,13 @@ from openpype.lib import (
) )
from openpype.pipeline import ( from openpype.pipeline import (
legacy_io, legacy_io,
Creator, Creator as NewCreator,
CreatedInstance, CreatedInstance,
) Anatomy
from openpype.client import (
get_asset_by_name,
) )
class CreateSaver(Creator): class CreateSaver(NewCreator):
identifier = "io.openpype.creators.fusion.saver" identifier = "io.openpype.creators.fusion.saver"
label = "Render (saver)" label = "Render (saver)"
name = "render" name = "render"
@ -28,9 +27,24 @@ class CreateSaver(Creator):
description = "Fusion Saver to generate image sequence" description = "Fusion Saver to generate image sequence"
icon = "fa5.eye" icon = "fa5.eye"
instance_attributes = ["reviewable"] instance_attributes = [
"reviewable"
]
default_variants = [
"Main",
"Mask"
]
# TODO: This should be renamed together with Nuke so it is aligned
temp_rendering_path_template = (
"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}")
def create(self, subset_name, instance_data, pre_create_data): def create(self, subset_name, instance_data, pre_create_data):
instance_data.update({
"id": "pyblish.avalon.instance",
"subset": subset_name
})
# TODO: Add pre_create attributes to choose file format? # TODO: Add pre_create attributes to choose file format?
file_format = "OpenEXRFormat" file_format = "OpenEXRFormat"
@ -39,7 +53,6 @@ class CreateSaver(Creator):
args = (-32768, -32768) # Magical position numbers args = (-32768, -32768) # Magical position numbers
saver = comp.AddTool("Saver", *args) saver = comp.AddTool("Saver", *args)
instance_data["subset"] = subset_name
self._update_tool_with_data(saver, data=instance_data) self._update_tool_with_data(saver, data=instance_data)
saver["OutputFormat"] = file_format saver["OutputFormat"] = file_format
@ -78,7 +91,7 @@ class CreateSaver(Creator):
for tool in tools: for tool in tools:
data = self.get_managed_tool_data(tool) data = self.get_managed_tool_data(tool)
if not data: if not data:
data = self._collect_unmanaged_saver(tool) continue
# Add instance # Add instance
created_instance = CreatedInstance.from_existing(data, self) created_instance = CreatedInstance.from_existing(data, self)
@ -125,60 +138,35 @@ class CreateSaver(Creator):
original_subset = tool.GetData("openpype.subset") original_subset = tool.GetData("openpype.subset")
subset = data["subset"] subset = data["subset"]
if original_subset != subset: if original_subset != subset:
# Subset change detected self._configure_saver_tool(data, tool, subset)
# Update output filepath
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
filename = f"{subset}..exr"
filepath = os.path.join(workdir, "render", subset, filename)
tool["Clip"] = filepath
# Rename tool def _configure_saver_tool(self, data, tool, subset):
if tool.Name != subset: formatting_data = deepcopy(data)
print(f"Renaming {tool.Name} -> {subset}")
tool.SetAttrs({"TOOLS_Name": subset})
def _collect_unmanaged_saver(self, tool): # get frame padding from anatomy templates
# TODO: this should not be done this way - this should actually anatomy = Anatomy()
# get the data as stored on the tool explicitly (however) frame_padding = int(
# that would disallow any 'regular saver' to be collected anatomy.templates["render"].get("frame_padding", 4)
# unless the instance data is stored on it to begin with
print("Collecting unmanaged saver..")
comp = tool.Comp()
# Allow regular non-managed savers to also be picked up
project = legacy_io.Session["AVALON_PROJECT"]
asset = legacy_io.Session["AVALON_ASSET"]
task = legacy_io.Session["AVALON_TASK"]
asset_doc = get_asset_by_name(project_name=project, asset_name=asset)
path = tool["Clip"][comp.TIME_UNDEFINED]
fname = os.path.basename(path)
fname, _ext = os.path.splitext(fname)
variant = fname.rstrip(".")
subset = self.get_subset_name(
variant=variant,
task_name=task,
asset_doc=asset_doc,
project_name=project,
) )
attrs = tool.GetAttrs() # Subset change detected
passthrough = attrs["TOOLB_PassThrough"] workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
return { formatting_data.update({
# Required data "workdir": workdir,
"project": project, "frame": "0" * frame_padding,
"asset": asset, "ext": "exr"
"subset": subset, })
"task": task,
"variant": variant, # build file path to render
"active": not passthrough, filepath = self.temp_rendering_path_template.format(
"family": self.family, **formatting_data)
# Unique identifier for instance and this creator
"id": "pyblish.avalon.instance", tool["Clip"] = os.path.normpath(filepath)
"creator_identifier": self.identifier,
} # Rename tool
if tool.Name != subset:
print(f"Renaming {tool.Name} -> {subset}")
tool.SetAttrs({"TOOLS_Name": subset})
def get_managed_tool_data(self, tool): def get_managed_tool_data(self, tool):
"""Return data of the tool if it matches creator identifier""" """Return data of the tool if it matches creator identifier"""
@ -238,3 +226,25 @@ class CreateSaver(Creator):
default=("reviewable" in self.instance_attributes), default=("reviewable" in self.instance_attributes),
label="Review", label="Review",
) )
def apply_settings(
self,
project_settings,
system_settings
):
"""Method called on initialization of plugin to apply settings."""
# plugin settings
plugin_settings = (
project_settings["fusion"]["create"][self.__class__.__name__]
)
# individual attributes
self.instance_attributes = plugin_settings.get(
"instance_attributes") or self.instance_attributes
self.default_variants = plugin_settings.get(
"default_variants") or self.default_variants
self.temp_rendering_path_template = (
plugin_settings.get("temp_rendering_path_template")
or self.temp_rendering_path_template
)

View file

@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None):
["default_render_image_folder"]) ["default_render_image_folder"])
def set_framerange(start_frame, end_frame): def set_render_frame_range(start_frame, end_frame):
""" """
Note: Note:
Frame range can be specified in different types. Possible values are: Frame range can be specified in different types. Possible values are:
@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame):
Todo: Todo:
Current type is hard-coded, there should be a custom setting for this. Current type is hard-coded, there should be a custom setting for this.
""" """
rt.rendTimeType = 4 rt.rendTimeType = 3
if start_frame is not None and end_frame is not None: if start_frame is not None and end_frame is not None:
frame_range = "{0}-{1}".format(start_frame, end_frame) rt.rendStart = int(start_frame)
rt.rendPickupFrames = frame_range rt.rendEnd = int(end_frame)
def get_multipass_setting(project_setting=None): def get_multipass_setting(project_setting=None):
@ -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
@ -243,6 +249,7 @@ def reset_frame_range(fps: bool = True):
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
frange_cmd = f"animationRange = interval {frame_start} {frame_end}" frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
rt.execute(frange_cmd) rt.execute(frange_cmd)
set_render_frame_range(frame_start, frame_end)
def set_context_setting(): def set_context_setting():
@ -259,6 +266,7 @@ def set_context_setting():
None None
""" """
reset_scene_resolution() reset_scene_resolution()
reset_frame_range()
def get_max_version(): def get_max_version():

View file

@ -36,8 +36,9 @@ class RenderProducts(object):
container) container)
context = get_current_project_asset() context = get_current_project_asset()
startFrame = context["data"].get("frameStart") # TODO: change the frame range follows the current render setting
endFrame = context["data"].get("frameEnd") + 1 startFrame = int(rt.rendStart)
endFrame = int(rt.rendEnd) + 1
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
full_render_list = self.beauty_render_product(output_file, full_render_list = self.beauty_render_product(output_file,

View file

@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io
from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.context_tools import get_current_project_asset
from openpype.hosts.max.api.lib import ( from openpype.hosts.max.api.lib import (
set_framerange, set_render_frame_range,
get_current_renderer, get_current_renderer,
get_default_render_folder get_default_render_folder
) )
@ -68,7 +68,7 @@ class RenderSettings(object):
# Set Frame Range # Set Frame Range
frame_start = context["data"].get("frame_start") frame_start = context["data"].get("frame_start")
frame_end = context["data"].get("frame_end") frame_end = context["data"].get("frame_end")
set_framerange(frame_start, frame_end) set_render_frame_range(frame_start, frame_end)
# get the production render # get the production render
renderer_class = get_current_renderer() renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0] renderer = str(renderer_class).split(":")[0]
@ -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

@ -46,7 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin):
self.log.debug(f"Setting {version_int} to context.") self.log.debug(f"Setting {version_int} to context.")
context.data["version"] = version_int context.data["version"] = version_int
# setup the plugin as 3dsmax for the internal renderer # setup the plugin as 3dsmax for the internal renderer
data = { data = {
"subset": instance.name, "subset": instance.name,
@ -59,8 +58,8 @@ class CollectRender(pyblish.api.InstancePlugin):
"source": filepath, "source": filepath,
"expectedFiles": render_layer_files, "expectedFiles": render_layer_files,
"plugin": "3dsmax", "plugin": "3dsmax",
"frameStart": context.data['frameStart'], "frameStart": int(rt.rendStart),
"frameEnd": context.data['frameEnd'], "frameEnd": int(rt.rendEnd),
"version": version_int, "version": version_int,
"farm": True "farm": True
} }

View file

@ -0,0 +1,64 @@
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline import (
OptionalPyblishPluginMixin
)
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError
)
class ValidateFrameRange(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validates the frame ranges.
This is an optional validator checking if the frame range on instance
matches the frame range specified for the asset.
It also validates render frame ranges of render layers.
Repair action will change everything to match the asset frame range.
This can be turned off by the artist to allow custom ranges.
"""
label = "Validate Frame Range"
order = ValidateContentsOrder
families = ["maxrender"]
hosts = ["max"]
optional = True
actions = [RepairAction]
def process(self, instance):
if not self.is_active(instance.data):
self.log.info("Skipping validation...")
return
context = instance.context
frame_start = int(context.data.get("frameStart"))
frame_end = int(context.data.get("frameEnd"))
inst_frame_start = int(instance.data.get("frameStart"))
inst_frame_end = int(instance.data.get("frameEnd"))
errors = []
if frame_start != inst_frame_start:
errors.append(
f"Start frame ({inst_frame_start}) on instance does not match " # noqa
f"with the start frame ({frame_start}) set on the asset data. ") # noqa
if frame_end != inst_frame_end:
errors.append(
f"End frame ({inst_frame_end}) on instance does not match "
f"with the end frame ({frame_start}) from the asset data. ")
if errors:
errors.append("You can use repair action to fix it.")
raise PublishValidationError("\n".join(errors))
@classmethod
def repair(cls, instance):
rt.rendStart = instance.context.data.get("frameStart")
rt.rendEnd = instance.context.data.get("frameEnd")

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

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

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

@ -21,5 +21,18 @@
"copy_path": "~/.openpype/hosts/fusion/profiles", "copy_path": "~/.openpype/hosts/fusion/profiles",
"copy_status": false, "copy_status": false,
"force_sync": false "force_sync": false
},
"create": {
"CreateSaver": {
"temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}",
"default_variants": [
"Main",
"Mask"
],
"instance_attributes": [
"reviewable",
"farm_rendering"
]
}
} }
} }

View file

@ -19,5 +19,12 @@
"custFloats": "custFloats", "custFloats": "custFloats",
"custVecs": "custVecs" "custVecs": "custVecs"
} }
},
"publish": {
"ValidateFrameRange": {
"enabled": true,
"optional": true,
"active": true
}
} }
} }

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

@ -68,6 +68,50 @@
"label": "Resync profile on each launch" "label": "Resync profile on each launch"
} }
] ]
},
{
"type": "dict",
"collapsible": true,
"key": "create",
"label": "Creator plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "CreateSaver",
"label": "Create Saver",
"is_group": true,
"children": [
{
"type": "text",
"key": "temp_rendering_path_template",
"label": "Temporary rendering path template"
},
{
"type": "list",
"key": "default_variants",
"label": "Default variants",
"object_type": {
"type": "text"
}
},
{
"key": "instance_attributes",
"label": "Instance attributes",
"type": "enum",
"multiselection": true,
"enum_items": [
{
"reviewable": "Reviewable"
},
{
"farm_rendering": "Farm rendering"
}
]
}
]
}
]
} }
] ]
} }

View file

@ -73,6 +73,10 @@
} }
} }
] ]
},
{
"type": "schema",
"name": "schema_max_publish"
} }
] ]
} }

View file

@ -0,0 +1,33 @@
{
"type": "dict",
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "ValidateFrameRange",
"label": "Validate Frame Range",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"type": "boolean",
"key": "active",
"label": "Active"
}
]
}
]
}

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