diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py b/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py index dd2e33df80..731e91ad76 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py @@ -13,6 +13,9 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): families = ["model", "camera", "rig", "action", "layout", "blendScene"] optional = True + # From settings + compress = False + def process(self, instance): if not self.is_active(instance.data): return @@ -53,7 +56,7 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): if node.image and node.image.packed_file is None: node.image.pack() - bpy.data.libraries.write(filepath, data_blocks) + bpy.data.libraries.write(filepath, data_blocks, compress=self.compress) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py b/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py index da663b46ea..64009c4b6c 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py @@ -16,6 +16,9 @@ class ExtractBlendAnimation( families = ["animation"] optional = True + # From settings + compress = False + def process(self, instance): if not self.is_active(instance.data): return @@ -46,7 +49,7 @@ class ExtractBlendAnimation( data_blocks.add(child.animation_data.action) data_blocks.add(obj) - bpy.data.libraries.write(filepath, data_blocks) + bpy.data.libraries.write(filepath, data_blocks, compress=self.compress) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_instance_in_context.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..26708e306b --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_instance_in_context.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" + +import pyblish.api +from ayon_core.hosts.houdini.api.action import SelectROPAction +from ayon_core.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) + + +class ValidateInstanceInContextHoudini(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validator to check if instance asset match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + """ + # Similar to maya-equivalent `ValidateInstanceInContext` + + order = ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["houdini"] + actions = [SelectROPAction, RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + + folder_path = instance.data.get("folderPath") + task = instance.data.get("task") + context = self.get_context(instance) + if (folder_path, task) != context: + context_label = "{} > {}".format(*context) + instance_label = "{} > {}".format(folder_path, task) + + raise PublishValidationError( + message=( + "Instance '{}' publishes to different asset than current " + "context: {}. Current context: {}".format( + instance.name, instance_label, context_label + ) + ), + description=( + "## Publishing to a different asset\n" + "There are publish instances present which are publishing " + "into a different asset than your current context.\n\n" + "Usually this is not what you want but there can be cases " + "where you might want to publish into another asset or " + "shot. If that's the case you can disable the validation " + "on the instance to ignore it." + ) + ) + + @classmethod + def repair(cls, instance): + context_folder, context_task = cls.get_context(instance) + + create_context = instance.context.data["create_context"] + instance_id = instance.data["instance_id"] + created_instance = create_context.get_instance_by_id( + instance_id + ) + created_instance["folderPath"] = context_folder + created_instance["task"] = context_task + create_context.save_changes() + + @staticmethod + def get_context(instance): + """Return folderPath, task from publishing context data""" + context = instance.context + return context.data["folderPath"], context.data["task"] diff --git a/client/ayon_core/hosts/max/plugins/create/create_workfile.py b/client/ayon_core/hosts/max/plugins/create/create_workfile.py index 1552149413..058fe10eb2 100644 --- a/client/ayon_core/hosts/max/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/max/plugins/create/create_workfile.py @@ -98,21 +98,6 @@ class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): created_inst.data_to_store() ) - def remove_instances(self, instances): - """Remove specified instance from the scene. - - This is only removing `id` parameter so instance is no longer - instance, because it might contain valuable data for artist. - - """ - for instance in instances: - instance_node = rt.GetNodeByName( - instance.data.get("instance_node")) - if instance_node: - rt.Delete(instance_node) - - self._remove_instance_from_context(instance) - def create_node(self, product_name): if rt.getNodeByName(product_name): node = rt.getNodeByName(product_name) diff --git a/client/ayon_core/hosts/nuke/api/utils.py b/client/ayon_core/hosts/nuke/api/utils.py index d738ba5464..1bfc1919fa 100644 --- a/client/ayon_core/hosts/nuke/api/utils.py +++ b/client/ayon_core/hosts/nuke/api/utils.py @@ -1,4 +1,6 @@ import os +import re + import nuke from ayon_core import resources @@ -103,9 +105,8 @@ def colorspace_exists_on_node(node, colorspace_name): except ValueError: # knob is not available on input node return False - all_clrs = get_colorspace_list(colorspace_knob) - return colorspace_name in all_clrs + return colorspace_name in get_colorspace_list(colorspace_knob) def get_colorspace_list(colorspace_knob): @@ -117,19 +118,22 @@ def get_colorspace_list(colorspace_knob): Returns: list: list of strings names of profiles """ + results = [] - all_clrs = list(colorspace_knob.values()) - reduced_clrs = [] + # This pattern is to match with roles which uses an indentation and + # parentheses with original colorspace. The value returned from the + # colorspace is the string before the indentation, so we'll need to + # convert the values to match with value returned from the knob, + # ei. knob.value(). + pattern = r".*\t.* \(.*\)" + for colorspace in nuke.getColorspaceList(colorspace_knob): + match = re.search(pattern, colorspace) + if match: + results.append(colorspace.split("\t", 1)[0]) + else: + results.append(colorspace) - if not colorspace_knob.getFlag(nuke.STRIP_CASCADE_PREFIX): - return all_clrs - - # strip colorspace with nested path - for clrs in all_clrs: - clrs = clrs.split('/')[-1] - reduced_clrs.append(clrs) - - return reduced_clrs + return results def is_headless(): diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index e9e71baa76..98c3303eb8 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -11,6 +11,9 @@ from ayon_core.pipeline import ( get_current_project_name, get_representation_path, ) +from ayon_core.pipeline.colorspace import ( + get_imageio_file_rules_colorspace_from_filepath +) from ayon_core.hosts.nuke.api.lib import ( get_imageio_input_colorspace, maintained_selection @@ -101,7 +104,6 @@ class LoadClip(plugin.NukeLoader): filepath = self.filepath_from_context(context) filepath = filepath.replace("\\", "/") - self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) @@ -154,8 +156,8 @@ class LoadClip(plugin.NukeLoader): with viewer_update_and_undo_stop(): read_node["file"].setValue(filepath) - used_colorspace = self._set_colorspace( - read_node, version_data, representation["data"], filepath) + self.set_colorspace_to_node( + read_node, filepath, version, representation) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -180,8 +182,6 @@ class LoadClip(plugin.NukeLoader): colorspace = representation["data"].get(key) colorspace = colorspace or version_data.get(key) data_imprint["db_colorspace"] = colorspace - if used_colorspace: - data_imprint["used_colorspace"] = used_colorspace else: value_ = context["version"]['data'].get( key, str(None)) @@ -304,8 +304,9 @@ class LoadClip(plugin.NukeLoader): # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): - used_colorspace = self._set_colorspace( - read_node, version_data, repre_doc["data"], filepath) + self.set_colorspace_to_node( + read_node, filepath, version_doc, repre_doc + ) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -322,10 +323,6 @@ class LoadClip(plugin.NukeLoader): "author": version_data.get("author") } - # add used colorspace if found any - if used_colorspace: - updated_dict["used_colorspace"] = used_colorspace - last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) @@ -352,6 +349,36 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) + def set_colorspace_to_node( + self, + read_node, + filepath, + version_doc, + representation_doc, + ): + """Set colorspace to read node. + + Sets colorspace with available names validation. + + Args: + read_node (nuke.Node): The nuke's read node + filepath (str): file path + version_doc (dict): version document + representation_doc (dict): representation document + + """ + used_colorspace = self._get_colorspace_data( + version_doc, representation_doc, filepath) + + if ( + used_colorspace + and colorspace_exists_on_node(read_node, used_colorspace) + ): + self.log.info(f"Used colorspace: {used_colorspace}") + read_node["colorspace"].setValue(used_colorspace) + else: + self.log.info("Colorspace not set...") + def remove(self, container): read_node = container["node"] assert read_node.Class() == "Read", "Must be Read" @@ -452,25 +479,47 @@ class LoadClip(plugin.NukeLoader): return self.node_name_template.format(**name_data) - def _set_colorspace(self, node, version_data, repre_data, path): - output_color = None - path = path.replace("\\", "/") - # get colorspace - colorspace = repre_data.get("colorspace") - colorspace = colorspace or version_data.get("colorspace") + def _get_colorspace_data(self, version_doc, representation_doc, filepath): + """Get colorspace data from version and representation documents - # colorspace from `project_settings/nuke/imageio/regex_inputs` - iio_colorspace = get_imageio_input_colorspace(path) + Args: + version_doc (dict): version document + representation_doc (dict): representation document + filepath (str): file path - # Set colorspace defined in version data - if ( - colorspace is not None - and colorspace_exists_on_node(node, str(colorspace)) - ): - node["colorspace"].setValue(str(colorspace)) - output_color = str(colorspace) - elif iio_colorspace is not None: - node["colorspace"].setValue(iio_colorspace) - output_color = iio_colorspace + Returns: + Any[str,None]: colorspace name or None + """ + # Get backward compatible colorspace key. + colorspace = representation_doc["data"].get("colorspace") + self.log.debug( + f"Colorspace from representation colorspace: {colorspace}" + ) - return output_color + # Get backward compatible version data key if colorspace is not found. + colorspace = colorspace or version_doc["data"].get("colorspace") + self.log.debug(f"Colorspace from version colorspace: {colorspace}") + + # Get colorspace from representation colorspaceData if colorspace is + # not found. + colorspace_data = representation_doc["data"].get("colorspaceData", {}) + colorspace = colorspace or colorspace_data.get("colorspace") + self.log.debug( + f"Colorspace from representation colorspaceData: {colorspace}" + ) + + # check if any filerules are not applicable + new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa + filepath, "nuke", get_current_project_name() + ) + self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}") + + # colorspace from `project_settings/nuke/imageio/regexInputs` + old_parsed_colorspace = get_imageio_input_colorspace(filepath) + self.log.debug(f"Colorspace old filerules: {old_parsed_colorspace}") + + return ( + new_parsed_colorspace + or old_parsed_colorspace + or colorspace + ) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py b/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py index 3baa0cd9b5..ac30bd6051 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py @@ -17,7 +17,8 @@ class CollectSlate(pyblish.api.InstancePlugin): ( n_ for n_ in nuke.allNodes() if "slate" in n_.name().lower() - if not n_["disable"].getValue() + if not n_["disable"].getValue() and + "publish_instance" not in n_.knobs() # Exclude instance nodes. ), None ) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py b/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py index 58afb2cd1f..745351dc49 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py @@ -194,7 +194,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, "frameEndHandle": last_frame, }) - # TODO temporarily set stagingDir as persistent for backward # compatibility. This is mainly focused on `renders`folders which # were previously not cleaned up (and could be used in read notes) @@ -269,10 +268,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, "tags": [] } - frame_start_str = self._get_frame_start_str(first_frame, last_frame) - - representation['frameStart'] = frame_start_str - # set slate frame collected_frames = self._add_slate_frame_to_collected_frames( instance, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index c2a989dd55..79c489d080 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -44,6 +44,14 @@ class ExtractBlendModel(BaseSettingsModel): default_factory=list, title="Families" ) + compress: bool = SettingsField(True, title="Compress") + + +class ExtractBlendAnimationModel(BaseSettingsModel): + enabled: bool = SettingsField(True) + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") + compress: bool = SettingsField(False, title="Compress") class ExtractPlayblastModel(BaseSettingsModel): @@ -51,6 +59,7 @@ class ExtractPlayblastModel(BaseSettingsModel): optional: bool = SettingsField(title="Optional") active: bool = SettingsField(title="Active") presets: str = SettingsField("", title="Presets", widget="textarea") + compress: bool = SettingsField(False, title="Compress") @validator("presets") def validate_json(cls, value): @@ -110,8 +119,8 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Extract ABC" ) - ExtractBlendAnimation: ValidatePluginModel = SettingsField( - default_factory=ValidatePluginModel, + ExtractBlendAnimation: ExtractBlendAnimationModel = SettingsField( + default_factory=ExtractBlendAnimationModel, title="Extract Blend Animation" ) ExtractAnimationFBX: ValidatePluginModel = SettingsField( @@ -198,7 +207,8 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "action", "layout", "blendScene" - ] + ], + "compress": False }, "ExtractFBX": { "enabled": False, @@ -213,7 +223,8 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "ExtractBlendAnimation": { "enabled": True, "optional": True, - "active": True + "active": True, + "compress": False }, "ExtractAnimationFBX": { "enabled": False, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index 0a8da88258..f1380eede2 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.6" +__version__ = "0.1.7" diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 1741568d63..8e0e7f7795 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -53,6 +53,9 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Latest Containers.", section="Validators") + ValidateInstanceInContextHoudini: BasicValidateModel = SettingsField( + default_factory=BasicValidateModel, + title="Validate Instance is in same Context.") ValidateMeshIsStatic: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Validate Mesh is Static.") @@ -84,6 +87,11 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateInstanceInContextHoudini": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateMeshIsStatic": { "enabled": True, "optional": True, diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 5635676f6b..b5c9b6cb71 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.11" +__version__ = "0.2.12"