# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later import bpy import shutil from .. import bkglobals, prefs, cache from pathlib import Path from typing import List, Any, Tuple, Set, cast from . import core, config from ..context import core as context_core from ..edit.core import edit_export_import_latest from .file_save import save_shot_builder_file from .template import replace_workspace_with_template from .assets import get_shot_assets from .hooks import Hooks ACTIVE_PROJECT = None def get_shots_for_seq(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]: if self.seq_id != '': seq = ACTIVE_PROJECT.get_sequence(self.seq_id) shot_enum = cache.get_shots_enum_for_seq(self, context, seq) if shot_enum != []: return shot_enum return [('NONE', "No Shots Found", '')] def get_tasks_for_shot(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]: global ACTIVE_PROJECT if not (self.shot_id == '' or self.shot_id == 'NONE'): shot = ACTIVE_PROJECT.get_shot(self.shot_id) task_enum = cache.get_shot_task_types_enum_for_shot(self, context, shot) if task_enum != []: return task_enum return [('NONE', "No Tasks Found", '')] class KITSU_OT_build_config_base_class(bpy.types.Operator): @classmethod def poll(cls, context: bpy.types.Context) -> bool: addon_prefs = prefs.addon_prefs_get(context) if not prefs.session_auth(context): cls.poll_message_set("Login to a Kitsu Server") if not cache.project_active_get(): cls.poll_message_set("Select an active project") return False if not addon_prefs.is_project_root_valid: cls.poll_message_set( "Check project root directory is configured in 'Blender Kitsu' addon preferences." ) return False return True class KITSU_OT_build_config_save_hooks(KITSU_OT_build_config_base_class): bl_idname = "kitsu.build_config_save_hooks" bl_label = "Save Shot Builder Hook File" bl_description = "Save hook.py file to `your_project_name/svn/pro/config/shot_builder` directory. Hooks are used to customize shot builder behaviour." def execute(self, context: bpy.types.Context): hooks_target_filepath = config.filepath_get(bkglobals.BUILD_HOOKS_FILENAME) if hooks_target_filepath.exists(): self.report( {'WARNING'}, f"{hooks_target_filepath.name} already exists, cannot overwrite", ) return {'CANCELLED'} hooks_example = config.example_filepath_get(bkglobals.BUILD_HOOKS_FILENAME) if not hooks_example.exists(): self.report( {'ERROR'}, f"Cannot find {hooks_target_filepath.name} example file", ) return {'CANCELLED'} config.copy_json_file(hooks_example, hooks_target_filepath) self.report({'INFO'}, f"Hook File saved to {str(hooks_target_filepath)}") return {'FINISHED'} class KITSU_OT_build_config_save_settings(KITSU_OT_build_config_base_class): bl_idname = "kitsu.build_config_save_settings" bl_label = "Save Shot Builder Settings File" bl_description = "Save settings.json file to `your_project_name/svn/pro/config/shot_builder` Config are used to customize shot builder behaviour." def execute(self, context: bpy.types.Context): settings_target_filepath = config.filepath_get(bkglobals.BUILD_SETTINGS_FILENAME) if settings_target_filepath.exists(): self.report( {'WARNING'}, f"{settings_target_filepath.name} already exists, cannot overwrite", ) return {'CANCELLED'} settings_example = config.example_filepath_get(bkglobals.BUILD_SETTINGS_FILENAME) if not settings_example.exists(): self.report( {'ERROR'}, "Cannot find example settings file", ) return {'CANCELLED'} config.copy_json_file(settings_example, settings_target_filepath) self.report({'INFO'}, f"Settings File saved to {str(settings_target_filepath)}") return {'FINISHED'} class KITSU_OT_build_config_save_templates(KITSU_OT_build_config_base_class): bl_idname = "kitsu.build_config_save_templates" bl_label = "Create Shot Builder Template Files" bl_description = ( "Save template files for shot builder in config directory." "Templates are used to customize the workspaces for each per task type" "Template names match each task type found on Kitsu Server" ) def execute(self, context: bpy.types.Context): source_dir = config.template_example_dir_get() target_dir = config.template_dir_get() # Ensure Target Directory Exists target_dir.mkdir(parents=True, exist_ok=True) source_files = list(source_dir.glob('*.blend')) + list(source_dir.glob('*.md')) for source_file in source_files: target_file = target_dir.joinpath(source_file.name) if target_file.exists(): print(f"Cannot overwrite file {str(target_file)}") continue shutil.copy2(source_file, target_dir.joinpath(source_file.name)) self.report({'INFO'}, f"Saved template files to {str(target_dir)}") return {'FINISHED'} class KITSU_OT_build_new_file_baseclass(bpy.types.Operator): bl_idname = "kitsu.build_new_file" bl_label = "Build New File" bl_description = "Build a new file based on the current context and save it" bl_options = {"REGISTER"} _kitsu_context_type = "" # Default context for this operator _current_kitsu_context = "" production_name: bpy.props.StringProperty( # type: ignore name="Production", description="Name of the production to create a shot file for", options=set(), ) save_file: bpy.props.BoolProperty( # type:ignore name="Save after building.", description="Automatically save build file after 'Shot Builder' is complete.", default=True, ) def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: global ACTIVE_PROJECT # Temporarily change kitsu context to asset self._current_kitsu_context = context.scene.kitsu.category context.scene.kitsu.category = self._kitsu_context_type addon_prefs = prefs.addon_prefs_get(bpy.context) project = cache.project_active_get() ACTIVE_PROJECT = project if addon_prefs.session.is_auth() is False: self.report( {'ERROR'}, "Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.", ) return {'CANCELLED'} if project.id == "": self.report( {'ERROR'}, "Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.", ) return {'CANCELLED'} if not addon_prefs.is_project_root_valid: self.report( {'ERROR'}, "Operator is not able to determine the project root directory. \nCheck project root directory is configured in 'Blender Kitsu' addon preferences.", ) return {'CANCELLED'} self.production_name = project.name return cast(Set[str], context.window_manager.invoke_props_dialog(self, width=400)) def cancel(self, context: bpy.types.Context): # Restore kitsu context if cancelled context.scene.kitsu.category = self._current_kitsu_context class KITSU_OT_build_new_asset(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.build_new_asset" bl_label = "Build New Asset" bl_description = "Build a New Asset file, based on project data from Kitsu Server" bl_options = {"REGISTER"} _kitsu_context_type = "ASSET" # Default context for this operator def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout layout.use_property_split = True layout.use_property_decorate = False flow = layout.grid_flow( row_major=True, columns=0, even_columns=True, even_rows=False, align=False ) col = flow.column() row = col.row() row.enabled = False row.prop(self, "production_name") context_core.draw_asset_type_selector(context, col) context_core.draw_asset_selector(context, col) col.prop(self, "save_file") def execute(self, context: bpy.types.Context): # Get Properties scene = context.scene asset_type = cache.asset_type_active_get() asset = cache.asset_active_get() if asset_type.id == "" or asset.id == "": self.report({'ERROR'}, "Please select a asset type and asset to build a shot file") return {'CANCELLED'} asset_file_path_str = asset.get_filepath(context) replace_workspace_with_template(context, "Asset") # Remove All Collections from Scene for collection in context.scene.collection.children: context.scene.collection.children.unlink(collection) bpy.data.collections.remove(collection) # Remove All Objects from Scene for object in context.scene.objects: context.scene.objects.unlink(object) bpy.data.objects.remove(object) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) asset_collection = bpy.data.collections.new(asset.get_collection_name()) context.scene.collection.children.link(asset_collection) # Set Kitsu Context scene.kitsu.category = "ASSET" scene.kitsu.asset_type_active_name = asset_type.name scene.kitsu.asset_active_name = asset.name scene.kitsu.asset_col = asset_collection relative_path = Path(asset_file_path_str).relative_to(prefs.project_root_dir_get(context)) asset.set_asset_path(str(relative_path), asset_collection.name) # Save File if self.save_file: if not save_shot_builder_file(file_path=asset_file_path_str): self.report( {"WARNING"}, f"Failed to save file at path `{asset_file_path_str}`", ) return {"FINISHED"} self.report({"INFO"}, f"Successfully Built Shot:`{asset.name}`") return {"FINISHED"} class KITSU_OT_open_asset_file(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.open_asset_file" bl_label = "Open Asset File" bl_description = "Open an Asset File from the current project" _kitsu_context_type = "ASSET" save_current: bpy.props.BoolProperty( # type: ignore name="Save Current File", description="Automatically save the current file before opening a new one.", default=True, ) def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout layout.use_property_split = True layout.use_property_decorate = False flow = layout.grid_flow( row_major=True, columns=0, even_columns=True, even_rows=False, align=False ) col = flow.column() row = col.row() row.enabled = False row.prop(self, "production_name") context_core.draw_asset_type_selector(context, col) context_core.draw_asset_selector(context, col) if bpy.data.is_dirty: col.prop(self, "save_current") def execute(self, context: bpy.types.Context): asset = cache.asset_active_get() asset_file_path_str = asset.get_filepath(context) if not Path(asset_file_path_str).exists(): self.report({'ERROR'}, f"Asset file does not exist: {asset_file_path_str}") return {'CANCELLED'} if bpy.data.is_dirty and self.save_current: bpy.ops.wm.save_mainfile() bpy.ops.wm.open_mainfile(filepath=asset_file_path_str) return {'FINISHED'} class KITSU_OT_build_new_shot(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.build_new_shot" bl_label = "Build New Shot" bl_description = "Build a New Shot file, based on project information found on Kitsu Server" bl_options = {"REGISTER"} _kitsu_context_type = "SHOT" def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout layout.use_property_split = True layout.use_property_decorate = False flow = layout.grid_flow( row_major=True, columns=0, even_columns=True, even_rows=False, align=False ) col = flow.column() row = col.row() row.enabled = False row.prop(self, "production_name") if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT: context_core.draw_episode_selector(context, col) context_core.draw_sequence_selector(context, col) context_core.draw_shot_selector(context, col) context_core.draw_task_type_selector(context, col) col.prop(self, "save_file") def _get_task_type_for_shot(self, context, shot): for task_type in shot.get_all_task_types(): if task_type.id == self.task_type: return task_type def _frame_range_invalid(self, context, shot) -> bool: if not (getattr(shot, "data") or getattr(shot, "nb_frames")): return True try: shot.data.get("frame_in") except AttributeError: return True def execute(self, context: bpy.types.Context): # Get Properties active_project = cache.project_active_get() seq = cache.sequence_active_get() shot = cache.shot_active_get() task_type = cache.task_type_active_get() config.settings_load() if seq.id == "" or shot.id == "" or task_type.id == "": self.report( {'ERROR'}, "Please select a sequence, shot and task type to build a shot file" ) return {'CANCELLED'} if self._frame_range_invalid(context, shot): self.report( {'WARNING'}, F"Shot {shot.name} is missing frame range data on Kitsu Server" ) task_type_short_name = task_type.get_short_name() shot_file_path_str = shot.get_filepath(context, task_type_short_name) # Open Template File replace_workspace_with_template(context, task_type.name) # Set Up Scene + Naming shot_task_name = shot.get_task_name(task_type.get_short_name()) scene = core.set_shot_scene(context, shot_task_name) core.remove_all_data() core.set_resolution_and_fps(active_project, scene) core.set_frame_range(shot, scene) # Set Render Settings if task_type_short_name == 'anim': # TODO get anim from a constant instead core.set_render_engine(context.scene, 'BLENDER_WORKBENCH') else: core.set_render_engine(context.scene) # Create Output Collection & Link Camera if config.OUTPUT_COL_CREATE.get(task_type_short_name): output_col = core.create_task_type_output_collection(context.scene, shot, task_type) if task_type_short_name == 'anim' or task_type_short_name == 'layout': core.link_camera_rig(context.scene, output_col) # Load Assets get_shot_assets(scene=scene, output_collection=output_col, shot=shot) # Link External Output Collections core.link_task_type_output_collections(shot, task_type) if config.LOAD_EDITORIAL_REF.get(task_type_short_name): edit_export_import_latest(context, shot) # Run Hooks hooks_instance = Hooks() hooks_instance.load_hooks(context) hooks_instance.execute_hooks( match_task_type=task_type_short_name, scene=context.scene, shot=shot, prod_path=prefs.project_root_dir_get(context), shot_path=shot_file_path_str, ) # Set Kitsu Context scene.kitsu.category = "SHOT" scene.kitsu.sequence_active_name = seq.name scene.kitsu.shot_active_name = shot.name scene.kitsu.task_type_active_name = task_type.name # Save File if self.save_file: if not save_shot_builder_file(file_path=shot_file_path_str): self.report( {"WARNING"}, f"Failed to save file at path `{shot_file_path_str}`", ) return {"FINISHED"} self.report({"INFO"}, f"Successfully Built Shot:`{shot.name}` Task: `{task_type.name}`") return {"FINISHED"} class KITSU_OT_open_shot_file(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.open_shot_file" bl_label = "Open Shot File" bl_description = "Open a Shot File from the current project" _kitsu_context_type = "SHOT" save_current: bpy.props.BoolProperty( # type: ignore name="Save Current File", description="Automatically save the current file before opening a new one.", default=True, ) def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout layout.use_property_split = True layout.use_property_decorate = False flow = layout.grid_flow( row_major=True, columns=0, even_columns=True, even_rows=False, align=False ) col = flow.column() row = col.row() row.enabled = False row.prop(self, "production_name") if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT: context_core.draw_episode_selector(context, col) context_core.draw_sequence_selector(context, col) context_core.draw_shot_selector(context, col) context_core.draw_task_type_selector(context, col) if bpy.data.is_dirty: col.prop(self, "save_current") def execute(self, context: bpy.types.Context): shot = cache.shot_active_get() task_type = cache.task_type_active_get() task_type_short_name = task_type.get_short_name() shot_file_path_str = shot.get_filepath(context, task_type_short_name) if not Path(shot_file_path_str).exists(): self.report({'ERROR'}, f"Shot file does not exist: {shot_file_path_str}") return {'CANCELLED'} if bpy.data.is_dirty and self.save_current: bpy.ops.wm.save_mainfile() bpy.ops.wm.open_mainfile(filepath=shot_file_path_str) return {'FINISHED'} class KITSU_OT_create_edit_file(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.create_edit_file" bl_label = "Create Edit File" bl_description = "Create a new .blend file for editing using Blender's Video Editing template" _edit_entity = None _production_name = None _kitsu_context_type = "EDIT" create_kitsu_edit: bpy.props.BoolProperty( # type: ignore name="Create Kitsu Edit if none exists.", description="Automatically create a Kitsu edit for the edit.", default=True, ) def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT: context_core.draw_episode_selector(context, layout) layout.prop(self, "create_kitsu_edit") layout.prop(self, "save_file") def execute(self, context): scene = context.scene active_project = cache.project_active_get() self._edit_entity = cache.edit_default_get( create=self.create_kitsu_edit, episode_id=context.scene.kitsu.episode_active_id ) self._edit_entity.set_edit_task() task_type = self._edit_entity.get_task_type() if not self._edit_entity: self.report({'ERROR'}, "Failed to create Kitsu edit entity.") return {'CANCELLED'} edit_file_path_str = self._edit_entity.get_filepath(context) # Create a new file using the Video Editing template replace_workspace_with_template(context, task_type.name) core.set_resolution_and_fps(active_project, scene) cache.reset_all_edits_enum_for_active_project() scene.kitsu.category = "EDIT" scene.kitsu.edit_active_name = context_core.get_versioned_file_basename( Path(edit_file_path_str).stem ) scene.kitsu.task_type_active_name = bkglobals.EDIT_TASK_TYPE # Save File if self.save_file: if not save_shot_builder_file(file_path=edit_file_path_str): self.report( {"WARNING"}, f"Failed to save file at path `{edit_file_path_str}`", ) return {"FINISHED"} self.report({'INFO'}, f"Created edit file at {edit_file_path_str}") return {'FINISHED'} class KITSU_OT_open_edit_file(KITSU_OT_build_new_file_baseclass): bl_idname = "kitsu.open_edit_file" bl_label = "Open Edit File" bl_description = "Open Latest Edit File from the current project" _kitsu_context_type = "EDIT" save_current: bpy.props.BoolProperty( # type: ignore name="Save Current File", description="Automatically save the current file before opening a new one.", default=True, ) def draw(self, context: bpy.types.Context) -> None: global ACTIVE_PROJECT layout = self.layout layout.use_property_split = True layout.use_property_decorate = False flow = layout.grid_flow( row_major=True, columns=0, even_columns=True, even_rows=False, align=False ) col = flow.column() row = col.row() row.enabled = False row.prop(self, "production_name") if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT: context_core.draw_episode_selector(context, col) context_core.draw_edit_selector(context, col) if bpy.data.is_dirty: col.prop(self, "save_current") def execute(self, context: bpy.types.Context): edit_entity = cache.edit_default_get(episode_id=context.scene.kitsu.episode_active_id) if not edit_entity: self.report({'ERROR'}, "No edit task found on Kitsu Server.") return {'CANCELLED'} edit_file_path_str = edit_entity.get_filepath(context) if not Path(edit_file_path_str).exists(): self.report({'ERROR'}, f"Edit file does not exist: {edit_file_path_str}") return {'CANCELLED'} if bpy.data.is_dirty and self.save_current: bpy.ops.wm.save_mainfile() bpy.ops.wm.open_mainfile(filepath=edit_file_path_str) return {'FINISHED'} classes = [ KITSU_OT_build_new_shot, KITSU_OT_build_new_asset, KITSU_OT_build_config_save_hooks, KITSU_OT_build_config_save_settings, KITSU_OT_build_config_save_templates, KITSU_OT_create_edit_file, KITSU_OT_open_edit_file, KITSU_OT_open_shot_file, KITSU_OT_open_asset_file, ] def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)