mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
85d526cf34
21 changed files with 972 additions and 459 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.18.2
|
||||
- 3.18.2-nightly.6
|
||||
- 3.18.2-nightly.5
|
||||
- 3.18.2-nightly.4
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.15.6-nightly.2
|
||||
- 3.15.6-nightly.1
|
||||
- 3.15.5
|
||||
- 3.15.5-nightly.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -294,6 +294,37 @@ def reset_frame_range(fps: bool = True):
|
|||
frame_range["frameStartHandle"], frame_range["frameEndHandle"])
|
||||
|
||||
|
||||
def reset_unit_scale():
|
||||
"""Apply the unit scale setting to 3dsMax
|
||||
"""
|
||||
project_name = get_current_project_name()
|
||||
settings = get_project_settings(project_name).get("max")
|
||||
scene_scale = settings.get("unit_scale_settings",
|
||||
{}).get("scene_unit_scale")
|
||||
if scene_scale:
|
||||
rt.units.DisplayType = rt.Name("Metric")
|
||||
rt.units.MetricType = rt.Name(scene_scale)
|
||||
else:
|
||||
rt.units.DisplayType = rt.Name("Generic")
|
||||
|
||||
|
||||
def convert_unit_scale():
|
||||
"""Convert system unit scale in 3dsMax
|
||||
for fbx export
|
||||
|
||||
Returns:
|
||||
str: unit scale
|
||||
"""
|
||||
unit_scale_dict = {
|
||||
"millimeters": "mm",
|
||||
"centimeters": "cm",
|
||||
"meters": "m",
|
||||
"kilometers": "km"
|
||||
}
|
||||
current_unit_scale = rt.Execute("units.MetricType as string")
|
||||
return unit_scale_dict[current_unit_scale]
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
"""Apply the project settings from the project definition
|
||||
|
||||
|
|
@ -310,6 +341,7 @@ def set_context_setting():
|
|||
reset_scene_resolution()
|
||||
reset_frame_range()
|
||||
reset_colorspace()
|
||||
reset_unit_scale()
|
||||
|
||||
|
||||
def get_max_version():
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ class OpenPypeMenu(object):
|
|||
colorspace_action.triggered.connect(self.colorspace_callback)
|
||||
openpype_menu.addAction(colorspace_action)
|
||||
|
||||
unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu)
|
||||
unit_scale_action.triggered.connect(self.unit_scale_callback)
|
||||
openpype_menu.addAction(unit_scale_action)
|
||||
|
||||
return openpype_menu
|
||||
|
||||
def load_callback(self):
|
||||
|
|
@ -157,3 +161,7 @@ class OpenPypeMenu(object):
|
|||
def colorspace_callback(self):
|
||||
"""Callback to reset colorspace"""
|
||||
return lib.reset_colorspace()
|
||||
|
||||
def unit_scale_callback(self):
|
||||
"""Callback to reset unit scale"""
|
||||
return lib.reset_unit_scale()
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.hosts.max.api import maintained_selection
|
||||
from openpype.pipeline import OptionalPyblishPluginMixin, publish
|
||||
|
||||
|
||||
class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
"""Extract Camera with FbxExporter."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Fbx Camera"
|
||||
hosts = ["max"]
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
rt.FBXExporterSetParam("Animation", True)
|
||||
rt.FBXExporterSetParam("Cameras", True)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.ExportFile(
|
||||
filepath,
|
||||
rt.Name("noPrompt"),
|
||||
selectedOnly=True,
|
||||
using=rt.FBXEXP,
|
||||
)
|
||||
|
||||
self.log.info("Performing Extraction ...")
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": "fbx",
|
||||
"ext": "fbx",
|
||||
"files": filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info(f"Extracted instance '{instance.name}' to: {filepath}")
|
||||
|
|
@ -3,6 +3,7 @@ import pyblish.api
|
|||
from openpype.pipeline import publish, OptionalPyblishPluginMixin
|
||||
from pymxs import runtime as rt
|
||||
from openpype.hosts.max.api import maintained_selection
|
||||
from openpype.hosts.max.api.lib import convert_unit_scale
|
||||
|
||||
|
||||
class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
|
|
@ -23,14 +24,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
rt.FBXExporterSetParam("Animation", False)
|
||||
rt.FBXExporterSetParam("Cameras", False)
|
||||
rt.FBXExporterSetParam("Lights", False)
|
||||
rt.FBXExporterSetParam("PointCache", False)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
self._set_fbx_attributes()
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
|
|
@ -56,3 +50,34 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
self.log.info(
|
||||
"Extracted instance '%s' to: %s" % (instance.name, filepath)
|
||||
)
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", False)
|
||||
rt.FBXExporterSetParam("Cameras", False)
|
||||
rt.FBXExporterSetParam("Lights", False)
|
||||
rt.FBXExporterSetParam("PointCache", False)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
|
||||
|
||||
class ExtractCameraFbx(ExtractModelFbx):
|
||||
"""Extract Camera with FbxExporter."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Fbx Camera"
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", True)
|
||||
rt.FBXExporterSetParam("Cameras", True)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
139
openpype/hosts/maya/api/exitstack.py
Normal file
139
openpype/hosts/maya/api/exitstack.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Backwards compatible implementation of ExitStack for Python 2.
|
||||
|
||||
ExitStack contextmanager was implemented with Python 3.3.
|
||||
As long as we supportPython 2 hosts we can use this backwards
|
||||
compatible implementation to support bothPython 2 and Python 3.
|
||||
|
||||
Instead of using ExitStack from contextlib, use it from this module:
|
||||
|
||||
>>> from openpype.hosts.maya.api.exitstack import ExitStack
|
||||
|
||||
It will provide the appropriate ExitStack implementation for the current
|
||||
running Python version.
|
||||
|
||||
"""
|
||||
# TODO: Remove the entire script once dropping Python 2 support.
|
||||
import contextlib
|
||||
if getattr(contextlib, "nested", None):
|
||||
from contextlib import ExitStack # noqa
|
||||
else:
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
class ExitStack(object):
|
||||
|
||||
"""Context manager for dynamic management of a stack of exit callbacks
|
||||
|
||||
For example:
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(fname))
|
||||
for fname in filenames]
|
||||
# All opened files will automatically be closed at the end of
|
||||
# the with statement, even if attempts to open files later
|
||||
# in the list raise an exception
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self._exit_callbacks = deque()
|
||||
|
||||
def pop_all(self):
|
||||
"""Preserve the context stack by transferring
|
||||
it to a new instance"""
|
||||
new_stack = type(self)()
|
||||
new_stack._exit_callbacks = self._exit_callbacks
|
||||
self._exit_callbacks = deque()
|
||||
return new_stack
|
||||
|
||||
def _push_cm_exit(self, cm, cm_exit):
|
||||
"""Helper to correctly register callbacks
|
||||
to __exit__ methods"""
|
||||
def _exit_wrapper(*exc_details):
|
||||
return cm_exit(cm, *exc_details)
|
||||
_exit_wrapper.__self__ = cm
|
||||
self.push(_exit_wrapper)
|
||||
|
||||
def push(self, exit):
|
||||
"""Registers a callback with the standard __exit__ method signature
|
||||
|
||||
Can suppress exceptions the same way __exit__ methods can.
|
||||
|
||||
Also accepts any object with an __exit__ method (registering a call
|
||||
to the method instead of the object itself)
|
||||
"""
|
||||
# We use an unbound method rather than a bound method to follow
|
||||
# the standard lookup behaviour for special methods
|
||||
_cb_type = type(exit)
|
||||
try:
|
||||
exit_method = _cb_type.__exit__
|
||||
except AttributeError:
|
||||
# Not a context manager, so assume its a callable
|
||||
self._exit_callbacks.append(exit)
|
||||
else:
|
||||
self._push_cm_exit(exit, exit_method)
|
||||
return exit # Allow use as a decorator
|
||||
|
||||
def callback(self, callback, *args, **kwds):
|
||||
"""Registers an arbitrary callback and arguments.
|
||||
|
||||
Cannot suppress exceptions.
|
||||
"""
|
||||
def _exit_wrapper(exc_type, exc, tb):
|
||||
callback(*args, **kwds)
|
||||
# We changed the signature, so using @wraps is not appropriate, but
|
||||
# setting __wrapped__ may still help with introspection
|
||||
_exit_wrapper.__wrapped__ = callback
|
||||
self.push(_exit_wrapper)
|
||||
return callback # Allow use as a decorator
|
||||
|
||||
def enter_context(self, cm):
|
||||
"""Enters the supplied context manager
|
||||
|
||||
If successful, also pushes its __exit__ method as a callback and
|
||||
returns the result of the __enter__ method.
|
||||
"""
|
||||
# We look up the special methods on the type to
|
||||
# match the with statement
|
||||
_cm_type = type(cm)
|
||||
_exit = _cm_type.__exit__
|
||||
result = _cm_type.__enter__(cm)
|
||||
self._push_cm_exit(cm, _exit)
|
||||
return result
|
||||
|
||||
def close(self):
|
||||
"""Immediately unwind the context stack"""
|
||||
self.__exit__(None, None, None)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_details):
|
||||
# We manipulate the exception state so it behaves as though
|
||||
# we were actually nesting multiple with statements
|
||||
frame_exc = sys.exc_info()[1]
|
||||
|
||||
def _fix_exception_context(new_exc, old_exc):
|
||||
while 1:
|
||||
exc_context = new_exc.__context__
|
||||
if exc_context in (None, frame_exc):
|
||||
break
|
||||
new_exc = exc_context
|
||||
new_exc.__context__ = old_exc
|
||||
|
||||
# Callbacks are invoked in LIFO order to match the behaviour of
|
||||
# nested context managers
|
||||
suppressed_exc = False
|
||||
while self._exit_callbacks:
|
||||
cb = self._exit_callbacks.pop()
|
||||
try:
|
||||
if cb(*exc_details):
|
||||
suppressed_exc = True
|
||||
exc_details = (None, None, None)
|
||||
except Exception:
|
||||
new_exc_details = sys.exc_info()
|
||||
# simulate the stack of exceptions by setting the context
|
||||
_fix_exception_context(new_exc_details[1], exc_details[1])
|
||||
if not self._exit_callbacks:
|
||||
raise
|
||||
exc_details = new_exc_details
|
||||
return suppressed_exc
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""Standalone helper functions"""
|
||||
|
||||
import os
|
||||
import copy
|
||||
from pprint import pformat
|
||||
import sys
|
||||
import uuid
|
||||
|
|
@ -9,6 +10,8 @@ import re
|
|||
import json
|
||||
import logging
|
||||
import contextlib
|
||||
import capture
|
||||
from .exitstack import ExitStack
|
||||
from collections import OrderedDict, defaultdict
|
||||
from math import ceil
|
||||
from six import string_types
|
||||
|
|
@ -172,6 +175,216 @@ def maintained_selection():
|
|||
cmds.select(clear=True)
|
||||
|
||||
|
||||
def reload_all_udim_tile_previews():
|
||||
"""Regenerate all UDIM tile preview in texture file"""
|
||||
for texture_file in cmds.ls(type="file"):
|
||||
if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0:
|
||||
cmds.ogs(regenerateUVTilePreview=texture_file)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def panel_camera(panel, camera):
|
||||
"""Set modelPanel's camera during the context.
|
||||
|
||||
Arguments:
|
||||
panel (str): modelPanel name.
|
||||
camera (str): camera name.
|
||||
|
||||
"""
|
||||
original_camera = cmds.modelPanel(panel, query=True, camera=True)
|
||||
try:
|
||||
cmds.modelPanel(panel, edit=True, camera=camera)
|
||||
yield
|
||||
finally:
|
||||
cmds.modelPanel(panel, edit=True, camera=original_camera)
|
||||
|
||||
|
||||
def render_capture_preset(preset):
|
||||
"""Capture playblast with a preset.
|
||||
|
||||
To generate the preset use `generate_capture_preset`.
|
||||
|
||||
Args:
|
||||
preset (dict): preset options
|
||||
|
||||
Returns:
|
||||
str: Output path of `capture.capture`
|
||||
"""
|
||||
|
||||
# Force a refresh at the start of the timeline
|
||||
# TODO (Question): Why do we need to do this? What bug does it solve?
|
||||
# Is this for simulations?
|
||||
cmds.refresh(force=True)
|
||||
refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True))
|
||||
cmds.currentTime(refresh_frame_int - 1, edit=True)
|
||||
cmds.currentTime(refresh_frame_int, edit=True)
|
||||
log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
preset = copy.deepcopy(preset)
|
||||
# not supported by `capture` so we pop it off of the preset
|
||||
reload_textures = preset["viewport_options"].pop("loadTextures", False)
|
||||
panel = preset.pop("panel")
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(maintained_time())
|
||||
stack.enter_context(panel_camera(panel, preset["camera"]))
|
||||
stack.enter_context(viewport_default_options(panel, preset))
|
||||
if reload_textures:
|
||||
# Force immediate texture loading when to ensure
|
||||
# all textures have loaded before the playblast starts
|
||||
stack.enter_context(material_loading_mode(mode="immediate"))
|
||||
# Regenerate all UDIM tiles previews
|
||||
reload_all_udim_tile_previews()
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def generate_capture_preset(instance, camera, path,
|
||||
start=None, end=None, capture_preset=None):
|
||||
"""Function for getting all the data of preset options for
|
||||
playblast capturing
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): instance
|
||||
camera (str): review camera
|
||||
path (str): filepath
|
||||
start (int): frameStart
|
||||
end (int): frameEnd
|
||||
capture_preset (dict): capture preset
|
||||
|
||||
Returns:
|
||||
dict: Resulting preset
|
||||
"""
|
||||
preset = load_capture_preset(data=capture_preset)
|
||||
|
||||
preset["camera"] = camera
|
||||
preset["start_frame"] = start
|
||||
preset["end_frame"] = end
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
preset["panel"] = instance.data["panel"]
|
||||
|
||||
# Disable viewer since we use the rendering logic for publishing
|
||||
# We don't want to open the generated playblast in a viewer directly.
|
||||
preset["viewer"] = False
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
|
||||
# Use resolution from instance if review width/height is set
|
||||
# Otherwise use the resolution from preset if it has non-zero values
|
||||
# Otherwise fall back to asset width x height
|
||||
# Else define no width, then `capture.capture` will use render resolution
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Override camera options
|
||||
# Enforce persisting camera depth of field
|
||||
camera_options = preset.setdefault("camera_options", {})
|
||||
camera_options["depthOfField"] = cmds.getAttr(
|
||||
"{0}.depthOfField".format(camera)
|
||||
)
|
||||
|
||||
# Use Pan/Zoom from instance data instead of from preset
|
||||
preset.pop("pan_zoom", None)
|
||||
camera_options["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
# Override viewport options by instance data
|
||||
viewport_options = preset.setdefault("viewport_options", {})
|
||||
viewport_options["displayLights"] = instance.data["displayLights"]
|
||||
viewport_options["imagePlane"] = instance.data.get("imagePlane", True)
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not capture_preset["Viewport Options"]["override_viewport_options"]:
|
||||
panel_preset = capture.parse_view(preset["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
|
||||
return preset
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_default_options(panel, preset):
|
||||
"""Context manager used by `render_capture_preset`.
|
||||
|
||||
We need to explicitly enable some viewport changes so the viewport is
|
||||
refreshed ahead of playblasting.
|
||||
|
||||
"""
|
||||
# TODO: Clarify in the docstring WHY we need to set it ahead of
|
||||
# playblasting. What issues does it solve?
|
||||
viewport_defaults = {}
|
||||
try:
|
||||
keys = [
|
||||
"useDefaultMaterial",
|
||||
"wireframeOnShaded",
|
||||
"xray",
|
||||
"jointXray",
|
||||
"backfaceCulling",
|
||||
"textures"
|
||||
]
|
||||
for key in keys:
|
||||
viewport_defaults[key] = cmds.modelEditor(
|
||||
panel, query=True, **{key: True}
|
||||
)
|
||||
if preset["viewport_options"].get(key):
|
||||
cmds.modelEditor(
|
||||
panel, edit=True, **{key: True}
|
||||
)
|
||||
yield
|
||||
finally:
|
||||
# Restoring viewport options.
|
||||
if viewport_defaults:
|
||||
cmds.modelEditor(
|
||||
panel, edit=True, **viewport_defaults
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def material_loading_mode(mode="immediate"):
|
||||
"""Set material loading mode during context"""
|
||||
original = cmds.displayPref(query=True, materialLoadingMode=True)
|
||||
cmds.displayPref(materialLoadingMode=mode)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cmds.displayPref(materialLoadingMode=original)
|
||||
|
||||
|
||||
def get_namespace(node):
|
||||
"""Return namespace of given node"""
|
||||
node_name = node.rsplit("|", 1)[-1]
|
||||
|
|
@ -2677,7 +2890,7 @@ def bake_to_world_space(nodes,
|
|||
return world_space_nodes
|
||||
|
||||
|
||||
def load_capture_preset(data=None):
|
||||
def load_capture_preset(data):
|
||||
"""Convert OpenPype Extract Playblast settings to `capture` arguments
|
||||
|
||||
Input data is the settings from:
|
||||
|
|
@ -2691,8 +2904,6 @@ def load_capture_preset(data=None):
|
|||
|
||||
"""
|
||||
|
||||
import capture
|
||||
|
||||
options = dict()
|
||||
viewport_options = dict()
|
||||
viewport2_options = dict()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
import clique
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
|
@ -11,16 +8,6 @@ from openpype.hosts.maya.api import lib
|
|||
from maya import cmds
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def panel_camera(panel, camera):
|
||||
original_camera = cmds.modelPanel(panel, query=True, camera=True)
|
||||
try:
|
||||
cmds.modelPanel(panel, edit=True, camera=camera)
|
||||
yield
|
||||
finally:
|
||||
cmds.modelPanel(panel, edit=True, camera=original_camera)
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
"""Extract viewport playblast.
|
||||
|
||||
|
|
@ -36,19 +23,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
capture_preset = {}
|
||||
profiles = None
|
||||
|
||||
def _capture(self, preset):
|
||||
if os.environ.get("OPENPYPE_DEBUG") == "1":
|
||||
self.log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
self.log.debug("playblast path {}".format(path))
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Extracting capture..")
|
||||
self.log.debug("Extracting playblast..")
|
||||
|
||||
# get scene fps
|
||||
fps = instance.data.get("fps") or instance.context.data.get("fps")
|
||||
|
|
@ -63,10 +39,6 @@ class ExtractPlayblast(publish.Extractor):
|
|||
end = cmds.playbackOptions(query=True, animationEndTime=True)
|
||||
|
||||
self.log.debug("start: {}, end: {}".format(start, end))
|
||||
|
||||
# get cameras
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
task_data = instance.data["anatomyData"].get("task", {})
|
||||
capture_preset = lib.get_capture_preset(
|
||||
task_data.get("name"),
|
||||
|
|
@ -75,174 +47,35 @@ class ExtractPlayblast(publish.Extractor):
|
|||
instance.context.data["project_settings"],
|
||||
self.log
|
||||
)
|
||||
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
preset["camera"] = camera
|
||||
|
||||
# Tests if project resolution is set,
|
||||
# if it is a value other than zero, that value is
|
||||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
preset["start_frame"] = start
|
||||
preset["end_frame"] = end
|
||||
|
||||
# Enforce persisting camera depth of field
|
||||
camera_options = preset.setdefault("camera_options", {})
|
||||
camera_options["depthOfField"] = cmds.getAttr(
|
||||
"{0}.depthOfField".format(camera))
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{0}".format(instance.name)
|
||||
filename = instance.name
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
# get cameras
|
||||
camera = instance.data["review_camera"]
|
||||
preset = lib.generate_capture_preset(
|
||||
instance, camera, path,
|
||||
start=start, end=end,
|
||||
capture_preset=capture_preset)
|
||||
lib.render_capture_preset(preset)
|
||||
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Use displayLights setting from instance
|
||||
key = "displayLights"
|
||||
preset["viewport_options"][key] = instance.data[key]
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Show/Hide image planes on request.
|
||||
image_plane = instance.data.get("imagePlane", True)
|
||||
if "viewport_options" in preset:
|
||||
preset["viewport_options"]["imagePlane"] = image_plane
|
||||
else:
|
||||
preset["viewport_options"] = {"imagePlane": image_plane}
|
||||
|
||||
# Disable Pan/Zoom.
|
||||
pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"]))
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
# Need to explicitly enable some viewport changes so the viewport is
|
||||
# refreshed ahead of playblasting.
|
||||
keys = [
|
||||
"useDefaultMaterial",
|
||||
"wireframeOnShaded",
|
||||
"xray",
|
||||
"jointXray",
|
||||
"backfaceCulling"
|
||||
]
|
||||
viewport_defaults = {}
|
||||
for key in keys:
|
||||
viewport_defaults[key] = cmds.modelEditor(
|
||||
instance.data["panel"], query=True, **{key: True}
|
||||
)
|
||||
if preset["viewport_options"][key]:
|
||||
cmds.modelEditor(
|
||||
instance.data["panel"], edit=True, **{key: True}
|
||||
)
|
||||
|
||||
override_viewport_options = (
|
||||
capture_preset["Viewport Options"]["override_viewport_options"]
|
||||
)
|
||||
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not override_viewport_options:
|
||||
panel_preset = capture.parse_view(instance.data["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
|
||||
# Need to ensure Python 2 compatibility.
|
||||
# TODO: Remove once dropping Python 2.
|
||||
if getattr(contextlib, "nested", None):
|
||||
# Python 3 compatibility.
|
||||
with contextlib.nested(
|
||||
lib.maintained_time(),
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
):
|
||||
self._capture(preset)
|
||||
else:
|
||||
# Python 2 compatibility.
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(lib.maintained_time())
|
||||
stack.enter_context(
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
)
|
||||
|
||||
self._capture(preset)
|
||||
|
||||
# Restoring viewport options.
|
||||
if viewport_defaults:
|
||||
cmds.modelEditor(
|
||||
instance.data["panel"], edit=True, **viewport_defaults
|
||||
)
|
||||
|
||||
try:
|
||||
cmds.setAttr(
|
||||
"{}.panZoomEnabled".format(preset["camera"]), pan_zoom)
|
||||
except RuntimeError:
|
||||
self.log.warning("Cannot restore Pan/Zoom settings.")
|
||||
|
||||
# Find playblast sequence
|
||||
collected_files = os.listdir(stagingdir)
|
||||
patterns = [clique.PATTERNS["frames"]]
|
||||
collections, remainder = clique.assemble(collected_files,
|
||||
minimum_items=1,
|
||||
patterns=patterns)
|
||||
|
||||
filename = preset.get("filename", "%TEMP%")
|
||||
self.log.debug("filename {}".format(filename))
|
||||
self.log.debug("Searching playblast collection for: %s", path)
|
||||
frame_collection = None
|
||||
for collection in collections:
|
||||
filebase = collection.format("{head}").rstrip(".")
|
||||
self.log.debug("collection head {}".format(filebase))
|
||||
if filebase in filename:
|
||||
self.log.debug("Checking collection head: %s", filebase)
|
||||
if filebase in path:
|
||||
frame_collection = collection
|
||||
self.log.debug(
|
||||
"we found collection of interest {}".format(
|
||||
str(frame_collection)))
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
"Found playblast collection: %s", frame_collection
|
||||
)
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
|
|
@ -256,6 +89,9 @@ class ExtractPlayblast(publish.Extractor):
|
|||
if len(collected_files) == 1:
|
||||
collected_files = collected_files[0]
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": capture_preset["Codec"]["compression"],
|
||||
"ext": capture_preset["Codec"]["compression"],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import os
|
||||
import glob
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""Extract viewport thumbnail.
|
||||
|
|
@ -24,7 +19,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Extracting capture..")
|
||||
self.log.debug("Extracting thumbnail..")
|
||||
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
|
|
@ -37,20 +32,24 @@ class ExtractThumbnail(publish.Extractor):
|
|||
self.log
|
||||
)
|
||||
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
override_viewport_options = (
|
||||
capture_preset["Viewport Options"]["override_viewport_options"]
|
||||
# Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_thumbnail")
|
||||
self.log.debug(
|
||||
"Create temp directory {} for thumbnail".format(dst_staging)
|
||||
)
|
||||
# Store new staging to cleanup paths
|
||||
filename = instance.name
|
||||
path = os.path.join(dst_staging, filename)
|
||||
|
||||
preset["camera"] = camera
|
||||
preset["start_frame"] = instance.data["frameStart"]
|
||||
preset["end_frame"] = instance.data["frameStart"]
|
||||
preset["camera_options"] = {
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
|
||||
preset = lib.generate_capture_preset(
|
||||
instance, camera, path,
|
||||
start=1, end=1,
|
||||
capture_preset=capture_preset)
|
||||
|
||||
preset["camera_options"].update({
|
||||
"displayGateMask": False,
|
||||
"displayResolution": False,
|
||||
"displayFilmGate": False,
|
||||
|
|
@ -60,101 +59,10 @@ class ExtractThumbnail(publish.Extractor):
|
|||
"displayFilmPivot": False,
|
||||
"displayFilmOrigin": False,
|
||||
"overscan": 1.0,
|
||||
"depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)),
|
||||
}
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
# Tests if project resolution is set,
|
||||
# if it is a value other than zero, that value is
|
||||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
})
|
||||
path = lib.render_capture_preset(preset)
|
||||
|
||||
# Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
self.log.debug(
|
||||
"Create temp directory {} for thumbnail".format(dst_staging)
|
||||
)
|
||||
# Store new staging to cleanup paths
|
||||
filename = "{0}".format(instance.name)
|
||||
path = os.path.join(dst_staging, filename)
|
||||
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Use displayLights setting from instance
|
||||
key = "displayLights"
|
||||
preset["viewport_options"][key] = instance.data[key]
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Show or Hide Image Plane
|
||||
image_plane = instance.data.get("imagePlane", True)
|
||||
if "viewport_options" in preset:
|
||||
preset["viewport_options"]["imagePlane"] = image_plane
|
||||
else:
|
||||
preset["viewport_options"] = {"imagePlane": image_plane}
|
||||
|
||||
# Disable Pan/Zoom.
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
with lib.maintained_time():
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
panel = cmds.getPanel(withFocus=True) or ""
|
||||
if not override_viewport_options and "modelPanel" in panel:
|
||||
panel_preset = capture.parse_active_view()
|
||||
preset.update(panel_preset)
|
||||
cmds.setFocus(panel)
|
||||
|
||||
if os.environ.get("OPENPYPE_DEBUG") == "1":
|
||||
self.log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(**preset)
|
||||
playblast = self._fix_playblast_output_path(path)
|
||||
playblast = self._fix_playblast_output_path(path)
|
||||
|
||||
_, thumbnail = os.path.split(playblast)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,113 @@ class MissingEventSystem(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def _get_func_ref(func):
|
||||
if inspect.ismethod(func):
|
||||
return WeakMethod(func)
|
||||
return weakref.ref(func)
|
||||
|
||||
|
||||
def _get_func_info(func):
|
||||
path = "<unknown path>"
|
||||
if func is None:
|
||||
return "<unknown>", path
|
||||
|
||||
if hasattr(func, "__name__"):
|
||||
name = func.__name__
|
||||
else:
|
||||
name = str(func)
|
||||
|
||||
# Get path to file and fallback to '<unknown path>' if fails
|
||||
# NOTE This was added because of 'partial' functions which is handled,
|
||||
# but who knows what else can cause this to fail?
|
||||
try:
|
||||
path = os.path.abspath(inspect.getfile(func))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return name, path
|
||||
|
||||
|
||||
class weakref_partial:
|
||||
"""Partial function with weak reference to the wrapped function.
|
||||
|
||||
Can be used as 'functools.partial' but it will store weak reference to
|
||||
function. That means that the function must be reference counted
|
||||
to avoid garbage collecting the function itself.
|
||||
|
||||
When the referenced functions is garbage collected then calling the
|
||||
weakref partial (no matter the args/kwargs passed) will do nothing.
|
||||
It will fail silently, returning `None`. The `is_valid()` method can
|
||||
be used to detect whether the reference is still valid.
|
||||
|
||||
Is useful for object methods. In that case the callback is
|
||||
deregistered when object is destroyed.
|
||||
|
||||
Warnings:
|
||||
Values passed as *args and **kwargs are stored strongly in memory.
|
||||
That may "keep alive" objects that should be already destroyed.
|
||||
It is recommended to pass only immutable objects like 'str',
|
||||
'bool', 'int' etc.
|
||||
|
||||
Args:
|
||||
func (Callable): Function to wrap.
|
||||
*args: Arguments passed to the wrapped function.
|
||||
**kwargs: Keyword arguments passed to the wrapped function.
|
||||
"""
|
||||
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func_ref = _get_func_ref(func)
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
func = self._func_ref()
|
||||
if func is None:
|
||||
return
|
||||
|
||||
new_args = tuple(list(self._args) + list(args))
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(kwargs)
|
||||
return func(*new_args, **new_kwargs)
|
||||
|
||||
def get_func(self):
|
||||
"""Get wrapped function.
|
||||
|
||||
Returns:
|
||||
Union[Callable, None]: Wrapped function or None if it was
|
||||
destroyed.
|
||||
"""
|
||||
|
||||
return self._func_ref()
|
||||
|
||||
def is_valid(self):
|
||||
"""Check if wrapped function is still valid.
|
||||
|
||||
Returns:
|
||||
bool: Is wrapped function still valid.
|
||||
"""
|
||||
|
||||
return self._func_ref() is not None
|
||||
|
||||
def validate_signature(self, *args, **kwargs):
|
||||
"""Validate if passed arguments are supported by wrapped function.
|
||||
|
||||
Returns:
|
||||
bool: Are passed arguments supported by wrapped function.
|
||||
"""
|
||||
|
||||
func = self._func_ref()
|
||||
if func is None:
|
||||
return False
|
||||
|
||||
new_args = tuple(list(self._args) + list(args))
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(kwargs)
|
||||
return is_func_signature_supported(
|
||||
func, *new_args, **new_kwargs
|
||||
)
|
||||
|
||||
|
||||
class EventCallback(object):
|
||||
"""Callback registered to a topic.
|
||||
|
||||
|
|
@ -34,20 +141,37 @@ class EventCallback(object):
|
|||
or none arguments. When 1 argument is expected then the processed 'Event'
|
||||
object is passed in.
|
||||
|
||||
The registered callbacks don't keep function in memory so it is not
|
||||
possible to store lambda function as callback.
|
||||
The callbacks are validated against their reference counter, that is
|
||||
achieved using 'weakref' module. That means that the callback must
|
||||
be stored in memory somewhere. e.g. lambda functions are not
|
||||
supported as valid callback.
|
||||
|
||||
You can use 'weakref_partial' functions. In that case is partial object
|
||||
stored in the callback object and reference counter is checked for
|
||||
the wrapped function.
|
||||
|
||||
Args:
|
||||
topic(str): Topic which will be listened.
|
||||
func(func): Callback to a topic.
|
||||
topic (str): Topic which will be listened.
|
||||
func (Callable): Callback to a topic.
|
||||
order (Union[int, None]): Order of callback. Lower number means higher
|
||||
priority.
|
||||
|
||||
Raises:
|
||||
TypeError: When passed function is not a callable object.
|
||||
"""
|
||||
|
||||
def __init__(self, topic, func):
|
||||
def __init__(self, topic, func, order):
|
||||
if not callable(func):
|
||||
raise TypeError((
|
||||
"Registered callback is not callable. \"{}\""
|
||||
).format(str(func)))
|
||||
|
||||
self._validate_order(order)
|
||||
|
||||
self._log = None
|
||||
self._topic = topic
|
||||
self._order = order
|
||||
self._enabled = True
|
||||
# Replace '*' with any character regex and escape rest of text
|
||||
# - when callback is registered for '*' topic it will receive all
|
||||
# events
|
||||
|
|
@ -63,37 +187,38 @@ class EventCallback(object):
|
|||
topic_regex = re.compile(topic_regex_str)
|
||||
self._topic_regex = topic_regex
|
||||
|
||||
# Convert callback into references
|
||||
# - deleted functions won't cause crashes
|
||||
if inspect.ismethod(func):
|
||||
func_ref = WeakMethod(func)
|
||||
elif callable(func):
|
||||
func_ref = weakref.ref(func)
|
||||
# Callback function prep
|
||||
if isinstance(func, weakref_partial):
|
||||
partial_func = func
|
||||
(name, path) = _get_func_info(func.get_func())
|
||||
func_ref = None
|
||||
expect_args = partial_func.validate_signature("fake")
|
||||
expect_kwargs = partial_func.validate_signature(event="fake")
|
||||
|
||||
else:
|
||||
raise TypeError((
|
||||
"Registered callback is not callable. \"{}\""
|
||||
).format(str(func)))
|
||||
partial_func = None
|
||||
(name, path) = _get_func_info(func)
|
||||
# Convert callback into references
|
||||
# - deleted functions won't cause crashes
|
||||
func_ref = _get_func_ref(func)
|
||||
|
||||
# Collect function name and path to file for logging
|
||||
func_name = func.__name__
|
||||
func_path = os.path.abspath(inspect.getfile(func))
|
||||
|
||||
# Get expected arguments from function spec
|
||||
# - positional arguments are always preferred
|
||||
expect_args = is_func_signature_supported(func, "fake")
|
||||
expect_kwargs = is_func_signature_supported(func, event="fake")
|
||||
# Get expected arguments from function spec
|
||||
# - positional arguments are always preferred
|
||||
expect_args = is_func_signature_supported(func, "fake")
|
||||
expect_kwargs = is_func_signature_supported(func, event="fake")
|
||||
|
||||
self._func_ref = func_ref
|
||||
self._func_name = func_name
|
||||
self._func_path = func_path
|
||||
self._partial_func = partial_func
|
||||
self._ref_is_valid = True
|
||||
self._expect_args = expect_args
|
||||
self._expect_kwargs = expect_kwargs
|
||||
self._ref_valid = func_ref is not None
|
||||
self._enabled = True
|
||||
|
||||
self._name = name
|
||||
self._path = path
|
||||
|
||||
def __repr__(self):
|
||||
return "< {} - {} > {}".format(
|
||||
self.__class__.__name__, self._func_name, self._func_path
|
||||
self.__class__.__name__, self._name, self._path
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -104,32 +229,83 @@ class EventCallback(object):
|
|||
|
||||
@property
|
||||
def is_ref_valid(self):
|
||||
return self._ref_valid
|
||||
"""
|
||||
|
||||
Returns:
|
||||
bool: Is reference to callback valid.
|
||||
"""
|
||||
|
||||
self._validate_ref()
|
||||
return self._ref_is_valid
|
||||
|
||||
def validate_ref(self):
|
||||
if not self._ref_valid:
|
||||
return
|
||||
"""Validate if reference to callback is valid.
|
||||
|
||||
callback = self._func_ref()
|
||||
if not callback:
|
||||
self._ref_valid = False
|
||||
Deprecated:
|
||||
Reference is always live checkd with 'is_ref_valid'.
|
||||
"""
|
||||
|
||||
# Trigger validate by getting 'is_valid'
|
||||
_ = self.is_ref_valid
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Is callback enabled."""
|
||||
"""Is callback enabled.
|
||||
|
||||
Returns:
|
||||
bool: Is callback enabled.
|
||||
"""
|
||||
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Change if callback is enabled."""
|
||||
"""Change if callback is enabled.
|
||||
|
||||
Args:
|
||||
enabled (bool): Change enabled state of the callback.
|
||||
"""
|
||||
|
||||
self._enabled = enabled
|
||||
|
||||
def deregister(self):
|
||||
"""Calling this function will cause that callback will be removed."""
|
||||
# Fake reference
|
||||
self._ref_valid = False
|
||||
|
||||
self._ref_is_valid = False
|
||||
self._partial_func = None
|
||||
self._func_ref = None
|
||||
|
||||
def get_order(self):
|
||||
"""Get callback order.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Callback order.
|
||||
"""
|
||||
|
||||
return self._order
|
||||
|
||||
def set_order(self, order):
|
||||
"""Change callback order.
|
||||
|
||||
Args:
|
||||
order (Union[int, None]): Order of callback. Lower number means
|
||||
higher priority.
|
||||
"""
|
||||
|
||||
self._validate_order(order)
|
||||
self._order = order
|
||||
|
||||
order = property(get_order, set_order)
|
||||
|
||||
def topic_matches(self, topic):
|
||||
"""Check if event topic matches callback's topic."""
|
||||
"""Check if event topic matches callback's topic.
|
||||
|
||||
Args:
|
||||
topic (str): Topic name.
|
||||
|
||||
Returns:
|
||||
bool: Topic matches callback's topic.
|
||||
"""
|
||||
|
||||
return self._topic_regex.match(topic)
|
||||
|
||||
def process_event(self, event):
|
||||
|
|
@ -139,36 +315,69 @@ class EventCallback(object):
|
|||
event(Event): Event that was triggered.
|
||||
"""
|
||||
|
||||
# Skip if callback is not enabled or has invalid reference
|
||||
if not self._ref_valid or not self._enabled:
|
||||
# Skip if callback is not enabled
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
# Get reference
|
||||
callback = self._func_ref()
|
||||
# Check if reference is valid or callback's topic matches the event
|
||||
if not callback:
|
||||
# Change state if is invalid so the callback is removed
|
||||
self._ref_valid = False
|
||||
# Get reference and skip if is not available
|
||||
callback = self._get_callback()
|
||||
if callback is None:
|
||||
return
|
||||
|
||||
elif self.topic_matches(event.topic):
|
||||
# Try execute callback
|
||||
try:
|
||||
if self._expect_args:
|
||||
callback(event)
|
||||
if not self.topic_matches(event.topic):
|
||||
return
|
||||
|
||||
elif self._expect_kwargs:
|
||||
callback(event=event)
|
||||
# Try to execute callback
|
||||
try:
|
||||
if self._expect_args:
|
||||
callback(event)
|
||||
|
||||
else:
|
||||
callback()
|
||||
elif self._expect_kwargs:
|
||||
callback(event=event)
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute event callback {}".format(
|
||||
str(repr(self))
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
callback()
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute event callback {}".format(
|
||||
str(repr(self))
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def _validate_order(self, order):
|
||||
if isinstance(order, int):
|
||||
return
|
||||
|
||||
raise TypeError(
|
||||
"Expected type 'int' got '{}'.".format(str(type(order)))
|
||||
)
|
||||
|
||||
def _get_callback(self):
|
||||
if self._partial_func is not None:
|
||||
return self._partial_func
|
||||
|
||||
if self._func_ref is not None:
|
||||
return self._func_ref()
|
||||
return None
|
||||
|
||||
def _validate_ref(self):
|
||||
if self._ref_is_valid is False:
|
||||
return
|
||||
|
||||
if self._func_ref is not None:
|
||||
self._ref_is_valid = self._func_ref() is not None
|
||||
|
||||
elif self._partial_func is not None:
|
||||
self._ref_is_valid = self._partial_func.is_valid()
|
||||
|
||||
else:
|
||||
self._ref_is_valid = False
|
||||
|
||||
if not self._ref_is_valid:
|
||||
self._func_ref = None
|
||||
self._partial_func = None
|
||||
|
||||
|
||||
# Inherit from 'object' for Python 2 hosts
|
||||
|
|
@ -282,30 +491,39 @@ class Event(object):
|
|||
class EventSystem(object):
|
||||
"""Encapsulate event handling into an object.
|
||||
|
||||
System wraps registered callbacks and triggered events into single object
|
||||
so it is possible to create mutltiple independent systems that have their
|
||||
System wraps registered callbacks and triggered events into single object,
|
||||
so it is possible to create multiple independent systems that have their
|
||||
topics and callbacks.
|
||||
|
||||
|
||||
Callbacks are stored by order of their registration, but it is possible to
|
||||
manually define order of callbacks using 'order' argument within
|
||||
'add_callback'.
|
||||
"""
|
||||
|
||||
default_order = 100
|
||||
|
||||
def __init__(self):
|
||||
self._registered_callbacks = []
|
||||
|
||||
def add_callback(self, topic, callback):
|
||||
def add_callback(self, topic, callback, order=None):
|
||||
"""Register callback in event system.
|
||||
|
||||
Args:
|
||||
topic (str): Topic for EventCallback.
|
||||
callback (Callable): Function or method that will be called
|
||||
when topic is triggered.
|
||||
callback (Union[Callable, weakref_partial]): Function or method
|
||||
that will be called when topic is triggered.
|
||||
order (Optional[int]): Order of callback. Lower number means
|
||||
higher priority.
|
||||
|
||||
Returns:
|
||||
EventCallback: Created callback object which can be used to
|
||||
stop listening.
|
||||
"""
|
||||
|
||||
callback = EventCallback(topic, callback)
|
||||
if order is None:
|
||||
order = self.default_order
|
||||
|
||||
callback = EventCallback(topic, callback, order)
|
||||
self._registered_callbacks.append(callback)
|
||||
return callback
|
||||
|
||||
|
|
@ -341,22 +559,6 @@ class EventSystem(object):
|
|||
event.emit()
|
||||
return event
|
||||
|
||||
def _process_event(self, event):
|
||||
"""Process event topic and trigger callbacks.
|
||||
|
||||
Args:
|
||||
event (Event): Prepared event with topic and data.
|
||||
"""
|
||||
|
||||
invalid_callbacks = []
|
||||
for callback in self._registered_callbacks:
|
||||
callback.process_event(event)
|
||||
if not callback.is_ref_valid:
|
||||
invalid_callbacks.append(callback)
|
||||
|
||||
for callback in invalid_callbacks:
|
||||
self._registered_callbacks.remove(callback)
|
||||
|
||||
def emit_event(self, event):
|
||||
"""Emit event object.
|
||||
|
||||
|
|
@ -366,6 +568,21 @@ class EventSystem(object):
|
|||
|
||||
self._process_event(event)
|
||||
|
||||
def _process_event(self, event):
|
||||
"""Process event topic and trigger callbacks.
|
||||
|
||||
Args:
|
||||
event (Event): Prepared event with topic and data.
|
||||
"""
|
||||
|
||||
callbacks = tuple(sorted(
|
||||
self._registered_callbacks, key=lambda x: x.order
|
||||
))
|
||||
for callback in callbacks:
|
||||
callback.process_event(event)
|
||||
if not callback.is_ref_valid:
|
||||
self._registered_callbacks.remove(callback)
|
||||
|
||||
|
||||
class QueuedEventSystem(EventSystem):
|
||||
"""Events are automatically processed in queue.
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ def is_func_signature_supported(func, *args, **kwargs):
|
|||
True
|
||||
|
||||
Args:
|
||||
func (function): A function where the signature should be tested.
|
||||
func (Callable): A function where the signature should be tested.
|
||||
*args (Any): Positional arguments for function signature.
|
||||
**kwargs (Any): Keyword arguments for function signature.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
{
|
||||
"unit_scale_settings": {
|
||||
"enabled": true,
|
||||
"scene_unit_scale": "Meters"
|
||||
},
|
||||
"imageio": {
|
||||
"activate_host_color_management": true,
|
||||
"ocio_config": {
|
||||
|
|
|
|||
|
|
@ -1289,6 +1289,7 @@
|
|||
"twoSidedLighting": true,
|
||||
"lineAAEnable": true,
|
||||
"multiSample": 8,
|
||||
"loadTextures": false,
|
||||
"useDefaultMaterial": false,
|
||||
"wireframeOnShaded": false,
|
||||
"xray": false,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,34 @@
|
|||
"label": "Max",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "unit_scale_settings",
|
||||
"type": "dict",
|
||||
"label": "Set Unit Scale",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"key": "scene_unit_scale",
|
||||
"label": "Scene Unit Scale",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"defaults": "exr",
|
||||
"enum_items": [
|
||||
{"Millimeters": "mm"},
|
||||
{"Centimeters": "cm"},
|
||||
{"Meters": "m"},
|
||||
{"Kilometers": "km"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "imageio",
|
||||
"type": "dict",
|
||||
|
|
|
|||
|
|
@ -236,6 +236,11 @@
|
|||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "loadTextures",
|
||||
"label": "Load Textures"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "useDefaultMaterial",
|
||||
|
|
@ -908,6 +913,12 @@
|
|||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "loadTextures",
|
||||
"label": "Load Textures",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "useDefaultMaterial",
|
||||
|
|
|
|||
|
|
@ -1230,12 +1230,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
|
||||
version_ids = list()
|
||||
|
||||
version_docs_by_parent_id = {}
|
||||
version_docs_by_parent_id_and_name = collections.defaultdict(dict)
|
||||
for version_doc in version_docs:
|
||||
parent_id = version_doc["parent"]
|
||||
if parent_id not in version_docs_by_parent_id:
|
||||
version_ids.append(version_doc["_id"])
|
||||
version_docs_by_parent_id[parent_id] = version_doc
|
||||
version_ids.append(version_doc["_id"])
|
||||
name = version_doc["name"]
|
||||
version_docs_by_parent_id_and_name[parent_id][name] = version_doc
|
||||
|
||||
hero_version_docs_by_parent_id = {}
|
||||
for hero_version_doc in hero_version_docs:
|
||||
|
|
@ -1293,13 +1293,32 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
repre_doc = _repres.get(container_repre_name)
|
||||
|
||||
if not repre_doc:
|
||||
version_doc = version_docs_by_parent_id[subset_id]
|
||||
version_id = version_doc["_id"]
|
||||
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
|
||||
if selected_representation:
|
||||
repre_doc = repres_by_name[selected_representation]
|
||||
version_docs_by_name = version_docs_by_parent_id_and_name[
|
||||
subset_id
|
||||
]
|
||||
|
||||
# If asset or subset are selected for switching, we use latest
|
||||
# version else we try to keep the current container version.
|
||||
if (
|
||||
selected_asset not in (None, container_asset_name)
|
||||
or selected_subset not in (None, container_subset_name)
|
||||
):
|
||||
version_name = max(version_docs_by_name)
|
||||
else:
|
||||
repre_doc = repres_by_name[container_repre_name]
|
||||
version_name = container_version["name"]
|
||||
|
||||
version_doc = version_docs_by_name[version_name]
|
||||
version_id = version_doc["_id"]
|
||||
repres_docs_by_name = repre_docs_by_parent_id_by_name[
|
||||
version_id
|
||||
]
|
||||
|
||||
if selected_representation:
|
||||
repres_name = selected_representation
|
||||
else:
|
||||
repres_name = container_repre_name
|
||||
|
||||
repre_doc = repres_docs_by_name[repres_name]
|
||||
|
||||
error = None
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,25 @@ from .publishers import (
|
|||
)
|
||||
|
||||
|
||||
def unit_scale_enum():
|
||||
"""Return enumerator for scene unit scale."""
|
||||
return [
|
||||
{"label": "mm", "value": "Millimeters"},
|
||||
{"label": "cm", "value": "Centimeters"},
|
||||
{"label": "m", "value": "Meters"},
|
||||
{"label": "km", "value": "Kilometers"}
|
||||
]
|
||||
|
||||
|
||||
class UnitScaleSettings(BaseSettingsModel):
|
||||
enabled: bool = Field(True, title="Enabled")
|
||||
scene_unit_scale: str = Field(
|
||||
"Centimeters",
|
||||
title="Scene Unit Scale",
|
||||
enum_resolver=unit_scale_enum
|
||||
)
|
||||
|
||||
|
||||
class PRTAttributesModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = Field(title="Name")
|
||||
|
|
@ -24,6 +43,10 @@ class PointCloudSettings(BaseSettingsModel):
|
|||
|
||||
|
||||
class MaxSettings(BaseSettingsModel):
|
||||
unit_scale_settings: UnitScaleSettings = Field(
|
||||
default_factory=UnitScaleSettings,
|
||||
title="Set Unit Scale"
|
||||
)
|
||||
imageio: ImageIOSettings = Field(
|
||||
default_factory=ImageIOSettings,
|
||||
title="Color Management (ImageIO)"
|
||||
|
|
@ -46,6 +69,10 @@ class MaxSettings(BaseSettingsModel):
|
|||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"unit_scale_settings": {
|
||||
"enabled": True,
|
||||
"scene_unit_scale": "Centimeters"
|
||||
},
|
||||
"RenderSettings": DEFAULT_RENDER_SETTINGS,
|
||||
"CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS,
|
||||
"PointCloud": {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.3"
|
||||
__version__ = "0.1.4"
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class ViewportOptionsSetting(BaseSettingsModel):
|
|||
True, title="Enable Anti-Aliasing", section="Anti-Aliasing"
|
||||
)
|
||||
multiSample: int = Field(8, title="Anti Aliasing Samples")
|
||||
loadTextures: bool = Field(False, title="Load Textures")
|
||||
useDefaultMaterial: bool = Field(False, title="Use Default Material")
|
||||
wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded")
|
||||
xray: bool = Field(False, title="X-Ray")
|
||||
|
|
@ -302,6 +303,7 @@ DEFAULT_PLAYBLAST_SETTING = {
|
|||
"twoSidedLighting": True,
|
||||
"lineAAEnable": True,
|
||||
"multiSample": 8,
|
||||
"loadTextures": False,
|
||||
"useDefaultMaterial": False,
|
||||
"wireframeOnShaded": False,
|
||||
"xray": False,
|
||||
|
|
|
|||
|
|
@ -16,3 +16,8 @@ pynput = "^1.7.2" # Timers manager - TODO remove
|
|||
"Qt.py" = "^1.3.3"
|
||||
qtawesome = "0.7.3"
|
||||
speedcopy = "^2.1"
|
||||
|
||||
[ayon.runtimeDependencies]
|
||||
OpenTimelineIO = "0.14.1"
|
||||
opencolorio = "2.2.1"
|
||||
Pillow = "9.5.0"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from openpype.lib.events import EventSystem, QueuedEventSystem
|
||||
from functools import partial
|
||||
from openpype.lib.events import (
|
||||
EventSystem,
|
||||
QueuedEventSystem,
|
||||
weakref_partial,
|
||||
)
|
||||
|
||||
|
||||
def test_default_event_system():
|
||||
|
|
@ -81,3 +86,93 @@ def test_manual_event_system_queue():
|
|||
|
||||
assert output == expected_output, (
|
||||
"Callbacks were not called in correct order")
|
||||
|
||||
|
||||
def test_unordered_events():
|
||||
"""
|
||||
Validate if callbacks are triggered in order of their register.
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
def function_a():
|
||||
result.append("A")
|
||||
|
||||
def function_b():
|
||||
result.append("B")
|
||||
|
||||
def function_c():
|
||||
result.append("C")
|
||||
|
||||
# Without order
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_a)
|
||||
event_system.add_callback("test", function_b)
|
||||
event_system.add_callback("test", function_c)
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["A", "B", "C"]
|
||||
|
||||
|
||||
def test_ordered_events():
|
||||
"""
|
||||
Validate if callbacks are triggered by their order and order
|
||||
of their register.
|
||||
"""
|
||||
result = []
|
||||
|
||||
def function_a():
|
||||
result.append("A")
|
||||
|
||||
def function_b():
|
||||
result.append("B")
|
||||
|
||||
def function_c():
|
||||
result.append("C")
|
||||
|
||||
def function_d():
|
||||
result.append("D")
|
||||
|
||||
def function_e():
|
||||
result.append("E")
|
||||
|
||||
def function_f():
|
||||
result.append("F")
|
||||
|
||||
# Without order
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_a)
|
||||
event_system.add_callback("test", function_b, order=-10)
|
||||
event_system.add_callback("test", function_c, order=200)
|
||||
event_system.add_callback("test", function_d, order=150)
|
||||
event_system.add_callback("test", function_e)
|
||||
event_system.add_callback("test", function_f, order=200)
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["B", "A", "E", "D", "C", "F"]
|
||||
|
||||
|
||||
def test_events_partial_callbacks():
|
||||
"""
|
||||
Validate if partial callbacks are triggered.
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
def function(name):
|
||||
result.append(name)
|
||||
|
||||
def function_regular():
|
||||
result.append("regular")
|
||||
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_regular)
|
||||
event_system.add_callback("test", partial(function, "foo"))
|
||||
event_system.add_callback("test", weakref_partial(function, "bar"))
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
# Delete function should also make partial callbacks invalid
|
||||
del function
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["regular", "bar", "regular"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue