mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
74b1ff580a
24 changed files with 432 additions and 149 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.7-nightly.3
|
||||
- 3.15.7-nightly.2
|
||||
- 3.15.7-nightly.1
|
||||
- 3.15.6
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.1-nightly.4
|
||||
- 3.14.1-nightly.3
|
||||
- 3.14.1-nightly.2
|
||||
- 3.14.1-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from copy import deepcopy
|
||||
import os
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
|
|
@ -11,15 +12,13 @@ from openpype.lib import (
|
|||
)
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
Creator,
|
||||
Creator as NewCreator,
|
||||
CreatedInstance,
|
||||
)
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
Anatomy
|
||||
)
|
||||
|
||||
|
||||
class CreateSaver(Creator):
|
||||
class CreateSaver(NewCreator):
|
||||
identifier = "io.openpype.creators.fusion.saver"
|
||||
label = "Render (saver)"
|
||||
name = "render"
|
||||
|
|
@ -28,9 +27,24 @@ class CreateSaver(Creator):
|
|||
description = "Fusion Saver to generate image sequence"
|
||||
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):
|
||||
instance_data.update({
|
||||
"id": "pyblish.avalon.instance",
|
||||
"subset": subset_name
|
||||
})
|
||||
|
||||
# TODO: Add pre_create attributes to choose file format?
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
|
|
@ -39,7 +53,6 @@ class CreateSaver(Creator):
|
|||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
|
||||
instance_data["subset"] = subset_name
|
||||
self._update_tool_with_data(saver, data=instance_data)
|
||||
|
||||
saver["OutputFormat"] = file_format
|
||||
|
|
@ -78,7 +91,7 @@ class CreateSaver(Creator):
|
|||
for tool in tools:
|
||||
data = self.get_managed_tool_data(tool)
|
||||
if not data:
|
||||
data = self._collect_unmanaged_saver(tool)
|
||||
continue
|
||||
|
||||
# Add instance
|
||||
created_instance = CreatedInstance.from_existing(data, self)
|
||||
|
|
@ -125,60 +138,35 @@ class CreateSaver(Creator):
|
|||
original_subset = tool.GetData("openpype.subset")
|
||||
subset = data["subset"]
|
||||
if original_subset != subset:
|
||||
# Subset change detected
|
||||
# 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
|
||||
self._configure_saver_tool(data, tool, subset)
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
def _configure_saver_tool(self, data, tool, subset):
|
||||
formatting_data = deepcopy(data)
|
||||
|
||||
def _collect_unmanaged_saver(self, tool):
|
||||
# TODO: this should not be done this way - this should actually
|
||||
# get the data as stored on the tool explicitly (however)
|
||||
# that would disallow any 'regular saver' to be collected
|
||||
# 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,
|
||||
# get frame padding from anatomy templates
|
||||
anatomy = Anatomy()
|
||||
frame_padding = int(
|
||||
anatomy.templates["render"].get("frame_padding", 4)
|
||||
)
|
||||
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
return {
|
||||
# Required data
|
||||
"project": project,
|
||||
"asset": asset,
|
||||
"subset": subset,
|
||||
"task": task,
|
||||
"variant": variant,
|
||||
"active": not passthrough,
|
||||
"family": self.family,
|
||||
# Unique identifier for instance and this creator
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
# Subset change detected
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
formatting_data.update({
|
||||
"workdir": workdir,
|
||||
"frame": "0" * frame_padding,
|
||||
"ext": "exr"
|
||||
})
|
||||
|
||||
# build file path to render
|
||||
filepath = self.temp_rendering_path_template.format(
|
||||
**formatting_data)
|
||||
|
||||
tool["Clip"] = os.path.normpath(filepath)
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
|
||||
def get_managed_tool_data(self, tool):
|
||||
"""Return data of the tool if it matches creator identifier"""
|
||||
|
|
@ -238,3 +226,25 @@ class CreateSaver(Creator):
|
|||
default=("reviewable" in self.instance_attributes),
|
||||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None):
|
|||
["default_render_image_folder"])
|
||||
|
||||
|
||||
def set_framerange(start_frame, end_frame):
|
||||
def set_render_frame_range(start_frame, end_frame):
|
||||
"""
|
||||
Note:
|
||||
Frame range can be specified in different types. Possible values are:
|
||||
|
|
@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame):
|
|||
Todo:
|
||||
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:
|
||||
frame_range = "{0}-{1}".format(start_frame, end_frame)
|
||||
rt.rendPickupFrames = frame_range
|
||||
rt.rendStart = int(start_frame)
|
||||
rt.rendEnd = int(end_frame)
|
||||
|
||||
|
||||
def get_multipass_setting(project_setting=None):
|
||||
|
|
@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int):
|
|||
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.renderHeight = height
|
||||
|
||||
|
||||
def reset_scene_resolution():
|
||||
"""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"])
|
||||
frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
|
||||
rt.execute(frange_cmd)
|
||||
set_render_frame_range(frame_start, frame_end)
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
|
|
@ -259,6 +266,7 @@ def set_context_setting():
|
|||
None
|
||||
"""
|
||||
reset_scene_resolution()
|
||||
reset_frame_range()
|
||||
|
||||
|
||||
def get_max_version():
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ class RenderProducts(object):
|
|||
container)
|
||||
|
||||
context = get_current_project_asset()
|
||||
startFrame = context["data"].get("frameStart")
|
||||
endFrame = context["data"].get("frameEnd") + 1
|
||||
# TODO: change the frame range follows the current render setting
|
||||
startFrame = int(rt.rendStart)
|
||||
endFrame = int(rt.rendEnd) + 1
|
||||
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
full_render_list = self.beauty_render_product(output_file,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io
|
|||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
from openpype.hosts.max.api.lib import (
|
||||
set_framerange,
|
||||
set_render_frame_range,
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
)
|
||||
|
|
@ -68,7 +68,7 @@ class RenderSettings(object):
|
|||
# Set Frame Range
|
||||
frame_start = context["data"].get("frame_start")
|
||||
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
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
|
|
@ -105,6 +105,9 @@ class RenderSettings(object):
|
|||
|
||||
rt.rendSaveFile = True
|
||||
|
||||
if rt.renderSceneDialog.isOpen():
|
||||
rt.renderSceneDialog.close()
|
||||
|
||||
def arnold_setup(self):
|
||||
# get Arnold RenderView run in the background
|
||||
# for setting up renderable camera
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
|
|||
|
||||
def context_setting():
|
||||
return lib.set_context_setting()
|
||||
|
||||
rt.callbacks.addScript(rt.Name('systemPostNew'),
|
||||
context_setting)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ class CreateRender(plugin.MaxCreator):
|
|||
# for additional work on the 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)
|
||||
RenderSettings().set_render_camera(sel_obj)
|
||||
# set output paths for rendering(mandatory for deadline)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.debug(f"Setting {version_int} to context.")
|
||||
context.data["version"] = version_int
|
||||
|
||||
# setup the plugin as 3dsmax for the internal renderer
|
||||
data = {
|
||||
"subset": instance.name,
|
||||
|
|
@ -59,8 +58,8 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
"source": filepath,
|
||||
"expectedFiles": render_layer_files,
|
||||
"plugin": "3dsmax",
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd'],
|
||||
"frameStart": int(rt.rendStart),
|
||||
"frameEnd": int(rt.rendEnd),
|
||||
"version": version_int,
|
||||
"farm": True
|
||||
}
|
||||
|
|
|
|||
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal file
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal 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")
|
||||
|
|
@ -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()
|
||||
|
|
@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
|||
asset_name = instance.data.get("asset", None)
|
||||
|
||||
# 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:
|
||||
shading_engines = cmds.listConnections(shape,
|
||||
destination=True,
|
||||
|
|
@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
|||
)
|
||||
|
||||
for shader in shaders:
|
||||
m = r.match(cls.regex, shader)
|
||||
m = regex_compile.match(shader)
|
||||
if m is None:
|
||||
invalid.append(shape)
|
||||
cls.log.error(
|
||||
"object {0} has invalid shader name {1}".format(shape,
|
||||
shader)
|
||||
)
|
||||
cls.log.error(error_message.format(shape, shader))
|
||||
else:
|
||||
if 'asset' in r.groupindex:
|
||||
if 'asset' in regex_compile.groupindex:
|
||||
if m.group('asset') != asset_name:
|
||||
invalid.append(shape)
|
||||
cls.log.error(("object {0} has invalid "
|
||||
"shader name {1}").format(shape,
|
||||
shader))
|
||||
message = error_message
|
||||
message += " with missing asset name \"{2}\""
|
||||
cls.log.error(
|
||||
message.format(shape, shader, asset_name)
|
||||
)
|
||||
|
||||
return invalid
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from . import (
|
|||
register_inventory_action_path,
|
||||
register_creator_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
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
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_discovery_filter(filter_pyblish_plugins)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
|
||||
if host_name is None:
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
|
|
@ -223,6 +226,7 @@ def uninstall_host():
|
|||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_inventory_action_path(INVENTORY_PATH)
|
||||
log.info("Global plug-ins unregistred")
|
||||
|
||||
deregister_host()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import (
|
|||
get_default_values,
|
||||
)
|
||||
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 .creator_plugins import (
|
||||
|
|
@ -1383,6 +1383,8 @@ class CreateContext:
|
|||
self._current_task_name = None
|
||||
self._current_workfile_path = None
|
||||
|
||||
self._current_project_anatomy = None
|
||||
|
||||
self._host_is_valid = host_is_valid
|
||||
# Currently unused variable
|
||||
self.headless = headless
|
||||
|
|
@ -1546,6 +1548,18 @@ class CreateContext:
|
|||
|
||||
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
|
||||
def context_has_changed(self):
|
||||
"""Host context has changed.
|
||||
|
|
@ -1568,6 +1582,7 @@ class CreateContext:
|
|||
)
|
||||
|
||||
project_name = property(get_current_project_name)
|
||||
project_anatomy = property(get_current_project_anatomy)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
|
|
@ -1680,6 +1695,8 @@ class CreateContext:
|
|||
self._current_task_name = task_name
|
||||
self._current_workfile_path = workfile_path
|
||||
|
||||
self._current_project_anatomy = None
|
||||
|
||||
def reset_plugins(self, discover_publish_plugins=True):
|
||||
"""Reload plugins.
|
||||
|
||||
|
|
|
|||
|
|
@ -231,10 +231,24 @@ class BaseCreator:
|
|||
|
||||
@property
|
||||
def project_name(self):
|
||||
"""Family that plugin represents."""
|
||||
"""Current project name.
|
||||
|
||||
Returns:
|
||||
str: Name of a project.
|
||||
"""
|
||||
|
||||
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
|
||||
def host(self):
|
||||
return self.create_context.host
|
||||
|
|
|
|||
51
openpype/plugins/inventory/remove_and_load.py
Normal file
51
openpype/plugins/inventory/remove_and_load.py
Normal 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)
|
||||
|
|
@ -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,)
|
||||
|
|
@ -21,5 +21,18 @@
|
|||
"copy_path": "~/.openpype/hosts/fusion/profiles",
|
||||
"copy_status": 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,12 @@
|
|||
"custFloats": "custFloats",
|
||||
"custVecs": "custVecs"
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"ValidateFrameRange": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -734,6 +734,7 @@
|
|||
"ValidateShaderName": {
|
||||
"enabled": false,
|
||||
"optional": true,
|
||||
"active": true,
|
||||
"regex": "(?P<asset>.*)_(.*)_SHD"
|
||||
},
|
||||
"ValidateShadingEngine": {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,50 @@
|
|||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_max_publish"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -126,6 +126,11 @@
|
|||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"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=<asset>.+)_SHD</code></p>"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue