# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later from typing import Dict, Optional, Set from pathlib import Path import bpy from .. import bkglobals, cache, util, prefs from ..logger import LoggerFactory from ..types import TaskType, AssetType from ..context import core as context_core logger = LoggerFactory.getLogger() class KITSU_OT_con_productions_load(bpy.types.Operator): """ Gets all productions that are available in server and let's user select. Invokes a search Popup (enum_prop) on click. """ bl_idname = "kitsu.con_productions_load" bl_label = "Productions Load" bl_property = "enum_prop" bl_description = "Sets active project" enum_prop: bpy.props.EnumProperty(items=cache.get_projects_enum_list) # type: ignore @classmethod def poll(cls, context: bpy.types.Context) -> bool: return prefs.session_auth(context) def execute(self, context: bpy.types.Context) -> Set[str]: # Store vars to check if project / seq / shot changed. project_prev_id = cache.project_active_get().id # Update kitsu metadata. cache.project_active_set_by_id(context, self.enum_prop) # Clear active shot when sequence changes. if self.enum_prop != project_prev_id: cache.sequence_active_reset(context) cache.episode_active_reset(context) cache.asset_type_active_reset(context) cache.shot_active_reset(context) cache.asset_active_reset(context) util.ui_redraw() return {"FINISHED"} def invoke(self, context, event): context.window_manager.invoke_search_popup(self) return {"FINISHED"} class KITSU_OT_con_detect_context(bpy.types.Operator): bl_idname = "kitsu.con_detect_context" bl_label = "Detect Context" bl_description = "Auto detects context by looking at file path" @classmethod def poll(cls, context: bpy.types.Context) -> bool: return bool( prefs.session_auth(context) and cache.project_active_get() and bpy.data.filepath ) def execute(self, context: bpy.types.Context) -> Set[str]: # Update kitsu metadata. filepath = Path(bpy.data.filepath) active_project = cache.project_active_get() kitsu_props = context.scene.kitsu addon_prefs = context.preferences.addons['.'.join(__package__.split('.')[:-1])].preferences is_edit_type = str(filepath.resolve()).startswith(str(prefs.project_root_dir_get(context) / addon_prefs.edit_dir_name)) if is_edit_type: kitsu_props.category = "EDIT" if active_project.production_type == bkglobals.KITSU_TV_PROJECT: episode = active_project.get_episode_by_name(filepath.parents[0].name) if episode: kitsu_props.episode_active_name = episode.name kitsu_props.edit_active_name = context_core.get_versioned_file_basename(filepath.stem) kitsu_props.task_type_active_name = bkglobals.EDIT_TASK_TYPE util.ui_redraw() return {"FINISHED"} # TODO REFACTOR THIS WHOLE THING, BAD HACK # Path is different for tvshow if ( active_project.production_type == bkglobals.KITSU_TV_PROJECT and filepath.parents[3].name == addon_prefs.shot_dir_name ): episode = active_project.get_episode_by_name(filepath.parents[2].name) category = filepath.parents[3].name else: episode = None category = filepath.parents[2].name item_group = filepath.parents[1].name item = filepath.parents[0].name item_task_type = filepath.stem.split(bkglobals.DELIMITER)[-1] # Sanity check that the folder struture is correct depending on the type. is_shot_type = category == addon_prefs.shot_dir_name is_seq_type = category == addon_prefs.seq_dir_name is_asset_type = category == addon_prefs.asset_dir_name if not is_shot_type and not is_seq_type and not is_asset_type: self.report( {"ERROR"}, ( f"Expected '{addon_prefs.shot_dir_name}' or '{addon_prefs.asset_dir_name}' 3 folders up. " f"Got: '{filepath.parents[2].as_posix()}' instead. " "Blend file might not be saved in project structure" ), ) return {"CANCELLED"} if is_shot_type or is_seq_type: # TODO: check if frame range update gets triggered. # Set category. if is_shot_type: kitsu_props.category = "SHOT" task_mapping = bkglobals.SHOT_TASK_MAPPING else: kitsu_props.category = "SEQ" task_mapping = bkglobals.SEQ_TASK_MAPPING if episode: kitsu_props.episode_active_name = episode.name # Detect and load sequence. sequence = active_project.get_sequence_by_name(item_group, episode) if not sequence: self.report({"ERROR"}, f"Failed to find sequence: '{item_group}' on server") return {"CANCELLED"} kitsu_props.sequence_active_name = sequence.name if is_shot_type: # Detect and load shot. shot = active_project.get_shot_by_name(sequence, item) if not shot: self.report({"ERROR"}, f"Failed to find shot: '{item}' on server") return {"CANCELLED"} kitsu_props.shot_active_name = shot.name # Detect and load shot task type. kitsu_task_type_name = self._find_in_mapping( item_task_type, task_mapping, "shot task type" ) if not kitsu_task_type_name: return {"CANCELLED"} task_type = TaskType.by_name(kitsu_task_type_name) if not task_type: self.report( {"ERROR"}, f"Failed to find task type: '{kitsu_task_type_name}' on server", ) return {"CANCELLED"} kitsu_props.task_type_active_name = task_type.name elif is_asset_type: # Set category. kitsu_props.category = "ASSET" # Detect and load asset type. kitsu_asset_type_name = self._find_in_mapping( item_group, bkglobals.ASSET_TYPE_MAPPING, "asset type" ) if not kitsu_asset_type_name: return {"CANCELLED"} asset_type = AssetType.by_name(kitsu_asset_type_name) if not asset_type: self.report( {"ERROR"}, f"Failed to find asset type: '{kitsu_asset_type_name}' on server", ) return {"CANCELLED"} kitsu_props.asset_type_active_name = asset_type.name # Detect and load asset. asset = active_project.get_asset_by_name(item) if not asset: self.report({"ERROR"}, f"Failed to find asset: '{item}' on server") return {"CANCELLED"} kitsu_props.asset_active_name = asset.name # If split == 1 then filepath has no task type in name, skip asset task_type if len(filepath.stem.split(bkglobals.DELIMITER)) > 1: # Detect and load asset task_type. kitsu_task_type_name = self._find_in_mapping( item_task_type, bkglobals.ASSET_TASK_MAPPING, "task type" ) if not kitsu_task_type_name: return {"CANCELLED"} task_type = TaskType.by_name(kitsu_task_type_name) if not task_type: self.report( {"ERROR"}, f"Failed to find task type: '{kitsu_task_type_name}' on server", ) return {"CANCELLED"} kitsu_props.task_type_active_name = task_type.name util.ui_redraw() self.report({"INFO"}, f"Context Successfully Set!") return {"FINISHED"} def _find_in_mapping( self, key: str, mapping: Dict[str, str], entity_type: str ) -> Optional[str]: if not key in mapping: self.report( {"ERROR"}, f"Failed to find {entity_type}: '{key}' in {entity_type} remapping", ) return None return mapping[key] class KITSU_OT_con_set_asset(bpy.types.Operator): bl_idname = "kitsu.con_set_asset" bl_label = "Set Kitsu Asset" bl_description = ( "Mark the current file & target collection as an Asset on Kitsu Server " "Assets marked with this method will be automatically loaded by the " "Shot Builder, if the Asset is casted to the buider's target shot" ) _published_file_path: Path = None use_asset_pipeline_publish: bpy.props.BoolProperty( # type: ignore name="Use Asset Pipeline Publish", description=( "Find the Publish of this file in the 'Publish' folder and use it's filepath for Kitsu Asset`" "Selected Collection must be named exactly the same between current file and Publish" ), default=False, ) @classmethod def poll(cls, context): kitsu_props = context.scene.kitsu if bpy.data.filepath == "": cls.poll_message_set("Blend file must be saved") return False if not bpy.data.filepath.startswith(str(prefs.project_root_dir_get(context))): cls.poll_message_set("Blend file must be saved in project structure") return False if not context_core.is_asset_context(): cls.poll_message_set("Kitsu Context panel must be set to 'Asset'") return False if kitsu_props.asset_type_active_name == "": cls.poll_message_set("Asset Type must be set") return False if kitsu_props.asset_active_name == "": cls.poll_message_set("Asset must be set") return False if not kitsu_props.asset_col: cls.poll_message_set("Asset Collection must be set") return False return True def is_asset_pipeline_enabled(self, context) -> bool: for addon in context.preferences.addons: if addon.module == "asset_pipeline": return True return False def is_asset_pipeline_folder(self, context) -> bool: current_folder = Path(bpy.data.filepath).parent return current_folder.joinpath("task_layers.json").exists() def get_asset_pipeline_publish(self, context) -> Path: from asset_pipeline.merge.publish import find_latest_publish return find_latest_publish(Path(bpy.data.filepath)) def invoke(self, context, event): if self.is_asset_pipeline_enabled(context) and self.is_asset_pipeline_folder(context): self._published_file_path = self.get_asset_pipeline_publish(context) if self._published_file_path.exists(): self.use_asset_pipeline_publish = True wm = context.window_manager return wm.invoke_props_dialog(self) return self.execute(context) def draw(self, context): layout = self.layout relative_path = self._published_file_path.relative_to(Path(bpy.data.filepath).parent) box = layout.box() box.enabled = self.use_asset_pipeline_publish box.label(text=f"//{str(relative_path)}") layout.prop(self, "use_asset_pipeline_publish") def execute(self, context): project_root = prefs.project_root_dir_get(context) if self.use_asset_pipeline_publish: relative_path = self._published_file_path.relative_to(project_root) else: relative_path = Path(bpy.data.filepath).relative_to(project_root) blender_asset = context.scene.kitsu.asset_col kitsu_asset = cache.asset_active_get() if not kitsu_asset: self.report({"ERROR"}, "Failed to find active Kitsu Asset") return {"CANCELLED"} kitsu_asset.set_asset_path(str(relative_path), blender_asset.name) self.report( {"INFO"}, f"Kitsu Asset '{kitsu_asset.name}' set to Collection '{blender_asset.name}' at path '{relative_path}'", ) return {"FINISHED"} # ---------REGISTER ----------. classes = [ KITSU_OT_con_productions_load, KITSU_OT_con_detect_context, KITSU_OT_con_set_asset, ] def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)