Merge branch 'develop' into enhancement/OP-3622_Delivery-renamed-frame-numbers

This commit is contained in:
Kayla Man 2023-07-05 18:30:05 +08:00
commit 5358395008
53 changed files with 1018 additions and 275 deletions

View file

@ -35,6 +35,8 @@ 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.12-nightly.3
- 3.15.12-nightly.2
- 3.15.12-nightly.1 - 3.15.12-nightly.1
- 3.15.11 - 3.15.11
- 3.15.11-nightly.5 - 3.15.11-nightly.5
@ -133,8 +135,6 @@ body:
- 3.14.4 - 3.14.4
- 3.14.4-nightly.4 - 3.14.4-nightly.4
- 3.14.4-nightly.3 - 3.14.4-nightly.3
- 3.14.4-nightly.2
- 3.14.4-nightly.1
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View file

@ -21,8 +21,13 @@ from .pipeline import (
reset_selection reset_selection
) )
from .constants import (
OPENPYPE_TAG_NAME,
DEFAULT_SEQUENCE_NAME,
DEFAULT_BIN_NAME
)
from .lib import ( from .lib import (
pype_tag_name,
flatten, flatten,
get_track_items, get_track_items,
get_current_project, get_current_project,
@ -82,8 +87,12 @@ __all__ = [
"file_extensions", "file_extensions",
"work_root", "work_root",
# Constants
"OPENPYPE_TAG_NAME",
"DEFAULT_SEQUENCE_NAME",
"DEFAULT_BIN_NAME",
# Lib functions # Lib functions
"pype_tag_name",
"flatten", "flatten",
"get_track_items", "get_track_items",
"get_current_project", "get_current_project",

View file

@ -0,0 +1,3 @@
OPENPYPE_TAG_NAME = "openpypeData"
DEFAULT_SEQUENCE_NAME = "openpypeSequence"
DEFAULT_BIN_NAME = "openpypeBin"

View file

@ -5,7 +5,6 @@ Host specific functions where host api is connected
from copy import deepcopy from copy import deepcopy
import os import os
import re import re
import sys
import platform import platform
import functools import functools
import warnings import warnings
@ -29,12 +28,22 @@ from openpype.pipeline import (
from openpype.pipeline.load import filter_containers from openpype.pipeline.load import filter_containers
from openpype.lib import Logger from openpype.lib import Logger
from . import tags from . import tags
from .constants import (
OPENPYPE_TAG_NAME,
DEFAULT_SEQUENCE_NAME,
DEFAULT_BIN_NAME
)
from openpype.pipeline.colorspace import ( from openpype.pipeline.colorspace import (
get_imageio_config get_imageio_config
) )
class _CTX:
has_been_setup = False
has_menu = False
parent_gui = None
class DeprecatedWarning(DeprecationWarning): class DeprecatedWarning(DeprecationWarning):
pass pass
@ -82,23 +91,14 @@ def deprecated(new_destination):
log = Logger.get_logger(__name__) log = Logger.get_logger(__name__)
self = sys.modules[__name__]
self._has_been_setup = False
self._has_menu = False
self._registered_gui = None
self._parent = None
self.pype_tag_name = "openpypeData"
self.default_sequence_name = "openpypeSequence"
self.default_bin_name = "openpypeBin"
def flatten(list_):
def flatten(_list): for item_ in list_:
for item in _list: if isinstance(item_, (list, tuple)):
if isinstance(item, (list, tuple)): for sub_item in flatten(item_):
for sub_item in flatten(item):
yield sub_item yield sub_item
else: else:
yield item yield item_
def get_current_project(remove_untitled=False): def get_current_project(remove_untitled=False):
@ -131,7 +131,7 @@ def get_current_sequence(name=None, new=False):
if new: if new:
# create new # create new
name = name or self.default_sequence_name name = name or DEFAULT_SEQUENCE_NAME
sequence = hiero.core.Sequence(name) sequence = hiero.core.Sequence(name)
root_bin.addItem(hiero.core.BinItem(sequence)) root_bin.addItem(hiero.core.BinItem(sequence))
elif name: elif name:
@ -345,7 +345,7 @@ def get_track_item_tags(track_item):
# collect all tags which are not openpype tag # collect all tags which are not openpype tag
returning_tag_data.extend( returning_tag_data.extend(
tag for tag in _tags tag for tag in _tags
if tag.name() != self.pype_tag_name if tag.name() != OPENPYPE_TAG_NAME
) )
return returning_tag_data return returning_tag_data
@ -385,7 +385,7 @@ def set_track_openpype_tag(track, data=None):
# if pype tag available then update with input data # if pype tag available then update with input data
tag = tags.create_tag( tag = tags.create_tag(
"{}_{}".format( "{}_{}".format(
self.pype_tag_name, OPENPYPE_TAG_NAME,
_get_tag_unique_hash() _get_tag_unique_hash()
), ),
tag_data tag_data
@ -412,7 +412,7 @@ def get_track_openpype_tag(track):
return None return None
for tag in _tags: for tag in _tags:
# return only correct tag defined by global name # return only correct tag defined by global name
if self.pype_tag_name in tag.name(): if OPENPYPE_TAG_NAME in tag.name():
return tag return tag
@ -484,7 +484,7 @@ def get_trackitem_openpype_tag(track_item):
return None return None
for tag in _tags: for tag in _tags:
# return only correct tag defined by global name # return only correct tag defined by global name
if self.pype_tag_name in tag.name(): if OPENPYPE_TAG_NAME in tag.name():
return tag return tag
@ -516,7 +516,7 @@ def set_trackitem_openpype_tag(track_item, data=None):
# if pype tag available then update with input data # if pype tag available then update with input data
tag = tags.create_tag( tag = tags.create_tag(
"{}_{}".format( "{}_{}".format(
self.pype_tag_name, OPENPYPE_TAG_NAME,
_get_tag_unique_hash() _get_tag_unique_hash()
), ),
tag_data tag_data
@ -698,29 +698,29 @@ def setup(console=False, port=None, menu=True):
menu (bool, optional): Display file menu in Hiero. menu (bool, optional): Display file menu in Hiero.
""" """
if self._has_been_setup: if _CTX.has_been_setup:
teardown() teardown()
add_submission() add_submission()
if menu: if menu:
add_to_filemenu() add_to_filemenu()
self._has_menu = True _CTX.has_menu = True
self._has_been_setup = True _CTX.has_been_setup = True
log.debug("pyblish: Loaded successfully.") log.debug("pyblish: Loaded successfully.")
def teardown(): def teardown():
"""Remove integration""" """Remove integration"""
if not self._has_been_setup: if not _CTX.has_been_setup:
return return
if self._has_menu: if _CTX.has_menu:
remove_from_filemenu() remove_from_filemenu()
self._has_menu = False _CTX.has_menu = False
self._has_been_setup = False _CTX.has_been_setup = False
log.debug("pyblish: Integration torn down successfully") log.debug("pyblish: Integration torn down successfully")
@ -928,7 +928,7 @@ def create_bin(path=None, project=None):
# get the first loaded project # get the first loaded project
project = project or get_current_project() project = project or get_current_project()
path = path or self.default_bin_name path = path or DEFAULT_BIN_NAME
path = path.replace("\\", "/").split("/") path = path.replace("\\", "/").split("/")
@ -1311,11 +1311,11 @@ def before_project_save(event):
def get_main_window(): def get_main_window():
"""Acquire Nuke's main window""" """Acquire Nuke's main window"""
if self._parent is None: if _CTX.parent_gui is None:
top_widgets = QtWidgets.QApplication.topLevelWidgets() top_widgets = QtWidgets.QApplication.topLevelWidgets()
name = "Foundry::UI::DockMainWindow" name = "Foundry::UI::DockMainWindow"
main_window = next(widget for widget in top_widgets if main_window = next(widget for widget in top_widgets if
widget.inherits("QMainWindow") and widget.inherits("QMainWindow") and
widget.metaObject().className() == name) widget.metaObject().className() == name)
self._parent = main_window _CTX.parent_gui = main_window
return self._parent return _CTX.parent_gui

View file

@ -3,20 +3,18 @@
import os import os
import re import re
import sys
import ast import ast
import opentimelineio as otio import opentimelineio as otio
from . import utils from . import utils
import hiero.core import hiero.core
import hiero.ui import hiero.ui
self = sys.modules[__name__]
self.track_types = { TRACK_TYPE_MAP = {
hiero.core.VideoTrack: otio.schema.TrackKind.Video, hiero.core.VideoTrack: otio.schema.TrackKind.Video,
hiero.core.AudioTrack: otio.schema.TrackKind.Audio hiero.core.AudioTrack: otio.schema.TrackKind.Audio
} }
self.project_fps = None MARKER_COLOR_MAP = {
self.marker_color_map = {
"magenta": otio.schema.MarkerColor.MAGENTA, "magenta": otio.schema.MarkerColor.MAGENTA,
"red": otio.schema.MarkerColor.RED, "red": otio.schema.MarkerColor.RED,
"yellow": otio.schema.MarkerColor.YELLOW, "yellow": otio.schema.MarkerColor.YELLOW,
@ -24,30 +22,21 @@ self.marker_color_map = {
"cyan": otio.schema.MarkerColor.CYAN, "cyan": otio.schema.MarkerColor.CYAN,
"blue": otio.schema.MarkerColor.BLUE, "blue": otio.schema.MarkerColor.BLUE,
} }
self.timeline = None
self.include_tags = True
def flatten(_list): class CTX:
for item in _list: project_fps = None
if isinstance(item, (list, tuple)): timeline = None
for sub_item in flatten(item): include_tags = True
def flatten(list_):
for item_ in list_:
if isinstance(item_, (list, tuple)):
for sub_item in flatten(item_):
yield sub_item yield sub_item
else: else:
yield item yield item_
def get_current_hiero_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
if not remove_untitled:
return next(iter(projects))
# if remove_untitled
for proj in projects:
if "Untitled" in proj.name():
proj.close()
else:
return proj
def create_otio_rational_time(frame, fps): def create_otio_rational_time(frame, fps):
@ -152,7 +141,7 @@ def create_otio_reference(clip):
file_head = media_source.filenameHead() file_head = media_source.filenameHead()
is_sequence = not media_source.singleFile() is_sequence = not media_source.singleFile()
frame_duration = media_source.duration() frame_duration = media_source.duration()
fps = utils.get_rate(clip) or self.project_fps fps = utils.get_rate(clip) or CTX.project_fps
extension = os.path.splitext(path)[-1] extension = os.path.splitext(path)[-1]
if is_sequence: if is_sequence:
@ -217,8 +206,8 @@ def get_marker_color(tag):
res = re.search(pat, icon) res = re.search(pat, icon)
if res: if res:
color = res.groupdict().get('color') color = res.groupdict().get('color')
if color.lower() in self.marker_color_map: if color.lower() in MARKER_COLOR_MAP:
return self.marker_color_map[color.lower()] return MARKER_COLOR_MAP[color.lower()]
return otio.schema.MarkerColor.RED return otio.schema.MarkerColor.RED
@ -232,7 +221,7 @@ def create_otio_markers(otio_item, item):
# Hiero adds this tag to a lot of clips # Hiero adds this tag to a lot of clips
continue continue
frame_rate = utils.get_rate(item) or self.project_fps frame_rate = utils.get_rate(item) or CTX.project_fps
marked_range = otio.opentime.TimeRange( marked_range = otio.opentime.TimeRange(
start_time=otio.opentime.RationalTime( start_time=otio.opentime.RationalTime(
@ -279,7 +268,7 @@ def create_otio_clip(track_item):
duration = int(track_item.duration()) duration = int(track_item.duration())
fps = utils.get_rate(track_item) or self.project_fps fps = utils.get_rate(track_item) or CTX.project_fps
name = track_item.name() name = track_item.name()
media_reference = create_otio_reference(clip) media_reference = create_otio_reference(clip)
@ -296,7 +285,7 @@ def create_otio_clip(track_item):
) )
# Add tags as markers # Add tags as markers
if self.include_tags: if CTX.include_tags:
create_otio_markers(otio_clip, track_item) create_otio_markers(otio_clip, track_item)
create_otio_markers(otio_clip, track_item.source()) create_otio_markers(otio_clip, track_item.source())
@ -319,13 +308,13 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps):
def _create_otio_timeline(): def _create_otio_timeline():
project = get_current_hiero_project(remove_untitled=False) project = CTX.timeline.project()
metadata = _get_metadata(self.timeline) metadata = _get_metadata(CTX.timeline)
metadata.update({ metadata.update({
"openpype.timeline.width": int(self.timeline.format().width()), "openpype.timeline.width": int(CTX.timeline.format().width()),
"openpype.timeline.height": int(self.timeline.format().height()), "openpype.timeline.height": int(CTX.timeline.format().height()),
"openpype.timeline.pixelAspect": int(self.timeline.format().pixelAspect()), # noqa "openpype.timeline.pixelAspect": int(CTX.timeline.format().pixelAspect()), # noqa
"openpype.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa "openpype.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa
"openpype.project.lutSetting16Bit": project.lutSetting16Bit(), "openpype.project.lutSetting16Bit": project.lutSetting16Bit(),
"openpype.project.lutSetting8Bit": project.lutSetting8Bit(), "openpype.project.lutSetting8Bit": project.lutSetting8Bit(),
@ -339,10 +328,10 @@ def _create_otio_timeline():
}) })
start_time = create_otio_rational_time( start_time = create_otio_rational_time(
self.timeline.timecodeStart(), self.project_fps) CTX.timeline.timecodeStart(), CTX.project_fps)
return otio.schema.Timeline( return otio.schema.Timeline(
name=self.timeline.name(), name=CTX.timeline.name(),
global_start_time=start_time, global_start_time=start_time,
metadata=metadata metadata=metadata
) )
@ -351,7 +340,7 @@ def _create_otio_timeline():
def create_otio_track(track_type, track_name): def create_otio_track(track_type, track_name):
return otio.schema.Track( return otio.schema.Track(
name=track_name, name=track_name,
kind=self.track_types[track_type] kind=TRACK_TYPE_MAP[track_type]
) )
@ -363,7 +352,7 @@ def add_otio_gap(track_item, otio_track, prev_out):
gap = otio.opentime.TimeRange( gap = otio.opentime.TimeRange(
duration=otio.opentime.RationalTime( duration=otio.opentime.RationalTime(
gap_length, gap_length,
self.project_fps CTX.project_fps
) )
) )
otio_gap = otio.schema.Gap(source_range=gap) otio_gap = otio.schema.Gap(source_range=gap)
@ -396,14 +385,14 @@ def create_otio_timeline():
return track_item.parent().items()[itemindex - 1] return track_item.parent().items()[itemindex - 1]
# get current timeline # get current timeline
self.timeline = hiero.ui.activeSequence() CTX.timeline = hiero.ui.activeSequence()
self.project_fps = self.timeline.framerate().toFloat() CTX.project_fps = CTX.timeline.framerate().toFloat()
# convert timeline to otio # convert timeline to otio
otio_timeline = _create_otio_timeline() otio_timeline = _create_otio_timeline()
# loop all defined track types # loop all defined track types
for track in self.timeline.items(): for track in CTX.timeline.items():
# skip if track is disabled # skip if track is disabled
if not track.isEnabled(): if not track.isEnabled():
continue continue
@ -441,7 +430,7 @@ def create_otio_timeline():
otio_track.append(otio_clip) otio_track.append(otio_clip)
# Add tags as markers # Add tags as markers
if self.include_tags: if CTX.include_tags:
create_otio_markers(otio_track, track) create_otio_markers(otio_track, track)
# add track to otio timeline # add track to otio timeline

View file

@ -310,7 +310,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# add pypedata marker to otio_clip metadata # add pypedata marker to otio_clip metadata
for marker in otio_clip.markers: for marker in otio_clip.markers:
if phiero.pype_tag_name in marker.name: if phiero.OPENPYPE_TAG_NAME in marker.name:
otio_clip.metadata.update(marker.metadata) otio_clip.metadata.update(marker.metadata)
return {"otioClip": otio_clip} return {"otioClip": otio_clip}

View file

@ -8,7 +8,6 @@ from qtpy.QtGui import QPixmap
import hiero.ui import hiero.ui
from openpype.pipeline import legacy_io from openpype.pipeline import legacy_io
from openpype.hosts.hiero import api as phiero
from openpype.hosts.hiero.api.otio import hiero_export from openpype.hosts.hiero.api.otio import hiero_export
@ -22,8 +21,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
asset = legacy_io.Session["AVALON_ASSET"] asset = legacy_io.Session["AVALON_ASSET"]
subset = "workfile" subset = "workfile"
project = phiero.get_current_project()
active_timeline = hiero.ui.activeSequence() active_timeline = hiero.ui.activeSequence()
project = active_timeline.project()
fps = active_timeline.framerate().toFloat() fps = active_timeline.framerate().toFloat()
# adding otio timeline to context # adding otio timeline to context

View file

@ -633,23 +633,8 @@ def evalParmNoFrame(node, parm, pad_character="#"):
def get_color_management_preferences(): def get_color_management_preferences():
"""Get default OCIO preferences""" """Get default OCIO preferences"""
data = { return {
"config": hou.Color.ocio_configPath() "config": hou.Color.ocio_configPath(),
"display": hou.Color.ocio_defaultDisplay(),
"view": hou.Color.ocio_defaultView()
} }
# Get default display and view from OCIO
display = hou.Color.ocio_defaultDisplay()
disp_regex = re.compile(r"^(?P<name>.+-)(?P<display>.+)$")
disp_match = disp_regex.match(display)
view = hou.Color.ocio_defaultView()
view_regex = re.compile(r"^(?P<name>.+- )(?P<view>.+)$")
view_match = view_regex.match(view)
data.update({
"display": disp_match.group("display"),
"view": view_match.group("view")
})
return data

View file

@ -93,7 +93,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
import hdefereval # noqa, hdefereval is only available in ui mode import hdefereval # noqa, hdefereval is only available in ui mode
hdefereval.executeDeferred(creator_node_shelves.install) hdefereval.executeDeferred(creator_node_shelves.install)
def has_unsaved_changes(self): def workfile_has_unsaved_changes(self):
return hou.hipFile.hasUnsavedChanges() return hou.hipFile.hasUnsavedChanges()
def get_workfile_extensions(self): def get_workfile_extensions(self):

View file

@ -3,12 +3,12 @@
import hou # noqa import hou # noqa
from openpype.hosts.houdini.api import plugin from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import EnumDef from openpype.lib import EnumDef
class CreateRedshiftROP(plugin.HoudiniCreator): class CreateRedshiftROP(plugin.HoudiniCreator):
"""Redshift ROP""" """Redshift ROP"""
identifier = "io.openpype.creators.houdini.redshift_rop" identifier = "io.openpype.creators.houdini.redshift_rop"
label = "Redshift ROP" label = "Redshift ROP"
family = "redshift_rop" family = "redshift_rop"
@ -28,7 +28,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
instance = super(CreateRedshiftROP, self).create( instance = super(CreateRedshiftROP, self).create(
subset_name, subset_name,
instance_data, instance_data,
pre_create_data) # type: CreatedInstance pre_create_data)
instance_node = hou.node(instance.get("instance_node")) instance_node = hou.node(instance.get("instance_node"))
@ -57,6 +57,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
) )
ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3}
parms = { parms = {
# Render frame range # Render frame range
"trange": 1, "trange": 1,
@ -64,6 +66,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
"RS_outputFileNamePrefix": filepath, "RS_outputFileNamePrefix": filepath,
"RS_outputMultilayerMode": "1", # no multi-layered exr "RS_outputMultilayerMode": "1", # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty", "RS_outputBeautyAOVSuffix": "beauty",
"RS_outputFileFormat": ext_format_index[ext],
} }
if self.selected_nodes: if self.selected_nodes:
@ -93,8 +96,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
def get_pre_create_attr_defs(self): def get_pre_create_attr_defs(self):
attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()
image_format_enum = [ image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "exr", "tif", "jpg", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
] ]
return attrs + [ return attrs + [

View file

@ -19,7 +19,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
"Collected filename from current scene name." "Collected filename from current scene name."
) )
if host.has_unsaved_changes(): if host.workfile_has_unsaved_changes():
self.log.info("Saving current file: {}".format(current_file)) self.log.info("Saving current file: {}".format(current_file))
host.save_workfile(current_file) host.save_workfile(current_file)
else: else:

View file

@ -73,6 +73,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern)
cls.log.debug("Checking with path attribute: %s" % path_attr) cls.log.debug("Checking with path attribute: %s" % path_attr)
if not hasattr(output_node, "geometry"):
# In the case someone has explicitly set an Object
# node instead of a SOP node in Geometry context
# then for now we ignore - this allows us to also
# export object transforms.
cls.log.warning("No geometry output node found, skipping check..")
return
# Check if the primitive attribute exists # Check if the primitive attribute exists
frame = instance.data.get("frameStart", 0) frame = instance.data.get("frameStart", 0)
geo = output_node.geometryAtFrame(frame) geo = output_node.geometryAtFrame(frame)

View file

@ -60,6 +60,14 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
cls.log.debug("Checking for attribute: %s" % path_attr) cls.log.debug("Checking for attribute: %s" % path_attr)
if not hasattr(output_node, "geometry"):
# In the case someone has explicitly set an Object
# node instead of a SOP node in Geometry context
# then for now we ignore - this allows us to also
# export object transforms.
cls.log.warning("No geometry output node found, skipping check..")
return
# Check if the primitive attribute exists # Check if the primitive attribute exists
frame = instance.data.get("frameStart", 0) frame = instance.data.get("frameStart", 0)
geo = output_node.geometryAtFrame(frame) geo = output_node.geometryAtFrame(frame)

View file

@ -53,6 +53,8 @@ def update_mode_context(mode):
def get_geometry_at_frame(sop_node, frame, force=True): def get_geometry_at_frame(sop_node, frame, force=True):
"""Return geometry at frame but force a cooked value.""" """Return geometry at frame but force a cooked value."""
if not hasattr(sop_node, "geometry"):
return
with update_mode_context(hou.updateMode.AutoUpdate): with update_mode_context(hou.updateMode.AutoUpdate):
sop_node.cook(force=force, frame_range=(frame, frame)) sop_node.cook(force=force, frame_range=(frame, frame))
return sop_node.geometryAtFrame(frame) return sop_node.geometryAtFrame(frame)

View file

@ -284,6 +284,21 @@ def get_max_version():
return max_info[7] return max_info[7]
@contextlib.contextmanager
def viewport_camera(camera):
original = rt.viewport.getCamera()
if not original:
# if there is no original camera
# use the current camera as original
original = rt.getNodeByName(camera)
review_camera = rt.getNodeByName(camera)
try:
rt.viewport.setCamera(review_camera)
yield
finally:
rt.viewport.setCamera(original)
def set_timeline(frameStart, frameEnd): def set_timeline(frameStart, frameEnd):
"""Set frame range for timeline editor in Max """Set frame range for timeline editor in Max
""" """

View file

@ -42,6 +42,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData"
( (
handle_name = node_to_name c handle_name = node_to_name c
node_ref = NodeTransformMonitor node:c node_ref = NodeTransformMonitor node:c
idx = finditem list_node.items handle_name
if idx do (
continue
)
append temp_arr handle_name append temp_arr handle_name
append i_node_arr node_ref append i_node_arr node_ref
) )

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating review in Max."""
from openpype.hosts.max.api import plugin
from openpype.lib import BoolDef, EnumDef, NumberDef
class CreateReview(plugin.MaxCreator):
"""Review in 3dsMax"""
identifier = "io.openpype.creators.max.review"
label = "Review"
family = "review"
icon = "video-camera"
def create(self, subset_name, instance_data, pre_create_data):
instance_data["imageFormat"] = pre_create_data.get("imageFormat")
instance_data["keepImages"] = pre_create_data.get("keepImages")
instance_data["percentSize"] = pre_create_data.get("percentSize")
instance_data["rndLevel"] = pre_create_data.get("rndLevel")
super(CreateReview, self).create(
subset_name,
instance_data,
pre_create_data)
def get_pre_create_attr_defs(self):
attrs = super(CreateReview, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "hdr", "rgb", "png",
"rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg"
]
rndLevel_enum = [
"smoothhighlights", "smooth", "facethighlights",
"facet", "flat", "litwireframe", "wireframe", "box"
]
return attrs + [
BoolDef("keepImages",
label="Keep Image Sequences",
default=False),
EnumDef("imageFormat",
image_format_enum,
default="png",
label="Image Format Options"),
NumberDef("percentSize",
label="Percent of Output",
default=100,
minimum=1,
decimals=0),
EnumDef("rndLevel",
rndLevel_enum,
default="smoothhighlights",
label="Preference")
]

View file

@ -0,0 +1,92 @@
# dont forget getting the focal length for burnin
"""Collect Review"""
import pyblish.api
from pymxs import runtime as rt
from openpype.lib import BoolDef
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
class CollectReview(pyblish.api.InstancePlugin,
OpenPypePyblishPluginMixin):
"""Collect Review Data for Preview Animation"""
order = pyblish.api.CollectorOrder + 0.02
label = "Collect Review Data"
hosts = ['max']
families = ["review"]
def process(self, instance):
nodes = instance.data["members"]
focal_length = None
camera_name = None
for node in nodes:
if rt.classOf(node) in rt.Camera.classes:
camera_name = node.name
focal_length = node.fov
attr_values = self.get_attr_values_from_data(instance.data)
data = {
"review_camera": camera_name,
"frameStart": instance.context.data["frameStart"],
"frameEnd": instance.context.data["frameEnd"],
"fps": instance.context.data["fps"],
"dspGeometry": attr_values.get("dspGeometry"),
"dspShapes": attr_values.get("dspShapes"),
"dspLights": attr_values.get("dspLights"),
"dspCameras": attr_values.get("dspCameras"),
"dspHelpers": attr_values.get("dspHelpers"),
"dspParticles": attr_values.get("dspParticles"),
"dspBones": attr_values.get("dspBones"),
"dspBkg": attr_values.get("dspBkg"),
"dspGrid": attr_values.get("dspGrid"),
"dspSafeFrame": attr_values.get("dspSafeFrame"),
"dspFrameNums": attr_values.get("dspFrameNums")
}
# Enable ftrack functionality
instance.data.setdefault("families", []).append('ftrack')
burnin_members = instance.data.setdefault("burninDataMembers", {})
burnin_members["focalLength"] = focal_length
self.log.debug(f"data:{data}")
instance.data.update(data)
@classmethod
def get_attribute_defs(cls):
return [
BoolDef("dspGeometry",
label="Geometry",
default=True),
BoolDef("dspShapes",
label="Shapes",
default=False),
BoolDef("dspLights",
label="Lights",
default=False),
BoolDef("dspCameras",
label="Cameras",
default=False),
BoolDef("dspHelpers",
label="Helpers",
default=False),
BoolDef("dspParticles",
label="Particle Systems",
default=True),
BoolDef("dspBones",
label="Bone Objects",
default=False),
BoolDef("dspBkg",
label="Background",
default=True),
BoolDef("dspGrid",
label="Active Grid",
default=False),
BoolDef("dspSafeFrame",
label="Safe Frames",
default=False),
BoolDef("dspFrameNums",
label="Frame Numbers",
default=False)
]

View file

@ -0,0 +1,102 @@
import os
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline import publish
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
class ExtractReviewAnimation(publish.Extractor):
"""
Extract Review by Review Animation
"""
order = pyblish.api.ExtractorOrder + 0.001
label = "Extract Review Animation"
hosts = ["max"]
families = ["review"]
def process(self, instance):
staging_dir = self.staging_dir(instance)
ext = instance.data.get("imageFormat")
filename = "{0}..{1}".format(instance.name, ext)
start = int(instance.data["frameStart"])
end = int(instance.data["frameEnd"])
fps = int(instance.data["fps"])
filepath = os.path.join(staging_dir, filename)
filepath = filepath.replace("\\", "/")
filenames = self.get_files(
instance.name, start, end, ext)
self.log.debug(
"Writing Review Animation to"
" '%s' to '%s'" % (filename, staging_dir))
review_camera = instance.data["review_camera"]
with viewport_camera(review_camera):
preview_arg = self.set_preview_arg(
instance, filepath, start, end, fps)
rt.execute(preview_arg)
tags = ["review"]
if not instance.data.get("keepImages"):
tags.append("delete")
self.log.debug("Performing Extraction ...")
representation = {
"name": instance.data["imageFormat"],
"ext": instance.data["imageFormat"],
"files": filenames,
"stagingDir": staging_dir,
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
"tags": tags,
"preview": True,
"camera_name": review_camera
}
self.log.debug(f"{representation}")
if "representations" not in instance.data:
instance.data["representations"] = []
instance.data["representations"].append(representation)
def get_files(self, filename, start, end, ext):
file_list = []
for frame in range(int(start), int(end) + 1):
actual_name = "{}.{:04}.{}".format(
filename, frame, ext)
file_list.append(actual_name)
return file_list
def set_preview_arg(self, instance, filepath,
start, end, fps):
job_args = list()
default_option = f'CreatePreview filename:"{filepath}"'
job_args.append(default_option)
frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa
job_args.append(frame_option)
rndLevel = instance.data.get("rndLevel")
if rndLevel:
option = f"rndLevel:#{rndLevel}"
job_args.append(option)
options = [
"percentSize", "dspGeometry", "dspShapes",
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
]
for key in options:
enabled = instance.data.get(key)
if enabled:
job_args.append(f"{key}:{enabled}")
if get_max_version() == 2024:
# hardcoded for current stage
auto_play_option = "autoPlay:false"
job_args.append(auto_play_option)
job_str = " ".join(job_args)
self.log.debug(job_str)
return job_str

View file

@ -0,0 +1,91 @@
import os
import tempfile
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline import publish
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
class ExtractThumbnail(publish.Extractor):
"""
Extract Thumbnail for Review
"""
order = pyblish.api.ExtractorOrder
label = "Extract Thumbnail"
hosts = ["max"]
families = ["review"]
def process(self, instance):
# TODO: Create temp directory for thumbnail
# - this is to avoid "override" of source file
tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
self.log.debug(
f"Create temp directory {tmp_staging} for thumbnail"
)
fps = int(instance.data["fps"])
frame = int(instance.data["frameStart"])
instance.context.data["cleanupFullPaths"].append(tmp_staging)
filename = "{name}_thumbnail..png".format(**instance.data)
filepath = os.path.join(tmp_staging, filename)
filepath = filepath.replace("\\", "/")
thumbnail = self.get_filename(instance.name, frame)
self.log.debug(
"Writing Thumbnail to"
" '%s' to '%s'" % (filename, tmp_staging))
review_camera = instance.data["review_camera"]
with viewport_camera(review_camera):
preview_arg = self.set_preview_arg(
instance, filepath, fps, frame)
rt.execute(preview_arg)
representation = {
"name": "thumbnail",
"ext": "png",
"files": thumbnail,
"stagingDir": tmp_staging,
"thumbnail": True
}
self.log.debug(f"{representation}")
if "representations" not in instance.data:
instance.data["representations"] = []
instance.data["representations"].append(representation)
def get_filename(self, filename, target_frame):
thumbnail_name = "{}_thumbnail.{:04}.png".format(
filename, target_frame
)
return thumbnail_name
def set_preview_arg(self, instance, filepath, fps, frame):
job_args = list()
default_option = f'CreatePreview filename:"{filepath}"'
job_args.append(default_option)
frame_option = f"outputAVI:false start:{frame} end:{frame} fps:{fps}" # noqa
job_args.append(frame_option)
rndLevel = instance.data.get("rndLevel")
if rndLevel:
option = f"rndLevel:#{rndLevel}"
job_args.append(option)
options = [
"percentSize", "dspGeometry", "dspShapes",
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
]
for key in options:
enabled = instance.data.get(key)
if enabled:
job_args.append(f"{key}:{enabled}")
if get_max_version() == 2024:
# hardcoded for current stage
auto_play_option = "autoPlay:false"
job_args.append(auto_play_option)
job_str = " ".join(job_args)
self.log.debug(job_str)
return job_str

View file

@ -0,0 +1,48 @@
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError
)
from openpype.hosts.max.api.lib import get_frame_range, set_timeline
class ValidateAnimationTimeline(pyblish.api.InstancePlugin):
"""
Validates Animation Timeline for Preview Animation in Max
"""
label = "Animation Timeline for Review"
order = ValidateContentsOrder
families = ["review"]
hosts = ["max"]
actions = [RepairAction]
def process(self, instance):
frame_range = get_frame_range()
frame_start_handle = frame_range["frameStart"] - int(
frame_range["handleStart"]
)
frame_end_handle = frame_range["frameEnd"] + int(
frame_range["handleEnd"]
)
if rt.animationRange.start != frame_start_handle or (
rt.animationRange.end != frame_end_handle
):
raise PublishValidationError("Incorrect animation timeline "
"set for preview animation.. "
"\nYou can use repair action to "
"the correct animation timeline")
@classmethod
def repair(cls, instance):
frame_range = get_frame_range()
frame_start_handle = frame_range["frameStart"] - int(
frame_range["handleStart"]
)
frame_end_handle = frame_range["frameEnd"] + int(
frame_range["handleEnd"]
)
set_timeline(frame_start_handle, frame_end_handle)

View file

@ -11,7 +11,7 @@ class ValidateCameraContent(pyblish.api.InstancePlugin):
""" """
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
families = ["camera"] families = ["camera", "review"]
hosts = ["max"] hosts = ["max"]
label = "Camera Contents" label = "Camera Contents"
camera_type = ["$Free_Camera", "$Target_Camera", camera_type = ["$Free_Camera", "$Target_Camera",

View file

@ -13,7 +13,8 @@ class ValidateMaxContents(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
families = ["camera", families = ["camera",
"maxScene", "maxScene",
"maxrender"] "maxrender",
"review"]
hosts = ["max"] hosts = ["max"]
label = "Max Scene Contents" label = "Max Scene Contents"

View file

@ -3,7 +3,6 @@
import os import os
from pprint import pformat from pprint import pformat
import sys import sys
import platform
import uuid import uuid
import re import re
@ -2811,19 +2810,22 @@ def get_attr_in_layer(attr, layer):
def fix_incompatible_containers(): def fix_incompatible_containers():
"""Backwards compatibility: old containers to use new ReferenceLoader""" """Backwards compatibility: old containers to use new ReferenceLoader"""
old_loaders = {
"MayaAsciiLoader",
"AbcLoader",
"ModelLoader",
"CameraLoader",
"RigLoader",
"FBXLoader"
}
host = registered_host() host = registered_host()
for container in host.ls(): for container in host.ls():
loader = container['loader'] loader = container['loader']
if loader in old_loaders:
print(container['loader']) log.info(
"Converting legacy container loader {} to "
if loader in ["MayaAsciiLoader", "ReferenceLoader: {}".format(loader, container["objectName"])
"AbcLoader", )
"ModelLoader",
"CameraLoader",
"RigLoader",
"FBXLoader"]:
cmds.setAttr(container["objectName"] + ".loader", cmds.setAttr(container["objectName"] + ".loader",
"ReferenceLoader", type="string") "ReferenceLoader", type="string")
@ -2951,7 +2953,7 @@ def _get_render_instances():
list: list of instances list: list of instances
""" """
objectset = cmds.ls("*.id", long=True, type="objectSet", objectset = cmds.ls("*.id", long=True, exactType="objectSet",
recursive=True, objectsOnly=True) recursive=True, objectsOnly=True)
instances = [] instances = []
@ -3238,36 +3240,21 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None):
def set_colorspace(): def set_colorspace():
"""Set Colorspace from project configuration """Set Colorspace from project configuration"""
"""
# set color spaces for rendering space and view transforms project_name = get_current_project_name()
def _colormanage(**kwargs):
"""Wrapper around `cmds.colorManagementPrefs`.
This logs errors instead of raising an error so color management
settings get applied as much as possible.
"""
assert len(kwargs) == 1, "Must receive one keyword argument"
try:
cmds.colorManagementPrefs(edit=True, **kwargs)
log.debug("Setting Color Management Preference: {}".format(kwargs))
except RuntimeError as exc:
log.error(exc)
project_name = os.getenv("AVALON_PROJECT")
imageio = get_project_settings(project_name)["maya"]["imageio"] imageio = get_project_settings(project_name)["maya"]["imageio"]
# ocio compatibility variables # ocio compatibility variables
ocio_v2_maya_version = 2022 ocio_v2_maya_version = 2022
maya_version = int(cmds.about(version=True)) maya_version = int(cmds.about(version=True))
ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version
is_ocio_set = bool(os.environ.get("OCIO"))
root_dict = {}
use_workfile_settings = imageio.get("workfile", {}).get("enabled") use_workfile_settings = imageio.get("workfile", {}).get("enabled")
if use_workfile_settings: if use_workfile_settings:
root_dict = imageio["workfile"]
else:
# TODO: deprecated code from 3.15.5 - remove # TODO: deprecated code from 3.15.5 - remove
# Maya 2022+ introduces new OCIO v2 color management settings that # Maya 2022+ introduces new OCIO v2 color management settings that
# can override the old color management preferences. OpenPype has # can override the old color management preferences. OpenPype has
@ -3290,40 +3277,63 @@ def set_colorspace():
if not isinstance(root_dict, dict): if not isinstance(root_dict, dict):
msg = "set_colorspace(): argument should be dictionary" msg = "set_colorspace(): argument should be dictionary"
log.error(msg) log.error(msg)
return
else: # backward compatibility
root_dict = imageio["workfile"] # TODO: deprecated code from 3.15.5 - remove with deprecated code above
view_name = root_dict.get("viewTransform")
if view_name is None:
view_name = root_dict.get("viewName")
log.debug(">> root_dict: {}".format(pformat(root_dict))) log.debug(">> root_dict: {}".format(pformat(root_dict)))
if not root_dict:
return
if root_dict: # set color spaces for rendering space and view transforms
# enable color management def _colormanage(**kwargs):
cmds.colorManagementPrefs(e=True, cmEnabled=True) """Wrapper around `cmds.colorManagementPrefs`.
cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True)
# backward compatibility This logs errors instead of raising an error so color management
# TODO: deprecated code from 3.15.5 - refactor to use new settings settings get applied as much as possible.
view_name = root_dict.get("viewTransform")
if view_name is None:
view_name = root_dict.get("viewName")
if use_ocio_v2: """
# Use Maya 2022+ default OCIO v2 config assert len(kwargs) == 1, "Must receive one keyword argument"
try:
cmds.colorManagementPrefs(edit=True, **kwargs)
log.debug("Setting Color Management Preference: {}".format(kwargs))
except RuntimeError as exc:
log.error(exc)
# enable color management
cmds.colorManagementPrefs(edit=True, cmEnabled=True)
cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True)
if use_ocio_v2:
log.info("Using Maya OCIO v2")
if not is_ocio_set:
# Set the Maya 2022+ default OCIO v2 config file path
log.info("Setting default Maya OCIO v2 config") log.info("Setting default Maya OCIO v2 config")
cmds.colorManagementPrefs(edit=True, configFilePath="") # Note: Setting "" as value also sets this default however
# introduces a bug where launching a file on startup will prompt
# to save the empty scene before it, so we set using the path.
# This value has been the same for 2022, 2023 and 2024
path = "<MAYA_RESOURCES>/OCIO-configs/Maya2022-default/config.ocio"
cmds.colorManagementPrefs(edit=True, configFilePath=path)
# set rendering space and view transform # set rendering space and view transform
_colormanage(renderingSpaceName=root_dict["renderSpace"]) _colormanage(renderingSpaceName=root_dict["renderSpace"])
_colormanage(viewName=view_name) _colormanage(viewName=view_name)
_colormanage(displayName=root_dict["displayName"]) _colormanage(displayName=root_dict["displayName"])
else: else:
log.info("Using Maya OCIO v1 (legacy)")
if not is_ocio_set:
# Set the Maya default config file path # Set the Maya default config file path
log.info("Setting default Maya OCIO v1 legacy config") log.info("Setting default Maya OCIO v1 legacy config")
cmds.colorManagementPrefs(edit=True, configFilePath="legacy") cmds.colorManagementPrefs(edit=True, configFilePath="legacy")
# set rendering space and view transform # set rendering space and view transform
_colormanage(renderingSpaceName=root_dict["renderSpace"]) _colormanage(renderingSpaceName=root_dict["renderSpace"])
_colormanage(viewTransformName=view_name) _colormanage(viewTransformName=view_name)
@contextlib.contextmanager @contextlib.contextmanager

View file

@ -2,6 +2,7 @@ import os
import errno import errno
import logging import logging
import contextlib import contextlib
import shutil
from maya import utils, cmds, OpenMaya from maya import utils, cmds, OpenMaya
import maya.api.OpenMaya as om import maya.api.OpenMaya as om
@ -113,6 +114,9 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
register_event_callback("taskChanged", on_task_changed) register_event_callback("taskChanged", on_task_changed)
register_event_callback("workfile.open.before", before_workfile_open) register_event_callback("workfile.open.before", before_workfile_open)
register_event_callback("workfile.save.before", before_workfile_save) register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback(
"workfile.save.before", workfile_save_before_xgen
)
register_event_callback("workfile.save.after", after_workfile_save) register_event_callback("workfile.save.after", after_workfile_save)
def open_workfile(self, filepath): def open_workfile(self, filepath):
@ -480,18 +484,16 @@ def on_init():
# Force load objExport plug-in (requested by artists) # Force load objExport plug-in (requested by artists)
cmds.loadPlugin("objExport", quiet=True) cmds.loadPlugin("objExport", quiet=True)
from .customize import (
override_component_mask_commands,
override_toolbox_ui
)
safe_deferred(override_component_mask_commands)
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
if launch_workfiles:
safe_deferred(host_tools.show_workfiles)
if not lib.IS_HEADLESS: if not lib.IS_HEADLESS:
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
if launch_workfiles:
safe_deferred(host_tools.show_workfiles)
from .customize import (
override_component_mask_commands,
override_toolbox_ui
)
safe_deferred(override_component_mask_commands)
safe_deferred(override_toolbox_ui) safe_deferred(override_toolbox_ui)
@ -549,37 +551,29 @@ def on_save():
Any transform of a mesh, without an existing ID, is given one Any transform of a mesh, without an existing ID, is given one
automatically on file save. automatically on file save.
""" """
log.info("Running callback on save..") log.info("Running callback on save..")
# remove lockfile if users jumps over from one scene to another # remove lockfile if users jumps over from one scene to another
_remove_workfile_lock() _remove_workfile_lock()
# # Update current task for the current scene
# update_task_from_path(cmds.file(query=True, sceneName=True))
# Generate ids of the current context on nodes in the scene # Generate ids of the current context on nodes in the scene
nodes = lib.get_id_required_nodes(referenced_nodes=False) nodes = lib.get_id_required_nodes(referenced_nodes=False)
for node, new_id in lib.generate_ids(nodes): for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False) lib.set_id(node, new_id, overwrite=False)
def _update_render_layer_observers():
# Helper to trigger update for all renderlayer observer logic
lib.remove_render_layer_observer()
lib.add_render_layer_observer()
lib.add_render_layer_change_observer()
def on_open(): def on_open():
"""On scene open let's assume the containers have changed.""" """On scene open let's assume the containers have changed."""
from qtpy import QtWidgets
from openpype.widgets import popup from openpype.widgets import popup
cmds.evalDeferred( utils.executeDeferred(_update_render_layer_observers)
"from openpype.hosts.maya.api import lib;"
"lib.remove_render_layer_observer()")
cmds.evalDeferred(
"from openpype.hosts.maya.api import lib;"
"lib.add_render_layer_observer()")
cmds.evalDeferred(
"from openpype.hosts.maya.api import lib;"
"lib.add_render_layer_change_observer()")
# # Update current task for the current scene
# update_task_from_path(cmds.file(query=True, sceneName=True))
# Validate FPS after update_task_from_path to # Validate FPS after update_task_from_path to
# ensure it is using correct FPS for the asset # ensure it is using correct FPS for the asset
@ -590,10 +584,7 @@ def on_open():
log.warning("Scene has outdated content.") log.warning("Scene has outdated content.")
# Find maya main window # Find maya main window
top_level_widgets = {w.objectName(): w for w in parent = lib.get_main_window()
QtWidgets.QApplication.topLevelWidgets()}
parent = top_level_widgets.get("MayaWindow", None)
if parent is None: if parent is None:
log.info("Skipping outdated content pop-up " log.info("Skipping outdated content pop-up "
"because Maya window can't be found.") "because Maya window can't be found.")
@ -618,16 +609,9 @@ def on_new():
"""Set project resolution and fps when create a new file""" """Set project resolution and fps when create a new file"""
log.info("Running callback on new..") log.info("Running callback on new..")
with lib.suspended_refresh(): with lib.suspended_refresh():
cmds.evalDeferred(
"from openpype.hosts.maya.api import lib;"
"lib.remove_render_layer_observer()")
cmds.evalDeferred(
"from openpype.hosts.maya.api import lib;"
"lib.add_render_layer_observer()")
cmds.evalDeferred(
"from openpype.hosts.maya.api import lib;"
"lib.add_render_layer_change_observer()")
lib.set_context_settings() lib.set_context_settings()
utils.executeDeferred(_update_render_layer_observers)
_remove_workfile_lock() _remove_workfile_lock()
@ -681,6 +665,91 @@ def before_workfile_save(event):
create_workspace_mel(workdir_path, project_name) create_workspace_mel(workdir_path, project_name)
def workfile_save_before_xgen(event):
"""Manage Xgen external files when switching context.
Xgen has various external files that needs to be unique and relative to the
workfile, so we need to copy and potentially overwrite these files when
switching context.
Args:
event (Event) - openpype/lib/events.py
"""
if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True):
return
import xgenm
current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/")
expected_work_dir = event.data["workdir_path"].replace("\\", "/")
if current_work_dir == expected_work_dir:
return
palettes = cmds.ls(type="xgmPalette", long=True)
if not palettes:
return
transfers = []
overwrites = []
attribute_changes = {}
attrs = ["xgFileName", "xgBaseFile"]
for palette in palettes:
sanitized_palette = palette.replace("|", "")
project_path = xgenm.getAttr("xgProjectPath", sanitized_palette)
_, maya_extension = os.path.splitext(event.data["filename"])
for attr in attrs:
node_attr = "{}.{}".format(palette, attr)
attr_value = cmds.getAttr(node_attr)
if not attr_value:
continue
source = os.path.join(project_path, attr_value)
attr_value = event.data["filename"].replace(
maya_extension,
"__{}{}".format(
sanitized_palette.replace(":", "__"),
os.path.splitext(attr_value)[1]
)
)
target = os.path.join(expected_work_dir, attr_value)
transfers.append((source, target))
attribute_changes[node_attr] = attr_value
relative_path = xgenm.getAttr(
"xgDataPath", sanitized_palette
).split(os.pathsep)[0]
absolute_path = relative_path.replace("${PROJECT}", project_path)
for root, _, files in os.walk(absolute_path):
for f in files:
source = os.path.join(root, f).replace("\\", "/")
target = source.replace(project_path, expected_work_dir + "/")
transfers.append((source, target))
if os.path.exists(target):
overwrites.append(target)
# Ask user about overwriting files.
if overwrites:
log.warning(
"WARNING! Potential loss of data.\n\n"
"Found duplicate Xgen files in new context.\n{}".format(
"\n".join(overwrites)
)
)
return
for source, destination in transfers:
if not os.path.exists(os.path.dirname(destination)):
os.makedirs(os.path.dirname(destination))
shutil.copy(source, destination)
for attribute, value in attribute_changes.items():
cmds.setAttr(attribute, value, type="string")
def after_workfile_save(event): def after_workfile_save(event):
workfile_name = event["filename"] workfile_name = event["filename"]
if ( if (

View file

@ -1,5 +1,4 @@
import os import os
import re
from maya import cmds from maya import cmds

View file

@ -15,7 +15,6 @@ import contextlib
from maya import cmds from maya import cmds
from maya.app.renderSetup.model import renderSetup from maya.app.renderSetup.model import renderSetup
# from colorbleed.maya import lib
from .lib import pairwise from .lib import pairwise

View file

@ -272,7 +272,12 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
return return
roots = cmds.sets(container, q=True) roots = cmds.sets(container, q=True)
ref_node = get_reference_node(roots) ref_node = None
try:
ref_node = get_reference_node(roots)
except AssertionError as e:
self.log.info(e.args[0])
nodes_to_parent = [] nodes_to_parent = []
for root in roots: for root in roots:
if ref_node: if ref_node:

View file

@ -104,25 +104,40 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Define nice label # Define nice label
name = cmds.ls(objset, long=False)[0] # use short name name = cmds.ls(objset, long=False)[0] # use short name
label = "{0} ({1})".format(name, label = "{0} ({1})".format(name, data["asset"])
data["asset"])
# Convert frame values to integers
for attr_name in (
"handleStart", "handleEnd", "frameStart", "frameEnd",
):
value = data.get(attr_name)
if value is not None:
data[attr_name] = int(value)
# Append start frame and end frame to label if present # Append start frame and end frame to label if present
if "frameStart" and "frameEnd" in data: if "frameStart" in data and "frameEnd" in data:
# Take handles from context if not set locally on the instance # Take handles from context if not set locally on the instance
for key in ["handleStart", "handleEnd"]: for key in ["handleStart", "handleEnd"]:
if key not in data: if key not in data:
data[key] = context.data[key] value = context.data[key]
if value is not None:
value = int(value)
data[key] = value
data["frameStartHandle"] = data["frameStart"] - data["handleStart"] # noqa: E501 data["frameStartHandle"] = int(
data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] # noqa: E501 data["frameStart"] - data["handleStart"]
)
data["frameEndHandle"] = int(
data["frameEnd"] + data["handleEnd"]
)
label += " [{0}-{1}]".format(int(data["frameStartHandle"]), label += " [{0}-{1}]".format(
int(data["frameEndHandle"])) data["frameStartHandle"], data["frameEndHandle"]
)
instance.data["label"] = label instance.data["label"] = label
instance.data.update(data) instance.data.update(data)
self.log.debug("{}".format(instance.data))
# Produce diagnostic message for any graphical # Produce diagnostic message for any graphical
# user interface interested in visualising it. # user interface interested in visualising it.

View file

@ -17,10 +17,12 @@ class CollectMayaSceneTime(pyblish.api.InstancePlugin):
def process(self, instance): def process(self, instance):
instance.data.update({ instance.data.update({
"frameStart": cmds.playbackOptions(query=True, minTime=True), "frameStart": int(
"frameEnd": cmds.playbackOptions(query=True, maxTime=True), cmds.playbackOptions(query=True, minTime=True)),
"frameStartHandle": cmds.playbackOptions(query=True, "frameEnd": int(
animationStartTime=True), cmds.playbackOptions(query=True, maxTime=True)),
"frameEndHandle": cmds.playbackOptions(query=True, "frameStartHandle": int(
animationEndTime=True) cmds.playbackOptions(query=True, animationStartTime=True)),
"frameEndHandle": int(
cmds.playbackOptions(query=True, animationEndTime=True))
}) })

View file

@ -2,7 +2,6 @@
import os import os
from maya import cmds from maya import cmds
# import maya.mel as mel
import pyblish.api import pyblish.api
from openpype.pipeline import publish from openpype.pipeline import publish
from openpype.hosts.maya.api import lib from openpype.hosts.maya.api import lib

View file

@ -261,8 +261,8 @@ class ExtractPlayblast(publish.Extractor):
"ext": capture_preset["Codec"]["compression"], "ext": capture_preset["Codec"]["compression"],
"files": collected_files, "files": collected_files,
"stagingDir": stagingdir, "stagingDir": stagingdir,
"frameStart": start, "frameStart": int(start),
"frameEnd": end, "frameEnd": int(end),
"fps": fps, "fps": fps,
"tags": tags, "tags": tags,
"camera_name": camera_node_name "camera_name": camera_node_name

View file

@ -8,7 +8,10 @@ from openpype.client import get_last_version_by_subset_id
from openpype import style from openpype import style
from openpype.pipeline import legacy_io from openpype.pipeline import legacy_io
from openpype.tools.utils.lib import qt_app_context from openpype.tools.utils.lib import qt_app_context
from openpype.hosts.maya.api.lib import assign_look_by_version from openpype.hosts.maya.api.lib import (
assign_look_by_version,
get_main_window
)
from maya import cmds from maya import cmds
# old api for MFileIO # old api for MFileIO
@ -297,9 +300,7 @@ def show():
pass pass
# Get Maya main window # Get Maya main window
top_level_widgets = QtWidgets.QApplication.topLevelWidgets() mainwindow = get_main_window()
mainwindow = next(widget for widget in top_level_widgets
if widget.objectName() == "MayaWindow")
with qt_app_context(): with qt_app_context():
window = MayaLookAssignerWindow(parent=mainwindow) window = MayaLookAssignerWindow(parent=mainwindow)

View file

@ -86,7 +86,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
self._uninstall_menu() self._uninstall_menu()
self._deregister_callbacks() self._deregister_callbacks()
def has_unsaved_changes(self): def workfile_has_unsaved_changes(self):
if not substance_painter.project.is_open(): if not substance_painter.project.is_open():
return False return False

View file

@ -34,6 +34,18 @@ class CreateTextures(Creator):
if not substance_painter.project.is_open(): if not substance_painter.project.is_open():
raise CreatorError("Can't create a Texture Set instance without " raise CreatorError("Can't create a Texture Set instance without "
"an open project.") "an open project.")
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in [
"exportPresetUrl",
"exportFileFormat",
"exportSize",
"exportPadding",
"exportDilationDistance"
]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
instance = self.create_instance_in_context(subset_name, instance = self.create_instance_in_context(subset_name,
instance_data) instance_data)

View file

@ -20,7 +20,7 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin):
if context.data["currentFile"] != current: if context.data["currentFile"] != current:
raise KnownPublishError("Workfile has changed during publishing!") raise KnownPublishError("Workfile has changed during publishing!")
if host.has_unsaved_changes(): if host.workfile_has_unsaved_changes():
self.log.info("Saving current file: {}".format(current)) self.log.info("Saving current file: {}".format(current))
host.save_workfile() host.save_workfile()
else: else:

View file

@ -146,7 +146,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"FTRACK_SERVER", "FTRACK_SERVER",
"AVALON_APP_NAME", "AVALON_APP_NAME",
"OPENPYPE_USERNAME", "OPENPYPE_USERNAME",
"OPENPYPE_VERSION",
"OPENPYPE_SG_USER" "OPENPYPE_SG_USER"
] ]

View file

@ -1,7 +1,6 @@
from copy import deepcopy from copy import deepcopy
import re import re
import os import os
import sys
import json import json
import platform import platform
import contextlib import contextlib
@ -237,12 +236,13 @@ def get_data_subprocess(config_path, data_type):
return json.loads(return_json_data) return json.loads(return_json_data)
def compatible_python(): def compatibility_check():
"""Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper""" """Making sure PyOpenColorIO is importable"""
compatible = False try:
if sys.version[0] == 3 and sys.version[1] >= 9: import PyOpenColorIO # noqa: F401
compatible = True except ImportError:
return compatible return False
return True
def get_ocio_config_colorspaces(config_path): def get_ocio_config_colorspaces(config_path):
@ -257,12 +257,15 @@ def get_ocio_config_colorspaces(config_path):
Returns: Returns:
dict: colorspace and family in couple dict: colorspace and family in couple
""" """
if compatible_python(): if not compatibility_check():
from ..scripts.ocio_wrapper import _get_colorspace_data # python environment is not compatible with PyOpenColorIO
return _get_colorspace_data(config_path) # needs to be run in subprocess
else:
return get_colorspace_data_subprocess(config_path) return get_colorspace_data_subprocess(config_path)
from openpype.scripts.ocio_wrapper import _get_colorspace_data
return _get_colorspace_data(config_path)
def get_colorspace_data_subprocess(config_path): def get_colorspace_data_subprocess(config_path):
"""Get colorspace data via subprocess """Get colorspace data via subprocess
@ -290,12 +293,15 @@ def get_ocio_config_views(config_path):
Returns: Returns:
dict: `display/viewer` and viewer data dict: `display/viewer` and viewer data
""" """
if compatible_python(): if not compatibility_check():
from ..scripts.ocio_wrapper import _get_views_data # python environment is not compatible with PyOpenColorIO
return _get_views_data(config_path) # needs to be run in subprocess
else:
return get_views_data_subprocess(config_path) return get_views_data_subprocess(config_path)
from openpype.scripts.ocio_wrapper import _get_views_data
return _get_views_data(config_path)
def get_views_data_subprocess(config_path): def get_views_data_subprocess(config_path):
"""Get viewers data via subprocess """Get viewers data via subprocess

View file

@ -4,6 +4,11 @@ from .constants import (
PRE_CREATE_THUMBNAIL_KEY, PRE_CREATE_THUMBNAIL_KEY,
) )
from .utils import (
get_last_versions_for_instances,
get_next_versions_for_instances,
)
from .subset_name import ( from .subset_name import (
TaskNotSetError, TaskNotSetError,
get_subset_name_template, get_subset_name_template,
@ -46,6 +51,9 @@ __all__ = (
"DEFAULT_SUBSET_TEMPLATE", "DEFAULT_SUBSET_TEMPLATE",
"PRE_CREATE_THUMBNAIL_KEY", "PRE_CREATE_THUMBNAIL_KEY",
"get_last_versions_for_instances",
"get_next_versions_for_instances",
"TaskNotSetError", "TaskNotSetError",
"get_subset_name_template", "get_subset_name_template",
"get_subset_name", "get_subset_name",

View file

@ -1122,10 +1122,10 @@ class CreatedInstance:
@property @property
def creator_attribute_defs(self): def creator_attribute_defs(self):
"""Attribute defintions defined by creator plugin. """Attribute definitions defined by creator plugin.
Returns: Returns:
List[AbstractAttrDef]: Attribute defitions. List[AbstractAttrDef]: Attribute definitions.
""" """
return self.creator_attributes.attr_defs return self.creator_attributes.attr_defs

View file

@ -1,4 +1,3 @@
import os
import copy import copy
import collections import collections
@ -21,6 +20,7 @@ from openpype.pipeline.plugin_discover import (
) )
from .subset_name import get_subset_name from .subset_name import get_subset_name
from .utils import get_next_versions_for_instances
from .legacy_create import LegacyCreator from .legacy_create import LegacyCreator
@ -483,6 +483,27 @@ class BaseCreator:
thumbnail_path thumbnail_path
) )
def get_next_versions_for_instances(self, instances):
"""Prepare next versions for instances.
This is helper method to receive next possible versions for instances.
It is using context information on instance to receive them, 'asset'
and 'subset'.
Output will contain version by each instance id.
Args:
instances (list[CreatedInstance]): Instances for which to get next
versions.
Returns:
Dict[str, int]: Next versions by instance id.
"""
return get_next_versions_for_instances(
self.create_context.project_name, instances
)
class Creator(BaseCreator): class Creator(BaseCreator):
"""Creator that has more information for artist to show in UI. """Creator that has more information for artist to show in UI.

View file

@ -74,12 +74,12 @@ class LegacyCreator(object):
if not plugin_settings: if not plugin_settings:
return return
print(">>> We have preset for {}".format(plugin_name)) cls.log.debug(">>> We have preset for {}".format(plugin_name))
for option, value in plugin_settings.items(): for option, value in plugin_settings.items():
if option == "enabled" and value is False: if option == "enabled" and value is False:
print(" - is disabled by preset") cls.log.debug(" - is disabled by preset")
else: else:
print(" - setting `{}`: `{}`".format(option, value)) cls.log.debug(" - setting `{}`: `{}`".format(option, value))
setattr(cls, option, value) setattr(cls, option, value)
def process(self): def process(self):

View file

@ -0,0 +1,122 @@
import collections
from openpype.client import get_assets, get_subsets, get_last_versions
def get_last_versions_for_instances(
project_name, instances, use_value_for_missing=False
):
"""Get last versions for instances by their asset and subset name.
Args:
project_name (str): Project name.
instances (list[CreatedInstance]): Instances to get next versions for.
use_value_for_missing (Optional[bool]): Missing values are replaced
with negative value if True. Otherwise None is used. -2 is used
for instances without filled asset or subset name. -1 is used
for missing entities.
Returns:
dict[str, Union[int, None]]: Last versions by instance id.
"""
output = {
instance.id: -1 if use_value_for_missing else None
for instance in instances
}
subset_names_by_asset_name = collections.defaultdict(set)
instances_by_hierarchy = {}
for instance in instances:
asset_name = instance.data.get("asset")
subset_name = instance.subset_name
if not asset_name or not subset_name:
if use_value_for_missing:
output[instance.id] = -2
continue
(
instances_by_hierarchy
.setdefault(asset_name, {})
.setdefault(subset_name, [])
.append(instance)
)
subset_names_by_asset_name[asset_name].add(subset_name)
subset_names = set()
for names in subset_names_by_asset_name.values():
subset_names |= names
if not subset_names:
return output
asset_docs = get_assets(
project_name,
asset_names=subset_names_by_asset_name.keys(),
fields=["name", "_id"]
)
asset_names_by_id = {
asset_doc["_id"]: asset_doc["name"]
for asset_doc in asset_docs
}
if not asset_names_by_id:
return output
subset_docs = get_subsets(
project_name,
asset_ids=asset_names_by_id.keys(),
subset_names=subset_names,
fields=["_id", "name", "parent"]
)
subset_docs_by_id = {}
for subset_doc in subset_docs:
# Filter subset docs by subset names under parent
asset_id = subset_doc["parent"]
asset_name = asset_names_by_id[asset_id]
subset_name = subset_doc["name"]
if subset_name not in subset_names_by_asset_name[asset_name]:
continue
subset_docs_by_id[subset_doc["_id"]] = subset_doc
if not subset_docs_by_id:
return output
last_versions_by_subset_id = get_last_versions(
project_name,
subset_docs_by_id.keys(),
fields=["name", "parent"]
)
for subset_id, version_doc in last_versions_by_subset_id.items():
subset_doc = subset_docs_by_id[subset_id]
asset_id = subset_doc["parent"]
asset_name = asset_names_by_id[asset_id]
_instances = instances_by_hierarchy[asset_name][subset_doc["name"]]
for instance in _instances:
output[instance.id] = version_doc["name"]
return output
def get_next_versions_for_instances(project_name, instances):
"""Get next versions for instances by their asset and subset name.
Args:
project_name (str): Project name.
instances (list[CreatedInstance]): Instances to get next versions for.
Returns:
dict[str, Union[int, None]]: Next versions by instance id. Version is
'None' if instance has no asset or subset name.
"""
last_versions = get_last_versions_for_instances(
project_name, instances, True)
output = {}
for instance_id, version in last_versions.items():
if version == -2:
output[instance_id] = None
elif version == -1:
output[instance_id] = 1
else:
output[instance_id] = version + 1
return output

View file

@ -4,6 +4,8 @@ from .utils import (
LoadError, LoadError,
IncompatibleLoaderError, IncompatibleLoaderError,
InvalidRepresentationContext, InvalidRepresentationContext,
LoaderSwitchNotImplementedError,
LoaderNotFoundError,
get_repres_contexts, get_repres_contexts,
get_contexts_for_repre_docs, get_contexts_for_repre_docs,
@ -55,6 +57,8 @@ __all__ = (
"LoadError", "LoadError",
"IncompatibleLoaderError", "IncompatibleLoaderError",
"InvalidRepresentationContext", "InvalidRepresentationContext",
"LoaderSwitchNotImplementedError",
"LoaderNotFoundError",
"get_repres_contexts", "get_repres_contexts",
"get_contexts_for_repre_docs", "get_contexts_for_repre_docs",

View file

@ -79,6 +79,16 @@ class InvalidRepresentationContext(ValueError):
pass pass
class LoaderSwitchNotImplementedError(NotImplementedError):
"""Error when `switch` is used with Loader that has no implementation."""
pass
class LoaderNotFoundError(RuntimeError):
"""Error when Loader plugin for a loader name is not found."""
pass
def get_repres_contexts(representation_ids, dbcon=None): def get_repres_contexts(representation_ids, dbcon=None):
"""Return parenthood context for representation. """Return parenthood context for representation.
@ -432,7 +442,10 @@ def remove_container(container):
Loader = _get_container_loader(container) Loader = _get_container_loader(container)
if not Loader: if not Loader:
raise RuntimeError("Can't remove container. See log for details.") raise LoaderNotFoundError(
"Can't remove container because loader '{}' was not found."
.format(container.get("loader"))
)
loader = Loader(get_representation_context(container["representation"])) loader = Loader(get_representation_context(container["representation"]))
return loader.remove(container) return loader.remove(container)
@ -480,7 +493,10 @@ def update_container(container, version=-1):
# Run update on the Loader for this container # Run update on the Loader for this container
Loader = _get_container_loader(container) Loader = _get_container_loader(container)
if not Loader: if not Loader:
raise RuntimeError("Can't update container. See log for details.") raise LoaderNotFoundError(
"Can't update container because loader '{}' was not found."
.format(container.get("loader"))
)
loader = Loader(get_representation_context(container["representation"])) loader = Loader(get_representation_context(container["representation"]))
return loader.update(container, new_representation) return loader.update(container, new_representation)
@ -502,15 +518,18 @@ def switch_container(container, representation, loader_plugin=None):
loader_plugin = _get_container_loader(container) loader_plugin = _get_container_loader(container)
if not loader_plugin: if not loader_plugin:
raise RuntimeError("Can't switch container. See log for details.") raise LoaderNotFoundError(
"Can't switch container because loader '{}' was not found."
.format(container.get("loader"))
)
if not hasattr(loader_plugin, "switch"): if not hasattr(loader_plugin, "switch"):
# Backwards compatibility (classes without switch support # Backwards compatibility (classes without switch support
# might be better to just have "switch" raise NotImplementedError # might be better to just have "switch" raise NotImplementedError
# on the base class of Loader\ # on the base class of Loader\
raise RuntimeError("Loader '{}' does not support 'switch'".format( raise LoaderSwitchNotImplementedError(
loader_plugin.label "Loader {} does not support 'switch'".format(loader_plugin.label)
)) )
# Get the new representation to switch to # Get the new representation to switch to
project_name = legacy_io.active_project() project_name = legacy_io.active_project()
@ -520,7 +539,11 @@ def switch_container(container, representation, loader_plugin=None):
new_context = get_representation_context(new_representation) new_context = get_representation_context(new_representation)
if not is_compatible_loader(loader_plugin, new_context): if not is_compatible_loader(loader_plugin, new_context):
raise AssertionError("Must be compatible Loader") raise IncompatibleLoaderError(
"Loader {} is incompatible with {}".format(
loader_plugin.__name__, new_context["subset"]["name"]
)
)
loader = loader_plugin(new_context) loader = loader_plugin(new_context)

View file

@ -1,4 +1,5 @@
import copy import copy
import platform
from collections import defaultdict from collections import defaultdict
from qtpy import QtWidgets, QtCore, QtGui from qtpy import QtWidgets, QtCore, QtGui
@ -83,6 +84,12 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
self.templates = self._get_templates(self.anatomy) self.templates = self._get_templates(self.anatomy)
for name, _ in self.templates.items(): for name, _ in self.templates.items():
dropdown.addItem(name) dropdown.addItem(name)
if self.templates and platform.system() == "Darwin":
# fix macos QCombobox Style
dropdown.setItemDelegate(QtWidgets.QStyledItemDelegate())
# update combo box length to longest entry
longest_key = max(self.templates.keys(), key=len)
dropdown.setMinimumContentsLength(len(longest_key))
template_label = QtWidgets.QLabel() template_label = QtWidgets.QLabel()
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
@ -123,7 +130,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
input_layout.addRow("Representations", repre_checkboxes_layout) input_layout.addRow("Representations", repre_checkboxes_layout)
btn_delivery = QtWidgets.QPushButton("Deliver") btn_delivery = QtWidgets.QPushButton("Deliver")
btn_delivery.setEnabled(bool(dropdown.currentText())) btn_delivery.setEnabled(False)
progress_bar = QtWidgets.QProgressBar(self) progress_bar = QtWidgets.QProgressBar(self)
progress_bar.setMinimum = 0 progress_bar.setMinimum = 0
@ -162,6 +169,15 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
btn_delivery.clicked.connect(self.deliver) btn_delivery.clicked.connect(self.deliver)
dropdown.currentIndexChanged.connect(self._update_template_value) dropdown.currentIndexChanged.connect(self._update_template_value)
if not self.dropdown.count():
self.text_area.setVisible(True)
error_message = (
"No Delivery Templates found!\n"
"Add Template in [project_anatomy/templates/delivery]"
)
self.text_area.setText(error_message)
self.log.error(error_message.replace("\n", " "))
def deliver(self): def deliver(self):
"""Main method to loop through all selected representations""" """Main method to loop through all selected representations"""
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
@ -308,14 +324,17 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
self.files_selected, self.size_selected = \ self.files_selected, self.size_selected = \
self._get_counts(selected_repres) self._get_counts(selected_repres)
self.selected_label.setText(self._prepare_label()) self.selected_label.setText(self._prepare_label())
# update delivery button state if any templates found
if self.dropdown.count():
self.btn_delivery.setEnabled(bool(selected_repres))
def _update_template_value(self, _index=None): def _update_template_value(self, _index=None):
"""Sets template value to label after selection in dropdown.""" """Sets template value to label after selection in dropdown."""
name = self.dropdown.currentText() name = self.dropdown.currentText()
template_value = self.templates.get(name) template_value = self.templates.get(name)
if template_value: if template_value:
self.btn_delivery.setEnabled(True)
self.template_label.setText(template_value) self.template_label.setText(template_value)
self.btn_delivery.setEnabled(bool(self._get_selected_repres()))
def _update_progress(self, uploaded): def _update_progress(self, uploaded):
"""Update progress bar after each repre copied.""" """Update progress bar after each repre copied."""

View file

@ -51,7 +51,8 @@ class ExtractBurnin(publish.Extractor):
"aftereffects", "aftereffects",
"photoshop", "photoshop",
"flame", "flame",
"houdini" "houdini",
"max"
# "resolve" # "resolve"
] ]

View file

@ -49,6 +49,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
"maya", "maya",
"blender", "blender",
"houdini", "houdini",
"max",
"shell", "shell",
"hiero", "hiero",
"premiere", "premiere",

View file

@ -262,7 +262,8 @@
], ],
"hosts": [ "hosts": [
"maya", "maya",
"houdini" "houdini",
"max"
], ],
"task_types": [], "task_types": [],
"task_names": [], "task_names": [],

View file

@ -421,9 +421,9 @@
}, },
"workfile": { "workfile": {
"enabled": false, "enabled": false,
"renderSpace": "ACEScg", "renderSpace": "ACES - ACEScg",
"displayName": "sRGB", "displayName": "ACES",
"viewName": "ACES 1.0 SDR-video" "viewName": "sRGB"
}, },
"colorManagementPreference_v2": { "colorManagementPreference_v2": {
"enabled": true, "enabled": true,

View file

@ -19,6 +19,9 @@ from openpype.pipeline.load import (
switch_container, switch_container,
get_repres_contexts, get_repres_contexts,
loaders_from_repre_context, loaders_from_repre_context,
LoaderSwitchNotImplementedError,
IncompatibleLoaderError,
LoaderNotFoundError
) )
from .widgets import ( from .widgets import (
@ -1298,19 +1301,28 @@ class SwitchAssetDialog(QtWidgets.QDialog):
else: else:
repre_doc = repres_by_name[container_repre_name] repre_doc = repres_by_name[container_repre_name]
error = None
try: try:
switch_container(container, repre_doc, loader) switch_container(container, repre_doc, loader)
except (
LoaderSwitchNotImplementedError,
IncompatibleLoaderError,
LoaderNotFoundError,
) as exc:
error = str(exc)
except Exception: except Exception:
msg = ( error = (
"Switch asset failed. "
"Search console log for more details."
)
if error is not None:
log.warning((
"Couldn't switch asset." "Couldn't switch asset."
"See traceback for more information." "See traceback for more information."
) ), exc_info=True)
log.warning(msg, exc_info=True)
dialog = QtWidgets.QMessageBox(self) dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle("Switch asset failed") dialog.setWindowTitle("Switch asset failed")
dialog.setText( dialog.setText(error)
"Switch asset failed. Search console log for more details"
)
dialog.exec_() dialog.exec_()
self.switched.emit() self.switched.emit()

View file

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