Merge branch 'develop' into enhancement/maya_validate_non_manifold_repair

This commit is contained in:
Roy Nieterau 2024-03-28 16:15:26 +01:00 committed by GitHub
commit 954c129002
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
164 changed files with 1118 additions and 965 deletions

View file

@ -14,3 +14,15 @@ AYON_SERVER_ENABLED = True
# Indicate if AYON entities should be used instead of OpenPype entities # Indicate if AYON entities should be used instead of OpenPype entities
USE_AYON_ENTITIES = True USE_AYON_ENTITIES = True
# ------------------------- # -------------------------
__all__ = (
"__version__",
# Deprecated
"AYON_CORE_ROOT",
"PACKAGE_DIR",
"PLUGINS_DIR",
"AYON_SERVER_ENABLED",
"USE_AYON_ENTITIES",
)

View file

@ -1075,7 +1075,7 @@ class AddonsManager:
"""Print out report of time spent on addons initialization parts. """Print out report of time spent on addons initialization parts.
Reporting is not automated must be implemented for each initialization Reporting is not automated must be implemented for each initialization
part separatelly. Reports must be stored to `_report` attribute. part separately. Reports must be stored to `_report` attribute.
Print is skipped if `_report` is empty. Print is skipped if `_report` is empty.
Attribute `_report` is dictionary where key is "label" describing Attribute `_report` is dictionary where key is "label" describing
@ -1267,7 +1267,7 @@ class TrayAddonsManager(AddonsManager):
def add_doubleclick_callback(self, addon, callback): def add_doubleclick_callback(self, addon, callback):
"""Register doubleclick callbacks on tray icon. """Register doubleclick callbacks on tray icon.
Currently there is no way how to determine which is launched. Name of Currently, there is no way how to determine which is launched. Name of
callback can be defined with `doubleclick_callback` attribute. callback can be defined with `doubleclick_callback` attribute.
Missing feature how to define default callback. Missing feature how to define default callback.

View file

@ -3,7 +3,6 @@
import os import os
import sys import sys
import json import json
import warnings
class Commands: class Commands:

View file

@ -31,6 +31,7 @@ __all__ = [
"get_stub", "get_stub",
# pipeline # pipeline
"AfterEffectsHost",
"ls", "ls",
"containerise", "containerise",

View file

@ -1,14 +1,11 @@
import os import os
import re
import tempfile import tempfile
import attr
import attr
import pyblish.api import pyblish.api
from ayon_core.settings import get_project_settings
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import RenderInstance from ayon_core.pipeline.publish import RenderInstance
from ayon_core.hosts.aftereffects.api import get_stub from ayon_core.hosts.aftereffects.api import get_stub

View file

@ -191,7 +191,7 @@ def _process_app_events() -> Optional[float]:
class LaunchQtApp(bpy.types.Operator): class LaunchQtApp(bpy.types.Operator):
"""A Base class for opertors to launch a Qt app.""" """A Base class for operators to launch a Qt app."""
_app: QtWidgets.QApplication _app: QtWidgets.QApplication
_window = Union[QtWidgets.QDialog, ModuleType] _window = Union[QtWidgets.QDialog, ModuleType]

View file

@ -227,7 +227,7 @@ class BlendLoader(plugin.AssetLoader):
obj.animation_data_create() obj.animation_data_create()
obj.animation_data.action = actions[obj.name] obj.animation_data.action = actions[obj.name]
# Restore the old data, but reset memebers, as they don't exist anymore # Restore the old data, but reset members, as they don't exist anymore
# This avoids a crash, because the memory addresses of those members # This avoids a crash, because the memory addresses of those members
# are not valid anymore # are not valid anymore
old_data["members"] = [] old_data["members"] = []

View file

@ -4,7 +4,6 @@ import bpy
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):

View file

@ -4,7 +4,6 @@ import bpy
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):

View file

@ -32,7 +32,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
tree = bpy.context.scene.node_tree tree = bpy.context.scene.node_tree
output_type = "CompositorNodeOutputFile" output_type = "CompositorNodeOutputFile"
output_node = None output_node = None
# Remove all output nodes that inlcude "AYON" in the name. # Remove all output nodes that include "AYON" in the name.
# There should be only one. # There should be only one.
for node in tree.nodes: for node in tree.nodes:
if node.bl_idname == output_type and "AYON" in node.name: if node.bl_idname == output_type and "AYON" in node.name:

View file

@ -118,7 +118,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
def workfile_path(self): def workfile_path(self):
workfile_path = self.data["last_workfile_path"] workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path # copy workfile from template if doesn't exist any on path
if not os.path.exists(workfile_path): if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via # TODO add ability to set different template workfile path via
# settings # settings

View file

@ -38,7 +38,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
render_path = r_template_item["path"].format_strict(anatomy_data) render_path = r_template_item["path"].format_strict(anatomy_data)
self.log.debug("__ render_path: `{}`".format(render_path)) self.log.debug("__ render_path: `{}`".format(render_path))
# create dir if it doesnt exists # create dir if it doesn't exists
try: try:
if not os.path.isdir(render_dir): if not os.path.isdir(render_dir):
os.makedirs(render_dir, exist_ok=True) os.makedirs(render_dir, exist_ok=True)

View file

@ -23,7 +23,7 @@ from .lib import (
reset_segment_selection, reset_segment_selection,
get_segment_attributes, get_segment_attributes,
get_clips_in_reels, get_clips_in_reels,
get_reformated_filename, get_reformatted_filename,
get_frame_from_filename, get_frame_from_filename,
get_padding_from_filename, get_padding_from_filename,
maintained_object_duplication, maintained_object_duplication,
@ -101,7 +101,7 @@ __all__ = [
"reset_segment_selection", "reset_segment_selection",
"get_segment_attributes", "get_segment_attributes",
"get_clips_in_reels", "get_clips_in_reels",
"get_reformated_filename", "get_reformatted_filename",
"get_frame_from_filename", "get_frame_from_filename",
"get_padding_from_filename", "get_padding_from_filename",
"maintained_object_duplication", "maintained_object_duplication",

View file

@ -607,7 +607,7 @@ def get_clips_in_reels(project):
return output_clips return output_clips
def get_reformated_filename(filename, padded=True): def get_reformatted_filename(filename, padded=True):
""" """
Return fixed python expression path Return fixed python expression path
@ -615,10 +615,10 @@ def get_reformated_filename(filename, padded=True):
filename (str): file name filename (str): file name
Returns: Returns:
type: string with reformated path type: string with reformatted path
Example: Example:
get_reformated_filename("plate.1001.exr") > plate.%04d.exr get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
""" """
found = FRAME_PATTERN.search(filename) found = FRAME_PATTERN.search(filename)
@ -980,7 +980,7 @@ class MediaInfoFile(object):
@property @property
def file_pattern(self): def file_pattern(self):
"""Clips file patter """Clips file pattern.
Returns: Returns:
str: file pattern. ex. file.[1-2].exr str: file pattern. ex. file.[1-2].exr

View file

@ -644,13 +644,13 @@ class PublishableClip:
"families": [self.base_product_type, self.product_type] "families": [self.base_product_type, self.product_type]
} }
def _convert_to_entity(self, type, template): def _convert_to_entity(self, src_type, template):
""" Converting input key to key with type. """ """ Converting input key to key with type. """
# convert to entity type # convert to entity type
entity_type = self.types.get(type, None) folder_type = self.types.get(src_type, None)
assert entity_type, "Missing entity type for `{}`".format( assert folder_type, "Missing folder type for `{}`".format(
type src_type
) )
# first collect formatting data to use for formatting template # first collect formatting data to use for formatting template
@ -661,7 +661,7 @@ class PublishableClip:
formatting_data[_k] = value formatting_data[_k] = value
return { return {
"entity_type": entity_type, "folder_type": folder_type,
"entity_name": template.format( "entity_name": template.format(
**formatting_data **formatting_data
) )
@ -1018,7 +1018,7 @@ class OpenClipSolver(flib.MediaInfoFile):
self.feed_version_name)) self.feed_version_name))
else: else:
self.log.debug("adding new track element ..") self.log.debug("adding new track element ..")
# create new track as it doesnt exists yet # create new track as it doesn't exist yet
# set current version to feeds on tmp # set current version to feeds on tmp
tmp_xml_feeds = tmp_xml_track.find('feeds') tmp_xml_feeds = tmp_xml_track.find('feeds')
tmp_xml_feeds.set('currentVersion', self.feed_version_name) tmp_xml_feeds.set('currentVersion', self.feed_version_name)

View file

@ -256,7 +256,7 @@ def create_otio_reference(clip_data, fps=None):
if not otio_ex_ref_item: if not otio_ex_ref_item:
dirname, file_name = os.path.split(path) dirname, file_name = os.path.split(path)
file_name = utils.get_reformated_filename(file_name, padded=False) file_name = utils.get_reformatted_filename(file_name, padded=False)
reformated_path = os.path.join(dirname, file_name) reformated_path = os.path.join(dirname, file_name)
# in case old OTIO or video file create `ExternalReference` # in case old OTIO or video file create `ExternalReference`
otio_ex_ref_item = otio.schema.ExternalReference( otio_ex_ref_item = otio.schema.ExternalReference(

View file

@ -21,7 +21,7 @@ def frames_to_seconds(frames, framerate):
return otio.opentime.to_seconds(rt) return otio.opentime.to_seconds(rt)
def get_reformated_filename(filename, padded=True): def get_reformatted_filename(filename, padded=True):
""" """
Return fixed python expression path Return fixed python expression path
@ -29,10 +29,10 @@ def get_reformated_filename(filename, padded=True):
filename (str): file name filename (str): file name
Returns: Returns:
type: string with reformated path type: string with reformatted path
Example: Example:
get_reformated_filename("plate.1001.exr") > plate.%04d.exr get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
""" """
found = FRAME_PATTERN.search(filename) found = FRAME_PATTERN.search(filename)

View file

@ -17,7 +17,7 @@ class CreateShotClip(opfapi.Creator):
presets = deepcopy(self.presets) presets = deepcopy(self.presets)
gui_inputs = self.get_gui_inputs() gui_inputs = self.get_gui_inputs()
# get key pares from presets and match it on ui inputs # get key pairs from presets and match it on ui inputs
for k, v in gui_inputs.items(): for k, v in gui_inputs.items():
if v["type"] in ("dict", "section"): if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed # nested dictionary (only one level allowed
@ -236,7 +236,7 @@ class CreateShotClip(opfapi.Creator):
"type": "QCheckBox", "type": "QCheckBox",
"label": "Source resolution", "label": "Source resolution",
"target": "tag", "target": "tag",
"toolTip": "Is resloution taken from timeline or source?", # noqa "toolTip": "Is resolution taken from timeline or source?", # noqa
"order": 4}, "order": 4},
} }
}, },

View file

@ -37,7 +37,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
self.otio_timeline = context.data["otioTimeline"] self.otio_timeline = context.data["otioTimeline"]
self.fps = context.data["fps"] self.fps = context.data["fps"]
# process all sellected # process all selected
for segment in selected_segments: for segment in selected_segments:
# get openpype tag data # get openpype tag data
marker_data = opfapi.get_segment_data_marker(segment) marker_data = opfapi.get_segment_data_marker(segment)
@ -100,6 +100,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
marker_data["handleEnd"] = min( marker_data["handleEnd"] = min(
marker_data["handleEnd"], tail) marker_data["handleEnd"], tail)
# Backward compatibility fix of 'entity_type' > 'folder_type'
if "parents" in marker_data:
for parent in marker_data["parents"]:
if "entity_type" in parent:
parent["folder_type"] = parent.pop("entity_type")
workfile_start = self._set_workfile_start(marker_data) workfile_start = self._set_workfile_start(marker_data)
with_audio = bool(marker_data.pop("audio")) with_audio = bool(marker_data.pop("audio"))

View file

@ -396,7 +396,7 @@ class FtrackEntityOperator:
entity = session.query(query).first() entity = session.query(query).first()
# if entity doesnt exist then create one # if entity doesn't exist then create one
if not entity: if not entity:
entity = self.create_ftrack_entity( entity = self.create_ftrack_entity(
session, session,

View file

@ -28,7 +28,6 @@ from ayon_core.tools.utils import host_tools
from .lib import ( from .lib import (
get_current_comp, get_current_comp,
comp_lock_and_undo_chunk,
validate_comp_prefs validate_comp_prefs
) )

View file

@ -1,6 +1,11 @@
from ayon_core.lib import EnumDef from ayon_core.lib import (
UILabelDef,
NumberDef,
EnumDef
)
from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver
from ayon_core.hosts.fusion.api.lib import get_current_comp
class CreateSaver(GenericCreateSaver): class CreateSaver(GenericCreateSaver):
@ -45,6 +50,7 @@ class CreateSaver(GenericCreateSaver):
self._get_reviewable_bool(), self._get_reviewable_bool(),
self._get_frame_range_enum(), self._get_frame_range_enum(),
self._get_image_format_enum(), self._get_image_format_enum(),
*self._get_custom_frame_range_attribute_defs()
] ]
return attr_defs return attr_defs
@ -53,6 +59,7 @@ class CreateSaver(GenericCreateSaver):
"current_folder": "Current Folder context", "current_folder": "Current Folder context",
"render_range": "From render in/out", "render_range": "From render in/out",
"comp_range": "From composition timeline", "comp_range": "From composition timeline",
"custom_range": "Custom frame range",
} }
return EnumDef( return EnumDef(
@ -61,3 +68,82 @@ class CreateSaver(GenericCreateSaver):
label="Frame range source", label="Frame range source",
default=self.default_frame_range_option default=self.default_frame_range_option
) )
@staticmethod
def _get_custom_frame_range_attribute_defs() -> list:
# Define custom frame range defaults based on current comp
# timeline settings (if a comp is currently open)
comp = get_current_comp()
if comp is not None:
attrs = comp.GetAttrs()
frame_defaults = {
"frameStart": int(attrs["COMPN_GlobalStart"]),
"frameEnd": int(attrs["COMPN_GlobalEnd"]),
"handleStart": int(
attrs["COMPN_RenderStart"] - attrs["COMPN_GlobalStart"]
),
"handleEnd": int(
attrs["COMPN_GlobalEnd"] - attrs["COMPN_RenderEnd"]
),
}
else:
frame_defaults = {
"frameStart": 1001,
"frameEnd": 1100,
"handleStart": 0,
"handleEnd": 0
}
return [
UILabelDef(
label="<br><b>Custom Frame Range</b><br>"
"<i>only used with 'Custom frame range' source</i>"
),
NumberDef(
"custom_frameStart",
label="Frame Start",
default=frame_defaults["frameStart"],
minimum=0,
decimals=0,
tooltip=(
"Set the start frame for the export.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_frameEnd",
label="Frame End",
default=frame_defaults["frameEnd"],
minimum=0,
decimals=0,
tooltip=(
"Set the end frame for the export.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_handleStart",
label="Handle Start",
default=frame_defaults["handleStart"],
minimum=0,
decimals=0,
tooltip=(
"Set the start handles for the export, this will be "
"added before the start frame.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_handleEnd",
label="Handle End",
default=frame_defaults["handleEnd"],
minimum=0,
decimals=0,
tooltip=(
"Set the end handles for the export, this will be added "
"after the end frame.\n"
"Only used if frame range source is 'Custom frame range'."
)
)
]

View file

@ -57,6 +57,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
start_with_handle = comp_start start_with_handle = comp_start
end_with_handle = comp_end end_with_handle = comp_end
if frame_range_source == "custom_range":
start = int(instance.data["custom_frameStart"])
end = int(instance.data["custom_frameEnd"])
handle_start = int(instance.data["custom_handleStart"])
handle_end = int(instance.data["custom_handleEnd"])
start_with_handle = start - handle_start
end_with_handle = end + handle_end
frame = instance.data["creator_attributes"].get("frame") frame = instance.data["creator_attributes"].get("frame")
# explicitly publishing only single frame # explicitly publishing only single frame
if frame is not None: if frame is not None:

View file

@ -568,7 +568,7 @@ def save_scene():
"""Save the Harmony scene safely. """Save the Harmony scene safely.
The built-in (to Avalon) background zip and moving of the Harmony scene The built-in (to Avalon) background zip and moving of the Harmony scene
folder, interfers with server/client communication by sending two requests folder, interferes with server/client communication by sending two requests
at the same time. This only happens when sending "scene.saveAll()". This at the same time. This only happens when sending "scene.saveAll()". This
method prevents this double request and safely saves the scene. method prevents this double request and safely saves the scene.

View file

@ -21,12 +21,12 @@ class CreateFarmRender(plugin.Creator):
path = "render/{0}/{0}.".format(node.split("/")[-1]) path = "render/{0}/{0}.".format(node.split("/")[-1])
harmony.send( harmony.send(
{ {
"function": f"PypeHarmony.Creators.CreateRender.create", "function": "PypeHarmony.Creators.CreateRender.create",
"args": [node, path] "args": [node, path]
}) })
harmony.send( harmony.send(
{ {
"function": f"PypeHarmony.color", "function": "PypeHarmony.color",
"args": [[0.9, 0.75, 0.3, 1.0]] "args": [[0.9, 0.75, 0.3, 1.0]]
} }
) )

View file

@ -1,8 +1,8 @@
import os import os
import pyblish.api
import pyblish.api import pyblish.api
class CollectAudio(pyblish.api.InstancePlugin): class CollectAudio(pyblish.api.InstancePlugin):
""" """
Collect relative path for audio file to instance. Collect relative path for audio file to instance.

View file

@ -17,7 +17,7 @@ class CollectScene(pyblish.api.ContextPlugin):
"""Plugin entry point.""" """Plugin entry point."""
result = harmony.send( result = harmony.send(
{ {
f"function": "PypeHarmony.getSceneSettings", "function": "PypeHarmony.getSceneSettings",
"args": []} "args": []}
)["result"] )["result"]
@ -62,7 +62,7 @@ class CollectScene(pyblish.api.ContextPlugin):
result = harmony.send( result = harmony.send(
{ {
f"function": "PypeHarmony.getVersion", "function": "PypeHarmony.getVersion",
"args": []} "args": []}
)["result"] )["result"]
context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1]) context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1])

View file

@ -1,10 +1,12 @@
import os import os
import hiero.core.events import hiero.core.events
from ayon_core.lib import Logger, register_event_callback from ayon_core.lib import Logger, register_event_callback
from .lib import ( from .lib import (
sync_avalon_data_to_workfile, sync_avalon_data_to_workfile,
launch_workfiles_app, launch_workfiles_app,
selection_changed_timeline,
before_project_save, before_project_save,
) )
from .tags import add_tags_to_workfile from .tags import add_tags_to_workfile

View file

@ -166,7 +166,7 @@ def get_current_track(sequence, name, audio=False):
Creates new if none is found. Creates new if none is found.
Args: Args:
sequence (hiero.core.Sequence): hiero sequene object sequence (hiero.core.Sequence): hiero sequence object
name (str): name of track we want to return name (str): name of track we want to return
audio (bool)[optional]: switch to AudioTrack audio (bool)[optional]: switch to AudioTrack
@ -846,8 +846,8 @@ def create_nuke_workfile_clips(nuke_workfiles, seq=None):
[{ [{
'path': 'P:/Jakub_testy_pipeline/test_v01.nk', 'path': 'P:/Jakub_testy_pipeline/test_v01.nk',
'name': 'test', 'name': 'test',
'handleStart': 15, # added asymetrically to handles 'handleStart': 15, # added asymmetrically to handles
'handleEnd': 10, # added asymetrically to handles 'handleEnd': 10, # added asymmetrically to handles
"clipIn": 16, "clipIn": 16,
"frameStart": 991, "frameStart": 991,
"frameEnd": 1023, "frameEnd": 1023,
@ -1192,7 +1192,7 @@ def get_sequence_pattern_and_padding(file):
Return: Return:
string: any matching sequence pattern string: any matching sequence pattern
int: padding of sequnce numbering int: padding of sequence numbering
""" """
foundall = re.findall( foundall = re.findall(
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)

View file

@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
if isinstance(track, hiero.core.AudioTrack): if isinstance(track, hiero.core.AudioTrack):
kind = 'Audio' kind = 'Audio'
# Gather TrackItems involved in trasition # Gather TrackItems involved in transition
item_in, item_out = get_neighboring_trackitems( item_in, item_out = get_neighboring_trackitems(
otio_item, otio_item,
otio_track, otio_track,
@ -101,7 +101,7 @@ def apply_transition(otio_track, otio_item, track):
if transition_type == 'dissolve': if transition_type == 'dissolve':
transition_func = getattr( transition_func = getattr(
hiero.core.Transition, hiero.core.Transition,
'create{kind}DissolveTransition'.format(kind=kind) "create{kind}DissolveTransition".format(kind=kind)
) )
try: try:
@ -109,7 +109,7 @@ def apply_transition(otio_track, otio_item, track):
item_in, item_in,
item_out, item_out,
otio_item.in_offset.value, otio_item.in_offset.value,
otio_item.out_offset.value otio_item.out_offset.value,
) )
# Catch error raised if transition is bigger than TrackItem source # Catch error raised if transition is bigger than TrackItem source
@ -134,7 +134,7 @@ def apply_transition(otio_track, otio_item, track):
transition = transition_func( transition = transition_func(
item_out, item_out,
otio_item.out_offset.value otio_item.out_offset.value,
) )
elif transition_type == 'fade_out': elif transition_type == 'fade_out':
@ -183,9 +183,7 @@ def prep_url(url_in):
def create_offline_mediasource(otio_clip, path=None): def create_offline_mediasource(otio_clip, path=None):
global _otio_old global _otio_old
hiero_rate = hiero.core.TimeBase( hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
otio_clip.source_range.start_time.rate
)
try: try:
legal_media_refs = ( legal_media_refs = (
@ -212,7 +210,7 @@ def create_offline_mediasource(otio_clip, path=None):
source_range.start_time.value, source_range.start_time.value,
source_range.duration.value, source_range.duration.value,
hiero_rate, hiero_rate,
source_range.start_time.value source_range.start_time.value,
) )
return media return media
@ -385,7 +383,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
# Only reverse effect can be applied here # Only reverse effect can be applied here
if abs(time_scalar) == 1.: if abs(time_scalar) == 1.:
trackitem.setPlaybackSpeed( trackitem.setPlaybackSpeed(
trackitem.playbackSpeed() * time_scalar) trackitem.playbackSpeed() * time_scalar
)
elif isinstance(effect, otio.schema.FreezeFrame): elif isinstance(effect, otio.schema.FreezeFrame):
# For freeze frame, playback speed must be set after range # For freeze frame, playback speed must be set after range
@ -397,28 +396,21 @@ def create_trackitem(playhead, track, otio_clip, clip):
source_in = source_range.end_time_inclusive().value source_in = source_range.end_time_inclusive().value
timeline_in = playhead + source_out timeline_in = playhead + source_out
timeline_out = ( timeline_out = (timeline_in + source_range.duration.value) - 1
timeline_in +
source_range.duration.value
) - 1
else: else:
# Normal playback speed # Normal playback speed
source_in = source_range.start_time.value source_in = source_range.start_time.value
source_out = source_range.end_time_inclusive().value source_out = source_range.end_time_inclusive().value
timeline_in = playhead timeline_in = playhead
timeline_out = ( timeline_out = (timeline_in + source_range.duration.value) - 1
timeline_in +
source_range.duration.value
) - 1
# Set source and timeline in/out points # Set source and timeline in/out points
trackitem.setTimes( trackitem.setTimes(
timeline_in, timeline_in,
timeline_out, timeline_out,
source_in, source_in,
source_out source_out,
) )
# Apply playback speed for freeze frames # Apply playback speed for freeze frames
@ -435,7 +427,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
def build_sequence( def build_sequence(
otio_timeline, project=None, sequence=None, track_kind=None): otio_timeline, project=None, sequence=None, track_kind=None
):
if project is None: if project is None:
if sequence: if sequence:
project = sequence.project() project = sequence.project()
@ -509,10 +502,7 @@ def build_sequence(
# Create TrackItem # Create TrackItem
trackitem = create_trackitem( trackitem = create_trackitem(
playhead, playhead, track, otio_clip, clip
track,
otio_clip,
clip
) )
# Add markers # Add markers

View file

@ -25,7 +25,7 @@ def get_reformated_path(path, padded=True):
path (str): path url or simple file name path (str): path url or simple file name
Returns: Returns:
type: string with reformated path type: string with reformatted path
Example: Example:
get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr

View file

@ -449,7 +449,6 @@ class ClipLoader:
repr = self.context["representation"] repr = self.context["representation"]
repr_cntx = repr["context"] repr_cntx = repr["context"]
folder_path = self.context["folder"]["path"] folder_path = self.context["folder"]["path"]
folder_name = self.context["folder"]["name"]
product_name = self.context["product"]["name"] product_name = self.context["product"]["name"]
representation = repr["name"] representation = repr["name"]
self.data["clip_name"] = self.clip_name_template.format(**repr_cntx) self.data["clip_name"] = self.clip_name_template.format(**repr_cntx)
@ -906,16 +905,16 @@ class PublishClip:
"hierarchyData": hierarchy_formatting_data, "hierarchyData": hierarchy_formatting_data,
"productName": self.product_name, "productName": self.product_name,
"productType": self.product_type, "productType": self.product_type,
"families": [self.product_type, self.data["family"]] "families": [self.product_type, self.data["productType"]]
} }
def _convert_to_entity(self, type, template): def _convert_to_entity(self, src_type, template):
""" Converting input key to key with type. """ """ Converting input key to key with type. """
# convert to entity type # convert to entity type
entity_type = self.types.get(type, None) folder_type = self.types.get(src_type, None)
assert entity_type, "Missing entity type for `{}`".format( assert folder_type, "Missing folder type for `{}`".format(
type src_type
) )
# first collect formatting data to use for formatting template # first collect formatting data to use for formatting template
@ -926,7 +925,7 @@ class PublishClip:
formatting_data[_k] = value formatting_data[_k] = value
return { return {
"entity_type": entity_type, "folder_type": folder_type,
"entity_name": template.format( "entity_name": template.format(
**formatting_data **formatting_data
) )

View file

@ -3,9 +3,11 @@
# Note: This only prints the text data that is visible in the active Spreadsheet View. # Note: This only prints the text data that is visible in the active Spreadsheet View.
# If you've filtered text, only the visible text will be printed to the CSV file # If you've filtered text, only the visible text will be printed to the CSV file
# Usage: Copy to ~/.hiero/Python/StartupUI # Usage: Copy to ~/.hiero/Python/StartupUI
import os
import csv
import hiero.core.events import hiero.core.events
import hiero.ui import hiero.ui
import os, csv
try: try:
from PySide.QtGui import * from PySide.QtGui import *
from PySide.QtCore import * from PySide.QtCore import *

View file

@ -641,7 +641,7 @@ def _setStatus(self, status):
global gStatusTags global gStatusTags
# Get a valid Tag object from the Global list of statuses # Get a valid Tag object from the Global list of statuses
if not status in gStatusTags.keys(): if status not in gStatusTags.keys():
print("Status requested was not a valid Status string.") print("Status requested was not a valid Status string.")
return return

View file

@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
kind = "Audio" kind = "Audio"
try: try:
# Gather TrackItems involved in trasition # Gather TrackItems involved in transition
item_in, item_out = get_neighboring_trackitems( item_in, item_out = get_neighboring_trackitems(
otio_item, otio_item,
otio_track, otio_track,
@ -101,14 +101,14 @@ def apply_transition(otio_track, otio_item, track):
if transition_type == "dissolve": if transition_type == "dissolve":
transition_func = getattr( transition_func = getattr(
hiero.core.Transition, hiero.core.Transition,
'create{kind}DissolveTransition'.format(kind=kind) "create{kind}DissolveTransition".format(kind=kind)
) )
transition = transition_func( transition = transition_func(
item_in, item_in,
item_out, item_out,
otio_item.in_offset.value, otio_item.in_offset.value,
otio_item.out_offset.value otio_item.out_offset.value,
) )
elif transition_type == "fade_in": elif transition_type == "fade_in":
@ -116,20 +116,14 @@ def apply_transition(otio_track, otio_item, track):
hiero.core.Transition, hiero.core.Transition,
'create{kind}FadeInTransition'.format(kind=kind) 'create{kind}FadeInTransition'.format(kind=kind)
) )
transition = transition_func( transition = transition_func(item_out, otio_item.out_offset.value)
item_out,
otio_item.out_offset.value
)
elif transition_type == "fade_out": elif transition_type == "fade_out":
transition_func = getattr( transition_func = getattr(
hiero.core.Transition, hiero.core.Transition,
'create{kind}FadeOutTransition'.format(kind=kind) "create{kind}FadeOutTransition".format(kind=kind)
)
transition = transition_func(
item_in,
otio_item.in_offset.value
) )
transition = transition_func(item_in, otio_item.in_offset.value)
else: else:
# Unknown transition # Unknown transition
@ -138,11 +132,10 @@ def apply_transition(otio_track, otio_item, track):
# Apply transition to track # Apply transition to track
track.addTransition(transition) track.addTransition(transition)
except Exception, e: except Exception as e:
sys.stderr.write( sys.stderr.write(
'Unable to apply transition "{t}": "{e}"\n'.format( 'Unable to apply transition "{t}": "{e}"\n'.format(
t=otio_item, t=otio_item, e=e
e=e
) )
) )
@ -153,18 +146,14 @@ def prep_url(url_in):
if url.startswith("file://localhost/"): if url.startswith("file://localhost/"):
return url.replace("file://localhost/", "") return url.replace("file://localhost/", "")
url = '{url}'.format( if url.startswith(os.sep):
sep=url.startswith(os.sep) and "" or os.sep, url = url[1:]
url=url.startswith(os.sep) and url[1:] or url
)
return url return url
def create_offline_mediasource(otio_clip, path=None): def create_offline_mediasource(otio_clip, path=None):
hiero_rate = hiero.core.TimeBase( hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
otio_clip.source_range.start_time.rate
)
if isinstance(otio_clip.media_reference, otio.schema.ExternalReference): if isinstance(otio_clip.media_reference, otio.schema.ExternalReference):
source_range = otio_clip.available_range() source_range = otio_clip.available_range()
@ -180,7 +169,7 @@ def create_offline_mediasource(otio_clip, path=None):
source_range.start_time.value, source_range.start_time.value,
source_range.duration.value, source_range.duration.value,
hiero_rate, hiero_rate,
source_range.start_time.value source_range.start_time.value,
) )
return media return media
@ -203,7 +192,7 @@ marker_color_map = {
"MAGENTA": "Magenta", "MAGENTA": "Magenta",
"BLACK": "Blue", "BLACK": "Blue",
"WHITE": "Green", "WHITE": "Green",
"MINT": "Cyan" "MINT": "Cyan",
} }
@ -254,12 +243,6 @@ def add_markers(otio_item, hiero_item, tagsbin):
if _tag is None: if _tag is None:
_tag = hiero.core.Tag(marker_color_map[marker.color]) _tag = hiero.core.Tag(marker_color_map[marker.color])
start = marker.marked_range.start_time.value
end = (
marker.marked_range.start_time.value +
marker.marked_range.duration.value
)
tag = hiero_item.addTag(_tag) tag = hiero_item.addTag(_tag)
tag.setName(marker.name or marker_color_map[marker_color]) tag.setName(marker.name or marker_color_map[marker_color])
@ -275,12 +258,12 @@ def create_track(otio_track, tracknum, track_kind):
# Create a Track # Create a Track
if otio_track.kind == otio.schema.TrackKind.Video: if otio_track.kind == otio.schema.TrackKind.Video:
track = hiero.core.VideoTrack( track = hiero.core.VideoTrack(
otio_track.name or 'Video{n}'.format(n=tracknum) otio_track.name or "Video{n}".format(n=tracknum)
) )
else: else:
track = hiero.core.AudioTrack( track = hiero.core.AudioTrack(
otio_track.name or 'Audio{n}'.format(n=tracknum) otio_track.name or "Audio{n}".format(n=tracknum)
) )
return track return track
@ -315,34 +298,25 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
for effect in otio_clip.effects: for effect in otio_clip.effects:
if isinstance(effect, otio.schema.LinearTimeWarp): if isinstance(effect, otio.schema.LinearTimeWarp):
trackitem.setPlaybackSpeed( trackitem.setPlaybackSpeed(
trackitem.playbackSpeed() * trackitem.playbackSpeed() * effect.time_scalar
effect.time_scalar
) )
# If reverse playback speed swap source in and out # If reverse playback speed swap source in and out
if trackitem.playbackSpeed() < 0: if trackitem.playbackSpeed() < 0:
source_out = source_range.start_time.value source_out = source_range.start_time.value
source_in = ( source_in = (
source_range.start_time.value + source_range.start_time.value + source_range.duration.value
source_range.duration.value
) - 1 ) - 1
timeline_in = playhead + source_out timeline_in = playhead + source_out
timeline_out = ( timeline_out = (timeline_in + source_range.duration.value) - 1
timeline_in +
source_range.duration.value
) - 1
else: else:
# Normal playback speed # Normal playback speed
source_in = source_range.start_time.value source_in = source_range.start_time.value
source_out = ( source_out = (
source_range.start_time.value + source_range.start_time.value + source_range.duration.value
source_range.duration.value
) - 1 ) - 1
timeline_in = playhead timeline_in = playhead
timeline_out = ( timeline_out = (timeline_in + source_range.duration.value) - 1
timeline_in +
source_range.duration.value
) - 1
# Set source and timeline in/out points # Set source and timeline in/out points
trackitem.setSourceIn(source_in) trackitem.setSourceIn(source_in)
@ -357,7 +331,8 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
def build_sequence( def build_sequence(
otio_timeline, project=None, sequence=None, track_kind=None): otio_timeline, project=None, sequence=None, track_kind=None
):
if project is None: if project is None:
if sequence: if sequence:
@ -414,8 +389,7 @@ def build_sequence(
if isinstance(otio_clip, otio.schema.Stack): if isinstance(otio_clip, otio.schema.Stack):
bar = hiero.ui.mainWindow().statusBar() bar = hiero.ui.mainWindow().statusBar()
bar.showMessage( bar.showMessage(
"Nested sequences are created separately.", "Nested sequences are created separately.", timeout=3000
timeout=3000
) )
build_sequence(otio_clip, project, otio_track.kind) build_sequence(otio_clip, project, otio_track.kind)
@ -428,11 +402,7 @@ def build_sequence(
# Create TrackItem # Create TrackItem
trackitem = create_trackitem( trackitem = create_trackitem(
playhead, playhead, track, otio_clip, clip, tagsbin
track,
otio_clip,
clip,
tagsbin
) )
# Add trackitem to track # Add trackitem to track

View file

@ -89,7 +89,7 @@ def update_tag(tag, data):
# set all data metadata to tag metadata # set all data metadata to tag metadata
for _k, _v in data_mtd.items(): for _k, _v in data_mtd.items():
value = str(_v) value = str(_v)
if type(_v) == dict: if isinstance(_v, dict):
value = json.dumps(_v) value = json.dumps(_v)
# set the value # set the value

View file

@ -166,7 +166,7 @@ class CreateShotClip(phiero.Creator):
"type": "QCheckBox", "type": "QCheckBox",
"label": "Source resolution", "label": "Source resolution",
"target": "tag", "target": "tag",
"toolTip": "Is resloution taken from timeline or source?", # noqa "toolTip": "Is resolution taken from timeline or source?", # noqa
"order": 4}, "order": 4},
} }
}, },
@ -211,7 +211,7 @@ class CreateShotClip(phiero.Creator):
presets = deepcopy(self.presets) presets = deepcopy(self.presets)
gui_inputs = deepcopy(self.gui_inputs) gui_inputs = deepcopy(self.gui_inputs)
# get key pares from presets and match it on ui inputs # get key pairs from presets and match it on ui inputs
for k, v in gui_inputs.items(): for k, v in gui_inputs.items():
if v["type"] in ("dict", "section"): if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed # nested dictionary (only one level allowed

View file

@ -1,5 +1,5 @@
from itertools import product
import re import re
import pyblish.api import pyblish.api

View file

@ -43,7 +43,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
tracks_effect_items = self.collect_sub_track_items(all_tracks) tracks_effect_items = self.collect_sub_track_items(all_tracks)
context.data["tracksEffectItems"] = tracks_effect_items context.data["tracksEffectItems"] = tracks_effect_items
# process all sellected timeline track items # process all selected timeline track items
for track_item in selected_timeline_items: for track_item in selected_timeline_items:
data = {} data = {}
clip_name = track_item.name() clip_name = track_item.name()
@ -62,7 +62,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
}: }:
continue continue
# get clips subtracks and anotations # get clips subtracks and annotations
annotations = self.clip_annotations(source_clip) annotations = self.clip_annotations(source_clip)
subtracks = self.clip_subtrack(track_item) subtracks = self.clip_subtrack(track_item)
self.log.debug("Annotations: {}".format(annotations)) self.log.debug("Annotations: {}".format(annotations))
@ -84,6 +84,11 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
k: v for k, v in tag_data.items() k: v for k, v in tag_data.items()
if k not in ("id", "applieswhole", "label") if k not in ("id", "applieswhole", "label")
}) })
# Backward compatibility fix of 'entity_type' > 'folder_type'
if "parents" in data:
for parent in data["parents"]:
if "entity_type" in parent:
parent["folder_type"] = parent.pop("entity_type")
asset, asset_name = self._get_folder_data(tag_data) asset, asset_name = self._get_folder_data(tag_data)
@ -378,12 +383,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# collect all subtrack items # collect all subtrack items
sub_track_items = {} sub_track_items = {}
for track in tracks: for track in tracks:
items = track.items() effect_items = track.subTrackItems()
effet_items = track.subTrackItems()
# skip if no clips on track > need track with effect only # skip if no clips on track > need track with effect only
if not effet_items: if not effect_items:
continue continue
# skip all disabled tracks # skip all disabled tracks
@ -391,7 +394,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
continue continue
track_index = track.trackIndex() track_index = track.trackIndex()
_sub_track_items = phiero.flatten(effet_items) _sub_track_items = phiero.flatten(effect_items)
_sub_track_items = list(_sub_track_items) _sub_track_items = list(_sub_track_items)
# continue only if any subtrack items are collected # continue only if any subtrack items are collected
@ -439,10 +442,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
for item in subTrackItems: for item in subTrackItems:
if "TimeWarp" in item.name(): if "TimeWarp" in item.name():
continue continue
# avoid all anotation # avoid all annotation
if isinstance(item, hiero.core.Annotation): if isinstance(item, hiero.core.Annotation):
continue continue
# # avoid all not anaibled # avoid all disabled
if not item.isEnabled(): if not item.isEnabled():
continue continue
subtracks.append(item) subtracks.append(item)

View file

@ -35,10 +35,6 @@ class PrecollectRetime(api.InstancePlugin):
source_out = int(track_item.sourceOut()) source_out = int(track_item.sourceOut())
speed = track_item.playbackSpeed() speed = track_item.playbackSpeed()
# calculate available material before retime
available_in = int(track_item.handleInLength() * speed)
available_out = int(track_item.handleOutLength() * speed)
self.log.debug(( self.log.debug((
"_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n " "_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n "
"source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n " "source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n "

View file

@ -3,7 +3,6 @@ import sys
import os import os
import errno import errno
import re import re
import uuid
import logging import logging
import json import json
from contextlib import contextmanager from contextlib import contextmanager
@ -44,84 +43,6 @@ def get_folder_fps(folder_entity=None):
return folder_entity["attrib"]["fps"] return folder_entity["attrib"]["fps"]
def set_id(node, unique_id, overwrite=False):
exists = node.parm("id")
if not exists:
imprint(node, {"id": unique_id})
if not exists and overwrite:
node.setParm("id", unique_id)
def get_id(node):
"""Get the `cbId` attribute of the given node.
Args:
node (hou.Node): the name of the node to retrieve the attribute from
Returns:
str: cbId attribute of the node.
"""
if node is not None:
return node.parm("id")
def generate_ids(nodes, folder_id=None):
"""Returns new unique ids for the given nodes.
Note: This does not assign the new ids, it only generates the values.
To assign new ids using this method:
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id)
To also override any existing values (and assign regenerated ids):
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id, overwrite=True)
Args:
nodes (list): List of nodes.
folder_id (str): Folder id . Use current folder id if is ``None``.
Returns:
list: A list of (node, id) tuples.
"""
if folder_id is None:
project_name = get_current_project_name()
folder_path = get_current_folder_path()
# Get folder id of current context folder
folder_entity = ayon_api.get_folder_by_path(
project_name, folder_path, fields={"id"}
)
if not folder_entity:
raise ValueError("No current folder is set.")
folder_id = folder_entity["id"]
node_ids = []
for node in nodes:
_, uid = str(uuid.uuid4()).rsplit("-", 1)
unique_id = "{}:{}".format(folder_id, uid)
node_ids.append((node, unique_id))
return node_ids
def get_id_required_nodes():
valid_types = ["geometry"]
nodes = {n for n in hou.node("/out").children() if
n.type().name() in valid_types}
return list(nodes)
def get_output_parameter(node): def get_output_parameter(node):
"""Return the render output parameter of the given node """Return the render output parameter of the given node
@ -526,7 +447,7 @@ def maintained_selection():
node.setSelected(on=True) node.setSelected(on=True)
def reset_framerange(): def reset_framerange(fps=True, frame_range=True):
"""Set frame range and FPS to current folder.""" """Set frame range and FPS to current folder."""
project_name = get_current_project_name() project_name = get_current_project_name()
@ -535,29 +456,32 @@ def reset_framerange():
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
folder_attributes = folder_entity["attrib"] folder_attributes = folder_entity["attrib"]
# Get FPS # Set FPS
fps = get_folder_fps(folder_entity) if fps:
fps = get_folder_fps(folder_entity)
print("Setting scene FPS to {}".format(int(fps)))
set_scene_fps(fps)
# Get Start and End Frames if frame_range:
frame_start = folder_attributes.get("frameStart")
frame_end = folder_attributes.get("frameEnd")
if frame_start is None or frame_end is None: # Set Start and End Frames
log.warning("No edit information found for '{}'".format(folder_path)) frame_start = folder_attributes.get("frameStart")
return frame_end = folder_attributes.get("frameEnd")
handle_start = folder_attributes.get("handleStart", 0) if frame_start is None or frame_end is None:
handle_end = folder_attributes.get("handleEnd", 0) log.warning("No edit information found for '%s'", folder_path)
return
frame_start -= int(handle_start) handle_start = folder_attributes.get("handleStart", 0)
frame_end += int(handle_end) handle_end = folder_attributes.get("handleEnd", 0)
# Set frame range and FPS frame_start -= int(handle_start)
print("Setting scene FPS to {}".format(int(fps))) frame_end += int(handle_end)
set_scene_fps(fps)
hou.playbar.setFrameRange(frame_start, frame_end) # Set frame range and FPS
hou.playbar.setPlaybackRange(frame_start, frame_end) hou.playbar.setFrameRange(frame_start, frame_end)
hou.setFrame(frame_start) hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)
def get_main_window(): def get_main_window():
@ -1072,3 +996,84 @@ def add_self_publish_button(node):
template = node.parmTemplateGroup() template = node.parmTemplateGroup()
template.insertBefore((0,), button_parm) template.insertBefore((0,), button_parm)
node.setParmTemplateGroup(template) node.setParmTemplateGroup(template)
def update_content_on_context_change():
"""Update all Creator instances to current asset"""
host = registered_host()
context = host.get_current_context()
folder_path = context["folder_path"]
task = context["task_name"]
create_context = CreateContext(host, reset=True)
for instance in create_context.instances:
instance_folder_path = instance.get("folderPath")
if instance_folder_path and instance_folder_path != folder_path:
instance["folderPath"] = folder_path
instance_task = instance.get("task")
if instance_task and instance_task != task:
instance["task"] = task
create_context.save_changes()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset workfile FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset workfile start and end frame ranges",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
if options["fps"] or options["frame_range"]:
reset_framerange(
fps=options["fps"],
frame_range=options["frame_range"]
)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Pipeline tools for OpenPype Houdini integration.""" """Pipeline tools for OpenPype Houdini integration."""
import os import os
import sys
import logging import logging
import hou # noqa import hou # noqa
@ -39,6 +38,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create") CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
ABOUT_TO_SAVE = False
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "houdini" name = "houdini"
@ -61,10 +63,12 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
log.info("Installing callbacks ... ") log.info("Installing callbacks ... ")
# register_event_callback("init", on_init) # register_event_callback("init", on_init)
self._register_callbacks() self._register_callbacks()
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("before.save", before_save) register_event_callback("before.save", before_save)
register_event_callback("save", on_save) register_event_callback("save", on_save)
register_event_callback("open", on_open) register_event_callback("open", on_open)
register_event_callback("new", on_new) register_event_callback("new", on_new)
register_event_callback("taskChanged", on_task_changed)
self._has_been_setup = True self._has_been_setup = True
@ -166,7 +170,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
if not op_ctx: if not op_ctx:
op_ctx = self.create_context_node() op_ctx = self.create_context_node()
lib.imprint(op_ctx, data) lib.imprint(op_ctx, data, update=True)
def get_context_data(self): def get_context_data(self):
op_ctx = hou.node(CONTEXT_CONTAINER) op_ctx = hou.node(CONTEXT_CONTAINER)
@ -287,6 +291,11 @@ def ls():
yield parse_container(container) yield parse_container(container)
def before_workfile_save(event):
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = True
def before_save(): def before_save():
return lib.validate_fps() return lib.validate_fps()
@ -302,6 +311,17 @@ def on_save():
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)
# We are now starting the actual save directly
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = False
def on_task_changed():
global ABOUT_TO_SAVE
if not IS_HEADLESS and ABOUT_TO_SAVE:
# Let's prompt the user to update the context settings or not
lib.prompt_reset_context()
def _show_outdated_content_popup(): def _show_outdated_content_popup():
# Get main window # Get main window

View file

@ -15,6 +15,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
product_type = "redshift_rop" product_type = "redshift_rop"
icon = "magic" icon = "magic"
ext = "exr" ext = "exr"
multi_layered_mode = "No Multi-Layered EXR File"
# Default to split export and render jobs # Default to split export and render jobs
split_render = True split_render = True
@ -55,25 +56,36 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
# Set the linked rop to the Redshift ROP # Set the linked rop to the Redshift ROP
ipr_rop.parm("linked_rop").set(instance_node.path()) ipr_rop.parm("linked_rop").set(instance_node.path())
ext = pre_create_data.get("image_format") ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format( multi_layered_mode = pre_create_data.get("multi_layered_mode")
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
product_name=product_name,
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
)
ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3} ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3}
multilayer_mode_index = {"No Multi-Layered EXR File": "1",
"Full Multi-Layered EXR File": "2" }
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
product_name=product_name,
fmt="$AOV.$F4.{ext}".format(ext=ext)
)
if multilayer_mode_index[multi_layered_mode] == "1":
multipart = False
elif multilayer_mode_index[multi_layered_mode] == "2":
multipart = True
parms = { parms = {
# Render frame range # Render frame range
"trange": 1, "trange": 1,
# Redshift ROP settings # Redshift ROP settings
"RS_outputFileNamePrefix": filepath, "RS_outputFileNamePrefix": filepath,
"RS_outputMultilayerMode": "1", # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty", "RS_outputBeautyAOVSuffix": "beauty",
"RS_outputFileFormat": ext_format_index[ext], "RS_outputFileFormat": ext_format_index[ext],
} }
if ext == "exr":
parms["RS_outputMultilayerMode"] = multilayer_mode_index[multi_layered_mode]
parms["RS_aovMultipart"] = multipart
if self.selected_nodes: if self.selected_nodes:
# set up the render camera from the selected node # set up the render camera from the selected node
@ -111,6 +123,11 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
image_format_enum = [ image_format_enum = [
"exr", "tif", "jpg", "png", "exr", "tif", "jpg", "png",
] ]
multi_layered_mode = [
"No Multi-Layered EXR File",
"Full Multi-Layered EXR File"
]
return attrs + [ return attrs + [
BoolDef("farm", BoolDef("farm",
@ -122,5 +139,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
EnumDef("image_format", EnumDef("image_format",
image_format_enum, image_format_enum,
default=self.ext, default=self.ext,
label="Image Format Options") label="Image Format Options"),
EnumDef("multi_layered_mode",
multi_layered_mode,
default=self.multi_layered_mode,
label="Multi-Layered EXR")
] ]

View file

@ -76,8 +76,8 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
return return
# Include handles # Include handles
start -= version_data.get("handleStart", 0) start -= version_attributes.get("handleStart", 0)
end += version_data.get("handleEnd", 0) end += version_attributes.get("handleEnd", 0)
hou.playbar.setFrameRange(start, end) hou.playbar.setFrameRange(start, end)
hou.playbar.setPlaybackRange(start, end) hou.playbar.setPlaybackRange(start, end)

View file

@ -59,7 +59,7 @@ class AbcLoader(load.LoaderPlugin):
normal_node.setInput(0, unpack) normal_node.setInput(0, unpack)
null = container.createNode("null", node_name="OUT".format(name)) null = container.createNode("null", node_name="OUT")
null.setInput(0, normal_node) null.setInput(0, normal_node)
# Ensure display flag is on the Alembic input node and not on the OUT # Ensure display flag is on the Alembic input node and not on the OUT

View file

@ -167,6 +167,9 @@ class CameraLoader(load.LoaderPlugin):
temp_camera.destroy() temp_camera.destroy()
def switch(self, container, context):
self.update(container, context)
def remove(self, container): def remove(self, container):
node = container["node"] node = container["node"]
@ -195,7 +198,6 @@ class CameraLoader(load.LoaderPlugin):
def _match_maya_render_mask(self, camera): def _match_maya_render_mask(self, camera):
"""Workaround to match Maya render mask in Houdini""" """Workaround to match Maya render mask in Houdini"""
# print("Setting match maya render mask ")
parm = camera.parm("aperture") parm = camera.parm("aperture")
expression = parm.expression() expression = parm.expression()
expression = expression.replace("return ", "aperture = ") expression = expression.replace("return ", "aperture = ")

View file

@ -0,0 +1,129 @@
import os
import re
from ayon_core.pipeline import load
from openpype.hosts.houdini.api import pipeline
import hou
class FilePathLoader(load.LoaderPlugin):
"""Load a managed filepath to a null node.
This is useful if for a particular workflow there is no existing loader
yet. A Houdini artists can load as the generic filepath loader and then
reference the relevant Houdini parm to use the exact value. The benefit
is that this filepath will be managed and can be updated as usual.
"""
label = "Load filepath to node"
order = 9
icon = "link"
color = "white"
product_types = {"*"}
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a null node
container = obj.createNode("null", node_name=node_name)
# Destroy any children
for node in container.children():
node.destroy()
# Add filepath attribute, set value as default value
filepath = self.format_path(
path=self.filepath_from_context(context),
representation=context["representation"]
)
parm_template_group = container.parmTemplateGroup()
attr_folder = hou.FolderParmTemplate("attributes_folder", "Attributes")
parm = hou.StringParmTemplate(name="filepath",
label="Filepath",
num_components=1,
default_value=(filepath,))
attr_folder.addParmTemplate(parm)
parm_template_group.append(attr_folder)
# Hide some default labels
for folder_label in ["Transform", "Render", "Misc", "Redshift OBJ"]:
folder = parm_template_group.findFolder(folder_label)
if not folder:
continue
parm_template_group.hideFolder(folder_label, True)
container.setParmTemplateGroup(parm_template_group)
container.setDisplayFlag(False)
container.setSelectableInViewport(False)
container.useXray(False)
nodes = [container]
self[:] = nodes
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
# Update the file path
representation_entity = context["representation"]
file_path = self.format_path(
path=self.filepath_from_context(context),
representation=representation_entity
)
node = container["node"]
node.setParms({
"filepath": file_path,
"representation": str(representation_entity["id"])
})
# Update the parameter default value (cosmetics)
parm_template_group = node.parmTemplateGroup()
parm = parm_template_group.find("filepath")
parm.setDefaultValue((file_path,))
parm_template_group.replace(parm_template_group.find("filepath"),
parm)
node.setParmTemplateGroup(parm_template_group)
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
node = container["node"]
node.destroy()
@staticmethod
def format_path(path: str, representation: dict) -> str:
"""Format file path for sequence with $F."""
if not os.path.exists(path):
raise RuntimeError("Path does not exist: %s" % path)
# The path is either a single file or sequence in a folder.
frame = representation["context"].get("frame")
if frame is not None:
# Substitute frame number in sequence with $F with padding
ext = representation.get("ext", representation["name"])
token = "$F{}".format(len(frame)) # e.g. $F4
pattern = r"\.(\d+)\.{ext}$".format(ext=re.escape(ext))
path = re.sub(pattern, ".{}.{}".format(token, ext), path)
return os.path.normpath(path).replace("\\", "/")

View file

@ -0,0 +1,77 @@
import os
from ayon_core.pipeline import load
from ayon_core.hosts.houdini.api import pipeline
class SopUsdImportLoader(load.LoaderPlugin):
"""Load USD to SOPs via `usdimport`"""
label = "Load USD to SOPs"
product_types = {"*"}
representations = ["usd"]
order = -6
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Format file name, Houdini only wants forward slashes
file_path = self.filepath_from_context(context)
file_path = os.path.normpath(file_path)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
# Create a usdimport node
usdimport = container.createNode("usdimport", node_name=node_name)
usdimport.setParms({"filepath1": file_path})
# Set new position for unpack node else it gets cluttered
nodes = [container, usdimport]
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
node = container["node"]
try:
usdimport_node = next(
n for n in node.children() if n.type().name() == "usdimport"
)
except StopIteration:
self.log.error("Could not find node of type `usdimport`")
return
# Update the file path
file_path = self.filepath_from_context(context)
file_path = file_path.replace("\\", "/")
usdimport_node.setParms({"filepath1": file_path})
# Update attribute
node.setParms({"representation": context["representation"]["id"]})
def remove(self, container):
node = container["node"]
node.destroy()
def switch(self, container, representation):
self.update(container, representation)

View file

@ -41,23 +41,23 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size) self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "picture") default_prefix = evalParmNoFrame(rop, "picture")
render_products = [] render_products = []
# Default beauty AOV # Default beauty AOV
beauty_product = self.get_render_product_name( beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None prefix=default_prefix, suffix=None
) )
render_products.append(beauty_product) render_products.append(beauty_product)
files_by_aov = { files_by_aov = {
"beauty": self.generate_expected_files(instance, "beauty": self.generate_expected_files(instance,
beauty_product) beauty_product)
} }
filenames = list(render_products) filenames = list(render_products)
instance.data["files"] = filenames instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["renderProducts"] = colorspace.ARenderProduct()
for product in render_products: for product in render_products:
self.log.debug("Found render product: %s" % product) self.log.debug("Found render product: %s" % product)

View file

@ -41,57 +41,57 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size) self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "vm_picture") default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = [] render_products = []
# Store whether we are splitting the render job (export + render) # Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval()) split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render instance.data["splitRender"] = split_render
export_prefix = None export_prefix = None
export_products = [] export_products = []
if split_render: if split_render:
export_prefix = evalParmNoFrame( export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0" rop, "soho_diskfile", pad_character="0"
)
beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
) )
render_products.append(beauty_product) beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
files_by_aov = { # Default beauty AOV
"beauty": self.generate_expected_files(instance, beauty_product = self.get_render_product_name(
beauty_product) prefix=default_prefix, suffix=None
} )
render_products.append(beauty_product)
aov_numbers = rop.evalParm("vm_numaux") files_by_aov = {
if aov_numbers > 0: "beauty": self.generate_expected_files(instance,
# get the filenames of the AOVs beauty_product)
for i in range(1, aov_numbers + 1): }
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
for product in render_products: for product in render_products:
self.log.debug("Found render product: %s" % product) self.log.debug("Found render product: %s" % product)

View file

@ -60,15 +60,22 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["ifdFile"] = beauty_export_product instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products) instance.data["exportFiles"] = list(export_products)
# Default beauty AOV full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2")
if full_exr_mode:
# Ignore beauty suffix if full mode is enabled
# As this is what the rop does.
beauty_suffix = ""
# Default beauty/main layer AOV
beauty_product = self.get_render_product_name( beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=beauty_suffix prefix=default_prefix, suffix=beauty_suffix
) )
render_products = [beauty_product] render_products = [beauty_product]
files_by_aov = { files_by_aov = {
"_": self.generate_expected_files(instance, beauty_suffix: self.generate_expected_files(instance,
beauty_product)} beauty_product)
}
aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode() aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode()
if aovs_rop: if aovs_rop:
rop = aovs_rop rop = aovs_rop
@ -89,11 +96,14 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
if not aov_prefix: if not aov_prefix:
aov_prefix = default_prefix aov_prefix = default_prefix
aov_product = self.get_render_product_name(aov_prefix, aov_suffix) if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \
render_products.append(aov_product) not full_exr_mode:
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
render_products.append(aov_product)
files_by_aov[aov_suffix] = self.generate_expected_files(instance, files_by_aov[aov_suffix] = self.generate_expected_files(instance,
aov_product) # noqa aov_product) # noqa
for product in render_products: for product in render_products:
self.log.debug("Found render product: %s" % product) self.log.debug("Found render product: %s" % product)
@ -121,7 +131,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
# When AOV is explicitly defined in prefix we just swap it out # When AOV is explicitly defined in prefix we just swap it out
# directly with the AOV suffix to embed it. # directly with the AOV suffix to embed it.
# Note: ${AOV} seems to be evaluated in the parameter as %AOV% # Note: '$AOV' seems to be evaluated in the parameter as '%AOV%'
has_aov_in_prefix = "%AOV%" in prefix has_aov_in_prefix = "%AOV%" in prefix
if has_aov_in_prefix: if has_aov_in_prefix:
# It seems that when some special separator characters are present # It seems that when some special separator characters are present

View file

@ -71,6 +71,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
# the isinstance check above should be stricter than this category # the isinstance check above should be stricter than this category
if output_node.type().category().name() != "Cop2": if output_node.type().category().name() != "Cop2":
raise PublishValidationError( raise PublishValidationError(
("Output node %s is not of category Cop2. " (
"This is a bug...").format(output_node.path()), "Output node {} is not of category Cop2."
" This is a bug..."
).format(output_node.path()),
title=cls.label) title=cls.label)

View file

@ -7,7 +7,6 @@ from ayon_core.hosts.max.api.lib import (
maintained_selection, maintained_selection,
object_transform_set object_transform_set
) )
from ayon_core.hosts.max.api.lib import maintained_selection
from ayon_core.hosts.max.api.pipeline import ( from ayon_core.hosts.max.api.pipeline import (
containerise, containerise,
get_previous_loaded_object, get_previous_loaded_object,

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pyblish.api import pyblish.api
from ayon_core.pipeline import PublishValidationError from ayon_core.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateCameraContent(pyblish.api.InstancePlugin): class ValidateCameraContent(pyblish.api.InstancePlugin):

View file

@ -140,7 +140,7 @@ class ValidateRenderPasses(OptionalPyblishPluginMixin,
invalid = [] invalid = []
if instance.name not in file_name: if instance.name not in file_name:
cls.log.error("The renderpass filename should contain the instance name.") cls.log.error("The renderpass filename should contain the instance name.")
invalid.append((f"Invalid instance name", invalid.append(("Invalid instance name",
file_name)) file_name))
if renderpass is not None: if renderpass is not None:
if not file_name.rstrip(".").endswith(renderpass): if not file_name.rstrip(".").endswith(renderpass):

View file

@ -131,7 +131,7 @@ def get_main_window():
def suspended_refresh(suspend=True): def suspended_refresh(suspend=True):
"""Suspend viewport refreshes """Suspend viewport refreshes
cmds.ogs(pause=True) is a toggle so we cant pass False. cmds.ogs(pause=True) is a toggle so we can't pass False.
""" """
if IS_HEADLESS: if IS_HEADLESS:
yield yield
@ -583,7 +583,7 @@ def pairwise(iterable):
def collect_animation_defs(fps=False): def collect_animation_defs(fps=False):
"""Get the basic animation attribute defintions for the publisher. """Get the basic animation attribute definitions for the publisher.
Returns: Returns:
OrderedDict OrderedDict
@ -2152,9 +2152,13 @@ def get_related_sets(node):
sets = cmds.ls(sets) sets = cmds.ls(sets)
# Ignore `avalon.container` # Ignore `avalon.container`
sets = [s for s in sets if sets = [
not cmds.attributeQuery("id", node=s, exists=True) or s for s in sets
not cmds.getAttr("%s.id" % s) in ignored] if (
not cmds.attributeQuery("id", node=s, exists=True)
or cmds.getAttr(f"{s}.id") not in ignored
)
]
# Exclude deformer sets (`type=2` for `maya.cmds.listSets`) # Exclude deformer sets (`type=2` for `maya.cmds.listSets`)
deformer_sets = cmds.listSets(object=node, deformer_sets = cmds.listSets(object=node,
@ -3834,7 +3838,7 @@ def get_color_management_output_transform():
def image_info(file_path): def image_info(file_path):
# type: (str) -> dict # type: (str) -> dict
"""Based on tha texture path, get its bit depth and format information. """Based on the texture path, get its bit depth and format information.
Take reference from makeTx.py in Arnold: Take reference from makeTx.py in Arnold:
ImageInfo(filename): Get Image Information for colorspace ImageInfo(filename): Get Image Information for colorspace
AiTextureGetFormat(filename): Get Texture Format AiTextureGetFormat(filename): Get Texture Format

View file

@ -83,7 +83,7 @@ class MayaLegacyConvertor(ProductConvertorPlugin,
).format(product_type)) ).format(product_type))
continue continue
creator_id = product_type_to_id[family] creator_id = product_type_to_id[product_type]
creator = self.create_context.creators[creator_id] creator = self.create_context.creators[creator_id]
data["creator_identifier"] = creator_id data["creator_identifier"] = creator_id

View file

@ -20,13 +20,6 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator):
# Defined in settings # Defined in settings
joint_hints = set() joint_hints = set()
def apply_settings(self, project_settings):
"""Apply project settings to creator"""
settings = (
project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"]
)
self.joint_hints = set(settings.get("joint_hints", []))
def get_dynamic_data( def get_dynamic_data(
self, self,
project_name, project_name,

View file

@ -15,11 +15,6 @@ class CreateUnrealStaticMesh(plugin.MayaCreator):
# Defined in settings # Defined in settings
collision_prefixes = [] collision_prefixes = []
def apply_settings(self, project_settings):
"""Apply project settings to creator"""
settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"]
self.collision_prefixes = settings["collision_prefixes"]
def get_dynamic_data( def get_dynamic_data(
self, self,
project_name, project_name,

View file

@ -5,7 +5,7 @@ from ayon_core.hosts.maya.api import (
from ayon_core.lib import NumberDef from ayon_core.lib import NumberDef
class CreateYetiCache(plugin.MayaCreator): class CreateUnrealYetiCache(plugin.MayaCreator):
"""Output for procedural plugin nodes of Yeti """ """Output for procedural plugin nodes of Yeti """
identifier = "io.openpype.creators.maya.unrealyeticache" identifier = "io.openpype.creators.maya.unrealyeticache"

View file

@ -12,7 +12,6 @@ from ayon_core.hosts.maya.api.lib import (
unique_namespace, unique_namespace,
get_attribute_input, get_attribute_input,
maintained_selection, maintained_selection,
convert_to_maya_fps
) )
from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.pipeline import containerise
from ayon_core.hosts.maya.api.plugin import get_load_color_for_product_type from ayon_core.hosts.maya.api.plugin import get_load_color_for_product_type

View file

@ -1,5 +1,3 @@
import os
import maya.cmds as cmds import maya.cmds as cmds
from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.pipeline import containerise

View file

@ -1,10 +1,8 @@
import os
import copy import copy
from ayon_core.lib import EnumDef from ayon_core.lib import EnumDef
from ayon_core.pipeline import ( from ayon_core.pipeline import (
load, load,
get_representation_context,
get_current_host_name, get_current_host_name,
) )
from ayon_core.pipeline.load.utils import get_representation_path_from_context from ayon_core.pipeline.load.utils import get_representation_path_from_context

View file

@ -61,7 +61,7 @@ class LoadVDBtoArnold(load.LoaderPlugin):
path = self.filepath_from_context(context) path = self.filepath_from_context(context)
self._set_path(grid_node, self._set_path(grid_node,
path=path, path=path,
representation=context["representation"]) repre_entity=context["representation"])
# Lock the shape node so the user can't delete the transform/shape # Lock the shape node so the user can't delete the transform/shape
# as if it was referenced # as if it was referenced
@ -91,7 +91,7 @@ class LoadVDBtoArnold(load.LoaderPlugin):
assert len(grid_nodes) == 1, "This is a bug" assert len(grid_nodes) == 1, "This is a bug"
# Update the VRayVolumeGrid # Update the VRayVolumeGrid
self._set_path(grid_nodes[0], path=path, representation=repre_entity) self._set_path(grid_nodes[0], path=path, repre_entity=repre_entity)
# Update container representation # Update container representation
cmds.setAttr(container["objectName"] + ".representation", cmds.setAttr(container["objectName"] + ".representation",

View file

@ -78,7 +78,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
layer = instance.data["transientData"]["layer"] layer = instance.data["transientData"]["layer"]
objset = instance.data.get("instance_node") objset = instance.data.get("instance_node")
filepath = context.data["currentFile"].replace("\\", "/") filepath = context.data["currentFile"].replace("\\", "/")
workspace = context.data["workspaceDir"]
# check if layer is renderable # check if layer is renderable
if not layer.isRenderable(): if not layer.isRenderable():
@ -314,7 +313,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
if not extend_frames: if not extend_frames:
instance.data["overrideExistingFrame"] = False instance.data["overrideExistingFrame"] = False
# Update the instace # Update the instance
instance.data.update(data) instance.data.update(data)
@staticmethod @staticmethod

View file

@ -26,6 +26,10 @@ class ExtractAlembic(publish.Extractor):
families = ["pointcache", "model", "vrayproxy.alembic"] families = ["pointcache", "model", "vrayproxy.alembic"]
targets = ["local", "remote"] targets = ["local", "remote"]
# From settings
bake_attributes = []
bake_attribute_prefixes = []
def process(self, instance): def process(self, instance):
if instance.data.get("farm"): if instance.data.get("farm"):
self.log.debug("Should be processed on farm, skipping.") self.log.debug("Should be processed on farm, skipping.")
@ -40,10 +44,12 @@ class ExtractAlembic(publish.Extractor):
attrs = instance.data.get("attr", "").split(";") attrs = instance.data.get("attr", "").split(";")
attrs = [value for value in attrs if value.strip()] attrs = [value for value in attrs if value.strip()]
attrs += instance.data.get("userDefinedAttributes", []) attrs += instance.data.get("userDefinedAttributes", [])
attrs += self.bake_attributes
attrs += ["cbId"] attrs += ["cbId"]
attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = instance.data.get("attrPrefix", "").split(";")
attr_prefixes = [value for value in attr_prefixes if value.strip()] attr_prefixes = [value for value in attr_prefixes if value.strip()]
attr_prefixes += self.bake_attribute_prefixes
self.log.debug("Extracting pointcache..") self.log.debug("Extracting pointcache..")
dirname = self.staging_dir(instance) dirname = self.staging_dir(instance)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Create Unreal Skeletal Mesh data to be extracted as FBX.""" """Create Unreal Skeletal Mesh data to be extracted as FBX."""
import os import os
from contextlib import contextmanager
from maya import cmds # noqa from maya import cmds # noqa

View file

@ -74,7 +74,7 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor):
renamed_to_extract.append("|".join(node_path)) renamed_to_extract.append("|".join(node_path))
with renamed(original_parent, parent_node): with renamed(original_parent, parent_node):
self.log.debug("Extracting: {}".format(renamed_to_extract, path)) self.log.debug("Extracting: {}".format(renamed_to_extract))
fbx_exporter.export(renamed_to_extract, path) fbx_exporter.export(renamed_to_extract, path)
if "representations" not in instance.data: if "representations" not in instance.data:

View file

@ -5,13 +5,13 @@ from maya import cmds
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
class ExtractYetiCache(publish.Extractor): class ExtractUnrealYetiCache(publish.Extractor):
"""Producing Yeti cache files using scene time range. """Producing Yeti cache files using scene time range.
This will extract Yeti cache file sequence and fur settings. This will extract Yeti cache file sequence and fur settings.
""" """
label = "Extract Yeti Cache" label = "Extract Yeti Cache (Unreal)"
hosts = ["maya"] hosts = ["maya"]
families = ["yeticacheUE"] families = ["yeticacheUE"]

View file

@ -7,7 +7,6 @@ from maya import cmds
import pyblish.api import pyblish.api
from ayon_core.hosts.maya.api.lib import extract_alembic from ayon_core.hosts.maya.api.lib import extract_alembic
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.lib import StringTemplate
class ExtractWorkfileXgen(publish.Extractor): class ExtractWorkfileXgen(publish.Extractor):

View file

@ -9,7 +9,6 @@ from ayon_core.pipeline import publish
from ayon_core.hosts.maya.api.lib import ( from ayon_core.hosts.maya.api.lib import (
maintained_selection, attribute_values, write_xgen_file, delete_after maintained_selection, attribute_values, write_xgen_file, delete_after
) )
from ayon_core.lib import StringTemplate
class ExtractXgen(publish.Extractor): class ExtractXgen(publish.Extractor):

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Shape IDs mismatch original shape</title>
<description>## Shapes mismatch IDs with original shape
Meshes are detected in the **rig** where the (deformed) mesh has a different
`cbId` than the same mesh in its deformation history.
Theses should normally be the same.
### How to repair?
By using the repair action the IDs from the shape in history will be
copied to the deformed shape. For rig instances, in many cases the
correct fix is to use the repair action **unless** you explicitly tried
to update the `cbId` values on the meshes - in that case you actually want
to do to the reverse and copy the IDs from the deformed mesh to the history
mesh instead.
</description>
<detail>
### How does this happen?
When a deformer is applied in the scene on a referenced mesh that had no
deformers then Maya will create a new shape node for the mesh that
does not have the original id. Then on scene save new ids get created for the
meshes lacking a `cbId` and thus the mesh then has a different `cbId` than
the mesh in the deformation history.
</detail>
</error>
</root>

View file

@ -36,18 +36,18 @@ class ValidateSubsetName(pyblish.api.InstancePlugin):
) )
if not isinstance(product_name, six.string_types): if not isinstance(product_name, six.string_types):
raise TypeError(( raise PublishValidationError((
"Instance product name must be string, got: {0} ({1})" "Instance product name must be string, got: {0} ({1})"
).format(product_name, type(product_name))) ).format(product_name, type(product_name)))
# Ensure is not empty product # Ensure is not empty product
if not product_name: if not product_name:
raise ValueError( raise PublishValidationError(
"Instance product name is empty: {0}".format(product_name) "Instance product name is empty: {0}".format(product_name)
) )
# Validate product characters # Validate product characters
if not validate_name(product_name): if not validate_name(product_name):
raise ValueError(( raise PublishValidationError((
"Instance product name contains invalid characters: {0}" "Instance product name contains invalid characters: {0}"
).format(product_name)) ).format(product_name))

View file

@ -51,5 +51,5 @@ class ValidateMeshEmpty(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError( raise PublishValidationError(
"Meshes found in instance without any vertices: %s" % invalid "Meshes found without any vertices: %s" % invalid
) )

View file

@ -2,7 +2,11 @@ from maya import cmds
import pyblish.api import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ValidateMeshOrder, OptionalPyblishPluginMixin from ayon_core.pipeline.publish import (
ValidateMeshOrder,
OptionalPyblishPluginMixin,
PublishValidationError
)
class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin,
@ -20,6 +24,16 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin,
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction]
optional = True optional = True
description = (
"## Meshes with Lamina Faces\n"
"Detected meshes with lamina faces. <b>Lamina faces</b> are faces "
"that share all of their edges and thus are merged together on top of "
"each other.\n\n"
"### How to repair?\n"
"You can repair them by using Maya's modeling tool `Mesh > Cleanup..` "
"and select to cleanup matching polygons for lamina faces."
)
@staticmethod @staticmethod
def get_invalid(instance): def get_invalid(instance):
meshes = cmds.ls(instance, type='mesh', long=True) meshes = cmds.ls(instance, type='mesh', long=True)
@ -36,5 +50,6 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Meshes found with lamina faces: " raise PublishValidationError(
"{0}".format(invalid)) "Meshes found with lamina faces: {0}".format(invalid),
description=self.description)

View file

@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action
from ayon_core.hosts.maya.api import lib from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
@ -27,6 +28,15 @@ class ValidateMeshNgons(pyblish.api.Validator,
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction]
optional = True optional = True
description = (
"## Meshes with NGONs Faces\n"
"Detected meshes with NGON faces. **NGONS** are faces that "
"with more than four sides.\n\n"
"### How to repair?\n"
"You can repair them by usings Maya's modeling tool Mesh > Cleanup.. "
"and select to cleanup matching polygons for lamina faces."
)
@staticmethod @staticmethod
def get_invalid(instance): def get_invalid(instance):
@ -49,5 +59,6 @@ class ValidateMeshNgons(pyblish.api.Validator,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Meshes found with n-gons" raise PublishValidationError(
"values: {0}".format(invalid)) "Meshes found with n-gons: {0}".format(invalid),
description=self.description)

View file

@ -107,8 +107,9 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError("Shapes found with invalid shader " raise PublishValidationError(
"connections: {0}".format(invalid)) "Shapes found with invalid shader connections: "
"{0}".format(invalid))
@staticmethod @staticmethod
def get_invalid(instance): def get_invalid(instance):

View file

@ -6,7 +6,8 @@ from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairAction, RepairAction,
ValidateMeshOrder, ValidateMeshOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
@ -66,7 +67,7 @@ class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin,
if allowed: if allowed:
self.log.warning(message) self.log.warning(message)
else: else:
raise ValueError(message) raise PublishValidationError(message)
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairAction, RepairAction,
ValidateMeshOrder, ValidateMeshOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
@ -55,8 +56,8 @@ class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Meshes found without 'map1' " raise PublishValidationError(
"UV set: {0}".format(invalid)) "Meshes found without 'map1' UV set: {0}".format(invalid))
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -49,11 +49,17 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
invalid_namespaces = {get_namespace(node) for node in invalid}
raise PublishValidationError( raise PublishValidationError(
"Namespaces found:\n\n{0}".format( message="Namespaces found:\n\n{0}".format(
_as_report_list(sorted(invalid)) _as_report_list(sorted(invalid_namespaces))
), ),
title="Namespaces in model" title="Namespaces in model",
description=(
"## Namespaces found in model\n"
"It is not allowed to publish a model that contains "
"namespaces."
)
) )
@classmethod @classmethod

View file

@ -5,10 +5,12 @@ import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
class ValidateNodeNoGhosting(pyblish.api.InstancePlugin.
class ValidateNodeNoGhosting(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure nodes do not have ghosting enabled. """Ensure nodes do not have ghosting enabled.
@ -55,5 +57,5 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin.
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Nodes with ghosting enabled found: " raise PublishValidationError(
"{0}".format(invalid)) "Nodes with ghosting enabled found: {0}".format(invalid))

View file

@ -7,7 +7,7 @@ from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairAction, RepairAction,
ValidateContentsOrder, ValidateContentsOrder,
PublishValidationError, PublishXmlValidationError,
OptionalPyblishPluginMixin, OptionalPyblishPluginMixin,
get_plugin_settings, get_plugin_settings,
apply_plugin_settings_automatically apply_plugin_settings_automatically
@ -58,8 +58,20 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin,
# if a deformer has been created on the shape # if a deformer has been created on the shape
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError(
"Nodes found with mismatching IDs: {0}".format(invalid) # Use the short names
invalid = cmds.ls(invalid)
invalid.sort()
# Construct a human-readable list
invalid = "\n".join("- {}".format(node) for node in invalid)
raise PublishXmlValidationError(
plugin=ValidateRigOutSetNodeIds,
message=(
"Rig nodes have different IDs than their input "
"history: \n{0}".format(invalid)
)
) )
@classmethod @classmethod

View file

@ -1,5 +1,8 @@
import pyblish.api import pyblish.api
from ayon_core.pipeline.publish import ValidateContentsOrder from ayon_core.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError
)
class ValidateSetdressRoot(pyblish.api.InstancePlugin): class ValidateSetdressRoot(pyblish.api.InstancePlugin):
@ -20,4 +23,6 @@ class ValidateSetdressRoot(pyblish.api.InstancePlugin):
root = cmds.ls(set_member, assemblies=True, long=True) root = cmds.ls(set_member, assemblies=True, long=True)
if not root or root[0] not in set_member: if not root or root[0] not in set_member:
raise Exception("Setdress top root node is not being published.") raise PublishValidationError(
"Setdress top root node is not being published."
)

View file

@ -8,7 +8,8 @@ import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
RepairAction, RepairAction,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
@ -84,8 +85,8 @@ class ValidateShapeDefaultNames(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Incorrectly named shapes " raise PublishValidationError(
"found: {0}".format(invalid)) "Incorrectly named shapes found: {0}".format(invalid))
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -1,5 +1,8 @@
import pyblish.api import pyblish.api
from ayon_core.pipeline.publish import ValidateContentsOrder from ayon_core.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError
)
class ValidateSingleAssembly(pyblish.api.InstancePlugin): class ValidateSingleAssembly(pyblish.api.InstancePlugin):
@ -30,7 +33,11 @@ class ValidateSingleAssembly(pyblish.api.InstancePlugin):
# ensure unique (somehow `maya.cmds.ls` doesn't manage that) # ensure unique (somehow `maya.cmds.ls` doesn't manage that)
assemblies = set(assemblies) assemblies = set(assemblies)
assert len(assemblies) > 0, ( if len(assemblies) == 0:
"One assembly required for: %s (currently empty?)" % instance) raise PublishValidationError(
assert len(assemblies) < 2, ( "One assembly required for: %s (currently empty?)" % instance
'Multiple assemblies found: %s' % assemblies) )
elif len(assemblies) > 1:
raise PublishValidationError(
'Multiple assemblies found: %s' % assemblies
)

View file

@ -3,7 +3,11 @@ from maya import cmds
import pyblish.api import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ValidateContentsOrder,OptionalPyblishPluginMixin from ayon_core.pipeline.publish import (
ValidateContentsOrder,
OptionalPyblishPluginMixin,
PublishValidationError
)
class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin, class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin,
@ -30,8 +34,10 @@ class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Invalid skinCluster relationships " raise PublishValidationError(
"found on meshes: {0}".format(invalid)) "Invalid skinCluster relationships found on meshes: {0}"
.format(invalid)
)
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):

View file

@ -29,7 +29,7 @@ class ValidateStepSize(pyblish.api.InstancePlugin,
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):
objset = instance.data['name'] objset = instance.data['instance_node']
step = instance.data.get("step", 1.0) step = instance.data.get("step", 1.0)
if step < cls.MIN or step > cls.MAX: if step < cls.MIN or step > cls.MAX:
@ -47,4 +47,4 @@ class ValidateStepSize(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError( raise PublishValidationError(
"Invalid instances found: {0}".format(invalid)) "Instance found with invalid step size: {0}".format(invalid))

View file

@ -4,9 +4,11 @@ import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
class ValidateUniqueNames(pyblish.api.Validator, class ValidateUniqueNames(pyblish.api.Validator,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""transform names should be unique """transform names should be unique
@ -40,5 +42,5 @@ class ValidateUniqueNames(pyblish.api.Validator,
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Nodes found with none unique names. " raise PublishValidationError(
"values: {0}".format(invalid)) "Nodes found with non-unique names:\n{0}".format(invalid))

View file

@ -5,7 +5,8 @@ import pyblish.api
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateMeshOrder, ValidateMeshOrder,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
@ -26,8 +27,8 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin,
invalid = [] invalid = []
meshes = cmds.ls(instance, type="mesh", long=True) meshes = cmds.ls(instance, type="mesh", long=True)
for mesh in meshes: for mesh in meshes:
faces = cmds.polyEvaluate(mesh, f=True) faces = cmds.polyEvaluate(mesh, face=True)
tris = cmds.polyEvaluate(mesh, t=True) tris = cmds.polyEvaluate(mesh, triangle=True)
if faces != tris: if faces != tris:
invalid.append(mesh) invalid.append(mesh)
@ -37,5 +38,5 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin,
if not self.is_active(instance.data): if not self.is_active(instance.data):
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
assert len(invalid) == 0, ( if invalid:
"Found meshes without triangles") raise PublishValidationError("Found meshes without triangles")

View file

@ -6,7 +6,8 @@ import pyblish.api
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
RepairAction, RepairAction,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
@ -26,9 +27,10 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin,
if not self.is_active(context.data): if not self.is_active(context.data):
return return
assert cmds.upAxis(q=True, axis=True) == "z", ( if cmds.upAxis(q=True, axis=True) != "z":
"Invalid axis set as up axis" raise PublishValidationError(
) "Invalid axis set as up axis"
)
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -34,8 +34,9 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
start, end = self.get_frame_range(instance) start, end = self.get_frame_range(instance)
raise PublishValidationError("No visible nodes found in " raise PublishValidationError(
"frame range {}-{}.".format(start, end)) f"No visible nodes found in frame range {start}-{end}."
)
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):

View file

@ -3,6 +3,7 @@ from maya import cmds
from ayon_core.hosts.maya.api import lib from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
KnownPublishError,
PublishValidationError, PublishValidationError,
RepairAction, RepairAction,
ValidateContentsOrder, ValidateContentsOrder,
@ -35,11 +36,14 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin,
if not self.is_active(instance.data): if not self.is_active(instance.data):
return return
if instance.data.get("renderer") != "vray": if instance.data.get("renderer") != "vray":
# If not V-Ray ignore.. # If not V-Ray, ignore
return return
vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode") vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode")
assert vray_settings, "Please ensure a VRay Settings Node is present" if not vray_settings:
raise KnownPublishError(
"Please ensure a VRay Settings Node is present"
)
renderlayer = instance.data['renderlayer'] renderlayer = instance.data['renderlayer']
@ -51,8 +55,8 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin,
# during batch mode we invalidate the instance # during batch mode we invalidate the instance
if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer): if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer):
raise PublishValidationError( raise PublishValidationError(
("Renderlayer has distributed rendering enabled " "Renderlayer has distributed rendering enabled "
"but is not set to ignore in batch mode.")) "but is not set to ignore in batch mode.")
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):

View file

@ -6,9 +6,11 @@ from maya import cmds
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairContextAction, RepairContextAction,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
) )
class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Validate whether the V-Ray Render Elements (AOVs) include references. """Validate whether the V-Ray Render Elements (AOVs) include references.
@ -60,7 +62,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin,
self.log.error(( self.log.error((
"'Use referenced' not enabled in Vray Render Settings." "'Use referenced' not enabled in Vray Render Settings."
)) ))
raise AssertionError("Invalid render settings") raise PublishValidationError("Invalid render settings")
@classmethod @classmethod
def repair(cls, context): def repair(cls, context):

View file

@ -1,7 +1,10 @@
import pyblish.api import pyblish.api
from ayon_core.pipeline import KnownPublishError from ayon_core.pipeline.publish import (
from ayon_core.pipeline.publish import OptionalPyblishPluginMixin OptionalPyblishPluginMixin,
PublishValidationError
)
class ValidateVrayProxy(pyblish.api.InstancePlugin, class ValidateVrayProxy(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
@ -17,18 +20,18 @@ class ValidateVrayProxy(pyblish.api.InstancePlugin,
if not self.is_active(data): if not self.is_active(data):
return return
if not data["setMembers"]: if not data["setMembers"]:
raise KnownPublishError( raise PublishValidationError(
"'%s' is empty! This is a bug" % instance.name f"Instance '{instance.name}' is empty."
) )
if data["animation"]: if data["animation"]:
if data["frameEnd"] < data["frameStart"]: if data["frameEnd"] < data["frameStart"]:
raise KnownPublishError( raise PublishValidationError(
"End frame is smaller than start frame" "End frame is smaller than start frame"
) )
if not data["vrmesh"] and not data["alembic"]: if not data["vrmesh"] and not data["alembic"]:
raise KnownPublishError( raise PublishValidationError(
"Both vrmesh and alembic are off. Needs at least one to" "Both vrmesh and alembic are off. Needs at least one to"
" publish." " publish."
) )

View file

@ -34,7 +34,7 @@ class ValidateXgen(pyblish.api.InstancePlugin):
" Node type found: {}".format(node_type) " Node type found: {}".format(node_type)
) )
# Cant have inactive modifiers in collection cause Xgen will try and # Can't have inactive modifiers in collection cause Xgen will try and
# look for them when loading. # look for them when loading.
palette = instance.data["xgmPalette"].replace("|", "") palette = instance.data["xgmPalette"].replace("|", "")
inactive_modifiers = {} inactive_modifiers = {}

View file

@ -3,9 +3,11 @@ from maya import cmds
import pyblish.api import pyblish.api
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateContentsOrder, ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin
) )
class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin, class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Check if the render script callbacks will be used during the rendering """Check if the render script callbacks will be used during the rendering
@ -45,8 +47,8 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin,
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise ValueError("Invalid render callbacks found for '%s'!" raise PublishValidationError(
% instance.name) f"Invalid render callbacks found for '{instance.name}'.")
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):

View file

@ -1,3 +1,5 @@
import inspect
import pyblish.api import pyblish.api
import maya.cmds as cmds import maya.cmds as cmds
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
@ -8,7 +10,6 @@ from ayon_core.pipeline.publish import (
) )
class ValidateYetiRigCacheState(pyblish.api.InstancePlugin, class ValidateYetiRigCacheState(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Validate the I/O attributes of the node """Validate the I/O attributes of the node
@ -32,7 +33,10 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin,
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError("Nodes have incorrect I/O settings") raise PublishValidationError(
"Nodes have incorrect I/O settings",
description=inspect.getdoc(self)
)
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):

View file

@ -29,7 +29,8 @@ class AssetModel(models.TreeModel):
self.beginResetModel() self.beginResetModel()
# Add the items sorted by label # Add the items sorted by label
sorter = lambda x: x["label"] def sorter(x):
return x["label"]
for item in sorted(items, key=sorter): for item in sorted(items, key=sorter):

View file

@ -389,7 +389,13 @@ def imprint(node, data, tab=None):
""" """
for knob in create_knobs(data, tab): for knob in create_knobs(data, tab):
node.addKnob(knob) # If knob name exists we set the value. Technically there could be
# multiple knobs with the same name, but the intent is not to have
# duplicated knobs so we do not account for that.
if knob.name() in node.knobs().keys():
node[knob.name()].setValue(knob.value())
else:
node.addKnob(knob)
@deprecated @deprecated
@ -814,7 +820,7 @@ def on_script_load():
def check_inventory_versions(): def check_inventory_versions():
""" """
Actual version idetifier of Loaded containers Actual version identifier of Loaded containers
Any time this function is run it will check all nodes and filter only Any time this function is run it will check all nodes and filter only
Loader nodes for its version. It will get all versions from database Loader nodes for its version. It will get all versions from database
@ -921,7 +927,7 @@ def writes_version_sync():
for each in nuke.allNodes(filter="Write"): for each in nuke.allNodes(filter="Write"):
# check if the node is avalon tracked # check if the node is avalon tracked
if _NODE_TAB_NAME not in each.knobs(): if NODE_TAB_NAME not in each.knobs():
continue continue
avalon_knob_data = read_avalon_data(each) avalon_knob_data = read_avalon_data(each)
@ -2381,7 +2387,7 @@ def launch_workfiles_app():
Context.workfiles_launched = True Context.workfiles_launched = True
# get all imortant settings # get all important settings
open_at_start = env_value_to_bool( open_at_start = env_value_to_bool(
env_key="AYON_WORKFILE_TOOL_ON_START", env_key="AYON_WORKFILE_TOOL_ON_START",
default=None) default=None)

View file

@ -30,13 +30,11 @@ from ayon_core.tools.utils import host_tools
from ayon_core.hosts.nuke import NUKE_ROOT_DIR from ayon_core.hosts.nuke import NUKE_ROOT_DIR
from ayon_core.tools.workfile_template_build import open_template_ui from ayon_core.tools.workfile_template_build import open_template_ui
from .command import viewer_update_and_undo_stop
from .lib import ( from .lib import (
Context, Context,
ROOT_DATA_KNOB, ROOT_DATA_KNOB,
INSTANCE_DATA_KNOB, INSTANCE_DATA_KNOB,
get_main_window, get_main_window,
add_publish_knob,
WorkfileSettings, WorkfileSettings,
# TODO: remove this once workfile builder will be removed # TODO: remove this once workfile builder will be removed
process_workfile_builder, process_workfile_builder,

Some files were not shown because too many files have changed in this diff Show more