diff --git a/scripts-blender/addons/blender_kitsu/.gitattributes b/scripts-blender/addons/blender_kitsu/.gitattributes new file mode 100644 index 00000000..80a1944b --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/.gitattributes @@ -0,0 +1 @@ +*.blend filter=lfs diff=lfs merge=lfs -text diff --git a/scripts-blender/addons/blender_kitsu/.gitignore b/scripts-blender/addons/blender_kitsu/.gitignore new file mode 100644 index 00000000..2621fabc --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/.gitignore @@ -0,0 +1 @@ +*.blend1 \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/README.md b/scripts-blender/addons/blender_kitsu/README.md index 72cb9827..249faba2 100644 --- a/scripts-blender/addons/blender_kitsu/README.md +++ b/scripts-blender/addons/blender_kitsu/README.md @@ -7,28 +7,45 @@ blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It - [Table of Contents](#table-of-contents) - [Installation](#installation) - [How to get started](#how-to-get-started) + - [**Setup Login Data**](#setup-login-data) + - [**Setup Project Settings**](#setup-project-settings) + - [**Setup Animation Tools**](#setup-animation-tools) + - [**Setup Lookdev Tools**](#setup-lookdev-tools) + - [**Setup Media Search Paths**](#setup-media-search-paths) + - [**Setup Miscellaneous**](#setup-miscellaneous) - [Features](#features) - - [Sequence Editor](#sequence-editor) - - [Metadata](#metadata) - - [Push](#push) - - [Pull](#pull) - - [Multi Edit](#multi-edit) - - [Shot as Image Sequence](#shot-as-image-sequence) - - [General Sequence Editor Tools](#general-sequence-editor-tools) - - [Context](#context) - - [Animation Tools](#animation-tools) - - [Lookdev Tools](#lookdev-tools) - - [Error System](#error-system) + - [Sequence Editor](#sequence-editor) + - [Metastrips](#metastrips) + - [Create a Metastrip](#create-a-metastrip) + - [Initialize a Shot](#initialize-a-shot) + - [Metadata](#metadata) + - [Push](#push) + - [Pull](#pull) + - [Multi Edit](#multi-edit) + - [Shot as Image Sequence](#shot-as-image-sequence) + - [Advanced Settings](#advanced-settings) + - [General Sequence Editor Tools](#general-sequence-editor-tools) + - [Context](#context) + - [Animation Tools](#animation-tools) + - [Lookdev Tools](#lookdev-tools) + - [Error System](#error-system) - [Shot Builder](#shot-builder) + - [Features](#features) + - [Getting Started](#getting-started) + - [Shot Setup](#shot-setup) + - [Asset Setup](#asset-setup) + - [Kitsu Server](#kitsu-server) + - [Asset Index](#asset-index) + - [Example asset_index.json](#example-asset_indexjson) + - [Hooks Setup](#hooks-setup) + - [Editorial Exports](#editorial-exports) + - [Run Shot Builder](#run-shot-builder) - [Development](#development) - [Update Dependencies](#update-dependencies) - [Troubleshoot](#troubleshoot) - [Credits](#credits) -blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It also has features that are not directly related to Kitsu but support certain aspects of the Blender Studio Pipeline. - -[Blender-Kitsu blogpost](https://studio.blender.org/blog/kitsu-addon-for-blender/) ## Table of Contents @@ -274,250 +291,83 @@ blender-kitsu has different checks that are performed during file load or during ![image info](/media/addons/blender_kitsu/error_animation.jpg) ## Shot Builder -Shot Builder is an Add-on that helps studios to work with task specific -Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are +Shot Builder is a Feature of the Blender Kitsu Add-on To automatically build shot files, using the data from Kitsu server and the file structures defined on the [Blender Studio](https://studio.blender.org/pipeline/naming-conventions/svn-folder-structure) website. -* Build blend files for a specific task and shot. -* Sync data back from work files to places like kitsu, or `edit.blend`. +### Features + - Saves a 'Shot' File for each of the Shot's Task Types on Kitsu Server. + - Automatically Names Scenes based on Shot and Task Type names + - Creates output collections for certain Task Types (anim, fx, layout, lighting, previz, storyboard) + - Links output collections between Task Types based on [Shot Assembly](https://studio.blender.org/pipeline/pipeline-overview/shot-production/shot-assembly) specifications + - Loads Editorial Export (defined in preferences) into Shot file's VSE area (if available) + - Loads Assets via `asset_index.json` file stored at `your_production/svn/pro/assets/asset_index.json` + - Executes a `hook.py` file stored at `your_production/svn/pro/assets/shot-builder/hooks.py` -### Design Principles +### Getting Started +#### Shot Setup +The Shot Builder requires shot data including Name, Frame Rate, and Duration to be stored on a Kitsu Server. Please follow the [Sequence Editor](#sequence-editor) guide to create metastrips and Push that data to the Kitsu server or follow the [Kitsu First Production](https://kitsu.cg-wire.com/first_production/) to manually enter this data into the Kitsu Server. -The main design principles are: +#### Asset Setup +##### Kitsu Server +The Shot Builder requires all Asset to be stored on the Kitsu Server with a [Metadata Column](https://kitsu.cg-wire.com/production_advanced/#create-custom-metadata-columns) with the exact name `slug` exactly matching the name of the asset's collection. -* The core-tool can be installed as an add-on, but the (production specific) - configuration should be part of the production repository. -* The configuration files are a collection of python files. The API between - the configuration files and the add-on should be easy to use as pipeline - TDs working on the production should be able to work with it. -* TDs/artists should be able to handle issues during building without looking - at how the add-on is structured. -* The tool contains connectors that can be configured to read/write data - from the system/file that is the main location of the data. For example - The start and end time of a shot could be stored in an external production tracking application. +Assets needs to be associated with each shot in your production. Please follow the [Kitsu Breakdown](https://kitsu.cg-wire.com/getting-started-production/) guide to Cast your assets to shots. -### Connectors +##### Asset Index +To match Assets File to the casting breakdown on the Kitsu server, we need to create an Asset Index. This is a json file that contains the mapping of the asset's name to the asset's filepath. Any collection Marked as an Asset in Blender in the directory `your_project/svn/pro/assets` will be added to this index. -Connectors are components that can be used to read or write to files or -systems. The connectors will add flexibility to the add-on so it could be used -in multiple productions or studios. - -In the configuration files the TD can setup the connectors that are used for -the production. Possible connectors would be: - -* Connector for text based config files (json/yaml). -* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html). -* Connector for blend files. - -### Layering & Hooks - -The configuration of the tool is layered. When building a work file for a sequence -there are multiple ways to change the configuration. - -* Configuration for the production. -* Configuration for the asset that is needed. -* Configuration for the asset type of the loaded asset. -* Configuration for the sequence. -* Configuration for the shot. -* Configuration for the task type. - -For any combination of these configurations hooks can be defined. - -``` -@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A') -def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides when Spring is loaded in 02_020A. - """ - -@shot_tools.hook(match_task_type='anim') -def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides for any animation task. - """ -``` - -#### Data - -All hooks must have Python’s `**kwargs` parameter. The `kwargs` contains -the context at the moment the hook is invoked. The context can contain the -following items. - -* `production`: `shot_tools.Production`: Include the name of the production - and the location on the filesystem. -* `task`: `shot_tools.Task`: The task (combination of task_type and shot) -* `task_type`: `shot_tools.TaskType`: Is part of the `task`. -* `sequence`: `shot_tools.Sequence`: Is part of `shot`. -* `shot`: `shot_tools.Shot` Is part of `task`. -* `asset`: `shot_tools.Asset`: Only available during asset loading phase. -* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase. - -#### Execution Order - -The add-on will internally create a list containing the hooks that needs to be -executed for the command in a sensible order. It will then execute them in that -order. - -By default the next order will be used: - -* Production wide hooks -* Asset Type hooks -* Asset hooks -* Sequence hooks -* Shot hooks -* Task type hooks - -A hook with a single ‘match’ rule will be run in the corresponding phase. A hook with -multiple ‘match’ rules will be run in the last matching phase. For example, a hook with -‘asset’ and ‘task type’ match rules will be run in the ‘task type’ phase. - -###### Events - -Order of execution can be customized by adding the optional `run_before` -or `run_after` parameters. - -``` -@shot_tools.hook(match_task_type='anim', - requires={shot_tools.events.AssetsLoaded, hook_task_other_anim}, - is_required_by={shot_tools.events.ShotOverrides}) -def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides for any animation task run after all assets have been loaded. - """ -``` - -Events could be: - -* `shot_tools.events.BuildStart` -* `shot_tools.events.ProductionSettingsLoaded` -* `shot_tools.events.AssetsLoaded` -* `shot_tools.events.AssetTypeOverrides` -* `shot_tools.events.SequenceOverrides` -* `shot_tools.events.ShotOverrides` -* `shot_tools.events.TaskTypeOverrides` -* `shot_tools.events.BuildFinished` -* `shot_tools.events.HookStart` -* `shot_tools.events.HookEnd` - -During usage we should see which one of these or other events are needed. - -`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded` -and `shot_tools.events.HookStart` can only be used in the `run_after` -parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished` -can only be used in the `run_before` parameter. - - -### API - -The shot builder has an API between the add-on and the configuration files. This -API contains convenience functions and classes to hide complexity and makes -sure that the configuration files are easy to maintain. - -``` -register_task_type(task_type="anim") -register_task_type(task_type="lighting") -``` - -``` -# shot_tool/characters.py -class Asset(shot_tool.some_module.Asset): - asset_file = "/{asset_type}/{name}/{name}.blend" - collection = “{class_name}” - name = “{class_name}” - -class Character(Asset): - asset_type = ‘char’ - - -class Ellie(Character): - collection = “{class_name}-{variant_name}” - variants = {‘default’, ‘short_hair’} - -class Victoria(Character): pass -class Rex(Character): pass - -# shot_tool/shots.py -class Shot_01_020_A(shot_tool.some_module.Shot): - shot_id = ‘01_020_A’ - assets = { - characters.Ellie(variant=”short_hair”), - characters.Rex, - sets.LogOverChasm, +##### Example `asset_index.json` +```json +{ + "CH-rain": { + "type": "Collection", + "filepath": "your_project/svn/pro/assets/chars/rain/rain.blend" + }, + "CH-snow": { + "type": "Collection", + "filepath": "your_project/svn/pro/assets/chars/snow/snow.blend" } - -class AllHumansShot(shot_tool.some_module.Shot): - assets = { - characters.Ellie(variant=”short_hair”), - characters.Rex, - characters.Victoria, - } - -class Shot_01_035_A(AllHumansShot): - assets = { - sets.Camp, - } - +} ``` -This API is structured/implemented in a way that it keeps track of what -is being done. This will be used when an error occurs so a descriptive -error message can be generated that would help the TD to solve the issue more -quickly. The goal would be that the error messages are descriptive enough to -direct the TD into the direction where the actual cause is. And when possible -propose several solutions to fix it. +To create/update the Asset Index: +1. Enter Asset Index directory `cd blender-studio-pipeline/scripts/index_assets` +2. Run using `./run_index_assets.py your_poduction` replace `your_production` with the path to your project's root directory +3. This will create an index file at `your_production/svn/pro/assets/asset_index.py` -### Setting up the tool +#### Hooks Setup +Shot Builder uses hooks to extend the functionality of the shot builder. To create a hook file +1. Open `Edit>Preferences>Add-Ons` +2. Search for the `Blender Kitsu` Add-On +3. In the `Blender Kitsu` Add-On preferences find the `Shot Builder` section +4. Run the Operator `Save Shot Builder Hook File` +5. Edit the file `your_project/svn/pro/assets/scripts/shot-builder/hooks.py` to customize your hooks. -The artist/TD can configure their current local project directory in the add-on preferences. -This can then be used for new blend files. The project associated with an opened (so existing) -blend file can be found automatically by iterating over parent directories until a Shot Builder -configuration file is found. Project-specific settings are not configured/stored in the add-on, -but in this configuration file. - -The add-on will look in the root of the production repository to locate the -main configuration file `/project_root_directory/pro/shot-builder/config.py`. This file contains general -settings about the production, including: - -* The name of the production for reporting back to the user when needed. -* Naming standards to test against when reporting deviations. -* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production. -* Configuration of the needed connectors. - -#### Directory Layout -``` bash -└── project-name/ # Project Root Directory - └── pro/ - ├── assets/ - ├── shot-builder/ - │ ├── assets.py - │ ├── config.py - │ ├── hooks.py - │ └── shots.py - └── shots/ ``` +Arguments to use in hooks + scene: bpy.types.Scene # current scene + shot: Shot class from blender_kitsu.types.py + prod_path: str # path to production root dir (your_project/svn/) + shot_path: str # path to shot file (your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_task_name}.blend) + +Notes + matching_task_type = ['anim', 'lighting', 'fx', 'comp'] # either use list or just one string + output_col_name = shot.get_output_collection_name(task_type_short_name="anim") -### Usage +``` +#### Editorial Exports +Shot Builder can load Exports from Editorial to the .blend's VSE for reference. -Any artist can open a shot file via the `File` menu. A modal panel appears -where the user can select the task type and sequence/shot. When the file -already exists, it will be opened. When the file doesn't exist, the file -will be built. +1. Open `Edit>Preferences>Add-Ons` +2. Search for the `Blender Kitsu` Add-On +3. In the `Blender Kitsu` Add-On preferences find the `Shot Builder` section +4. Set your `Editorial Export Directory` to `your_project/shared/editorial/export/` +5. Set your `Editorial File Pattern` to `your_project_v\d\d\d.mp4` where `\d` represents a digit. This pattern matches a file named `your_movie_v001.mp4`. -In the future other use cases will also be accessible, such as: - -* Syncing data back from a work file to the source of the data. -* Report of errors/differences between the shot file and the configuration. - -### Open Issues - -#### Security - -* Security keys needed by connectors need to be stored somewhere. The easy - place is to place inside the production repository, but that isn't secure - Anyone with access to the repository could misuse the keys to access the - connector. Other solution might be to use the OS key store or retrieve the - keys from an online service authenticated by the blender cloud add-on. - - We could use `keyring` to access OS key stores. +#### Run Shot Builder +1. Open Blender +2. Select File>New +3. From dialogue box, select the desired Sequence/Shot/Task Type +4. Hit `ok` to run the tool. The tool will create a new file in the directory `your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_name}+{task_type_name}.blend` ## Development ### Update Dependencies diff --git a/scripts-blender/addons/blender_kitsu/__init__.py b/scripts-blender/addons/blender_kitsu/__init__.py index 261e504b..7f01af7d 100644 --- a/scripts-blender/addons/blender_kitsu/__init__.py +++ b/scripts-blender/addons/blender_kitsu/__init__.py @@ -116,7 +116,6 @@ def unregister(): lookdev.unregister() playblast.unregister() shot_builder.unregister() - LoggerLevelManager.restore_levels() diff --git a/scripts-blender/addons/blender_kitsu/bkglobals.py b/scripts-blender/addons/blender_kitsu/bkglobals.py index 3bc93677..28a740b4 100644 --- a/scripts-blender/addons/blender_kitsu/bkglobals.py +++ b/scripts-blender/addons/blender_kitsu/bkglobals.py @@ -29,7 +29,7 @@ SHOT_DIR_NAME = "shots" SEQ_DIR_NAME = "sequences" ASSET_DIR_NAME = "assets" -FILE_DELIMITER = '-' +FILE_DELIMITER = "-" ASSET_TASK_MAPPING = { "geometry": "Geometry", @@ -90,3 +90,54 @@ RES_DIR_PATH = Path(os.path.abspath(__file__)).parent.joinpath("res") SCENE_NAME_PLAYBLAST = "playblast_playback" PLAYBLAST_DEFAULT_STATUS = "Todo" + +########################### +# Shot Builder Properties +########################### + +# TODO add documentation and move other shot builder props here + +OUTPUT_COL_CREATE = { + "anim": True, + "comp": False, + "fx": True, + "layout": True, + "lighting": True, + "previz": True, + "rendering": False, + "smear_to_mesh": False, + "storyboard": True, +} + +OUTPUT_COL_LINK_MAPPING = { + "anim": None, + "comp": ['anim', 'fx', 'lighting'], + "fx": ['anim', 'lighting'], + "layout": None, + "lighting": ['anim'], + "previz": None, + "rendering": None, + "smear_to_mesh": None, + "storyboard": None, +} + +LOAD_EDITORIAL_REF = { + "anim": True, + "comp": False, + "fx": False, + "layout": True, + "lighting": False, + "previz": False, + "rendering": False, + "smear_to_mesh": False, + "storyboard": False, +} + +ASSET_TYPE_TO_OVERRIDE = { + "CH": True, # Character + "PR": True, # Rigged Prop + "LI": True, # Library/Environment Asset + "SE": False, # Set + "LG": True, # Lighting Rig + "CA": True, # Camera Rig +} diff --git a/scripts-blender/addons/blender_kitsu/cache.py b/scripts-blender/addons/blender_kitsu/cache.py index 994e3beb..7f770acf 100644 --- a/scripts-blender/addons/blender_kitsu/cache.py +++ b/scripts-blender/addons/blender_kitsu/cache.py @@ -269,6 +269,18 @@ def get_shots_enum_for_active_seq( return _shot_enum_list +def get_shots_enum_for_seq( + self: bpy.types.Operator, context: bpy.types.Context, sequence: Sequence +) -> List[Tuple[str, str, str]]: + global _shot_enum_list + + _shot_enum_list.clear() + _shot_enum_list.extend( + [(s.id, s.name, s.description or "") for s in sequence.get_all_shots()] + ) + return _shot_enum_list + + def get_assetypes_enum_list( self: bpy.types.Operator, context: bpy.types.Context ) -> List[Tuple[str, str, str]]: @@ -343,6 +355,20 @@ def get_shot_task_types_enum( return _task_types_shots_enum_list +def get_shot_task_types_enum_for_shot( # TODO Rename + self: bpy.types.Operator, context: bpy.types.Context, shot: Shot +) -> List[Tuple[str, str, str]]: + # TODO why do we have global variables here can't I just return the items directly? We clear the list every time how is this a cahe? + global _task_types_shots_enum_list + + items = [(t.id, t.name, "") for t in shot.get_all_task_types()] + + _task_types_shots_enum_list.clear() + _task_types_shots_enum_list.extend(items) + + return _task_types_shots_enum_list + + def get_all_task_statuses_enum( self: bpy.types.Operator, context: bpy.types.Context ) -> List[Tuple[str, str, str]]: diff --git a/scripts-blender/addons/blender_kitsu/prefs.py b/scripts-blender/addons/blender_kitsu/prefs.py index 17d66644..46f2df2a 100644 --- a/scripts-blender/addons/blender_kitsu/prefs.py +++ b/scripts-blender/addons/blender_kitsu/prefs.py @@ -40,7 +40,7 @@ from .auth.ops import ( ) from .context.ops import KITSU_OT_con_productions_load from .lookdev.prefs import LOOKDEV_preferences -from .shot_builder.editorial.core import editorial_export_check_latest +from .shot_builder.editorial import editorial_export_check_latest logger = LoggerFactory.getLogger() @@ -339,12 +339,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): default="ANI-", ) - user_exec_code: bpy.props.StringProperty( # type: ignore - name="Post Execution Command", - description="Run this command after shot_builder is complete, but before the file is saved.", - default="", - ) - session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) @@ -439,7 +433,7 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): start_frame_row.prop(self, "shot_builder_frame_offset", text="") box.row().prop(self, "shot_builder_armature_prefix") box.row().prop(self, "shot_builder_action_prefix") - box.row().prop(self, "user_exec_code") + box.operator("kitsu.save_shot_builder_hooks", icon='FILE_SCRIPT') # Misc settings. box = layout.box() @@ -527,6 +521,11 @@ def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences: return context.preferences.addons["blender_kitsu"].preferences +def project_root_dir_get(context: bpy.types.Context): + addon_prefs = addon_prefs_get(context) + return Path(addon_prefs.project_root_dir).resolve() + + def session_auth(context: bpy.types.Context) -> bool: """ Shortcut to check if zession is authorized diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/__init__.py b/scripts-blender/addons/blender_kitsu/shot_builder/__init__.py index 9f91351e..fb9bf211 100644 --- a/scripts-blender/addons/blender_kitsu/shot_builder/__init__.py +++ b/scripts-blender/addons/blender_kitsu/shot_builder/__init__.py @@ -1,62 +1,13 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -from .ui import * -from .connectors.kitsu import * -from .operators import * import bpy -from .anim_setup import ops as anim_setup_ops # TODO Fix Registraion -from .editorial import ops as editorial_ops # TODO Fix Registraion - -# import logging -# logging.basicConfig(level=logging.DEBUG) - - -# bl_info = { -# 'name': 'Shot Builder', -# "author": "Jeroen Bakker", -# 'version': (0, 1), -# 'blender': (2, 90, 0), -# 'location': 'Addon Preferences panel and file new menu', -# 'description': 'Shot builder production tool.', -# 'category': 'Studio', -# } - - -classes = ( - KitsuPreferences, - SHOTBUILDER_OT_NewShotFile, -) +from . import ops +from .ui import topbar_file_new_draw_handler def register(): - anim_setup_ops.register() - editorial_ops.register() - for cls in classes: - bpy.utils.register_class(cls) bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler) + ops.register() def unregister(): - anim_setup_ops.unregister() - editorial_ops.unregister() bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler) - for cls in classes: - bpy.utils.unregister_class(cls) + ops.unregister() diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/core.py b/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/core.py deleted file mode 100644 index ccb4668b..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/core.py +++ /dev/null @@ -1,34 +0,0 @@ -import bpy -import re -from pathlib import Path -from typing import Set -from ... import prefs -from ... import cache - - -def animation_workspace_vse_area_add(context: bpy.types.Context): - """Split smallest 3D View in current workspace""" - for workspace in [ - workspace for workspace in bpy.data.workspaces if workspace.name == "Animation" - ]: - context.window.workspace = workspace - context.view_layer.update() - areas = workspace.screens[0].areas - view_3d_areas = sorted( - [area for area in areas if area.ui_type == "VIEW_3D"], - key=lambda x: x.width, - reverse=False, - ) - small_view_3d = view_3d_areas[0] - with context.temp_override(window=context.window, area=small_view_3d): - bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.5) - small_view_3d.ui_type = "SEQUENCE_EDITOR" - small_view_3d.spaces[0].view_type = "PREVIEW" - - -def animation_workspace_delete_others(): - """Delete any workspace that is not an animation workspace""" - for ws in bpy.data.workspaces: - if ws.name != "Animation": - with bpy.context.temp_override(workspace=ws): - bpy.ops.workspace.delete() diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/ops.py b/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/ops.py deleted file mode 100644 index dbc6640c..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/anim_setup/ops.py +++ /dev/null @@ -1,35 +0,0 @@ -import bpy -from typing import Set -from .core import animation_workspace_delete_others, animation_workspace_vse_area_add -class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): - bl_idname = "anim_setup.setup_workspaces" - bl_label = "Setup Workspace" - bl_description = "Sets up the workspaces for the animation task" - - def execute(self, context: bpy.types.Context) -> Set[str]: - animation_workspace_delete_others(self, context) - self.report({"INFO"}, "Deleted non Animation workspaces") - return {"FINISHED"} - -class ANIM_SETUP_OT_animation_workspace_vse_area_add(bpy.types.Operator): - bl_idname = "anim_setup.animation_workspace_vse_area_add" - bl_label = "Split Viewport" - bl_description = "Split smallest 3D View in current workspace" - - def execute(self, context: bpy.types.Context) -> Set[str]: - animation_workspace_vse_area_add(self, context) - return {"FINISHED"} - -classes = [ - ANIM_SETUP_OT_setup_workspaces, - ANIM_SETUP_OT_animation_workspace_vse_area_add -] - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/asset.py b/scripts-blender/addons/blender_kitsu/shot_builder/asset.py deleted file mode 100644 index f1734e25..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/asset.py +++ /dev/null @@ -1,50 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - - -class Asset: - """ - Container to hold data where the asset can be located in the production repository. - - path: absolute path to the blend file containing this asset. - - """ - - asset_type = "" - code = "" - name = "" - path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend" - collection = "{asset.code}" - - def __str__(self) -> str: - return self.name - - -class AssetRef: - """ - Reference to an asset from an external system. - """ - - def __init__(self, name: str = "", code: str = ""): - self.name = name - self.code = code - - def __str__(self) -> str: - return self.name diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/assets.py b/scripts-blender/addons/blender_kitsu/shot_builder/assets.py new file mode 100644 index 00000000..68c20671 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/assets.py @@ -0,0 +1,60 @@ +import bpy +from .. import prefs +from pathlib import Path +import json +from ..types import Shot +from .core import link_and_override_collection, link_data_block +from .. import bkglobals + + +def get_asset_index_file() -> str: + svn_project_root_dir = prefs.project_root_dir_get(bpy.context) + asset_index_file = ( + Path(svn_project_root_dir) + .joinpath("pro") + .joinpath("assets") + .joinpath("asset_index.json") + ) + if asset_index_file.exists(): + return asset_index_file.__str__() + + +def get_assset_index() -> dict: + asset_index_file = get_asset_index_file() + if asset_index_file is None: + return + return json.load(open(asset_index_file)) + + +def get_shot_assets( + scene: bpy.types.Scene, + output_collection: bpy.types.Collection, + shot: Shot, +): + asset_index = get_assset_index() + if asset_index is None: + return + assets = shot.get_all_assets() + asset_slugs = [ + asset.data.get("slug") for asset in assets if asset.data.get("slug") is not None + ] + if asset_slugs == []: + print("No asset slugs found on Kitsu Server. Assets will not be loaded") + for key, value in asset_index.items(): + if key in asset_slugs: + filepath = value.get('filepath') + data_type = value.get('type') + if bkglobals.ASSET_TYPE_TO_OVERRIDE.get(key.split('-')[0]): + if data_type != "Collection": + print(f"Cannot load {key} because it is not a collection") + continue + linked_collection = link_and_override_collection( + collection_name=key, file_path=filepath, scene=scene + ) + print(f"'{key}': Succesfully Linked & Overriden") + else: + linked_collection = link_data_block( + file_path=filepath, data_block_name=key, data_block_type=data_type + ) + print(f"'{key}': Succesfully Linked") + output_collection.children.link(linked_collection) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/__init__.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/__init__.py deleted file mode 100644 index 0cd620b2..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -from ..project import Production -from ..task_type import TaskType -from ..asset import Asset, AssetRef -from .build_step import BuildStep, BuildContext -from .init_asset import InitAssetStep -from .init_shot import InitShotStep -from .set_render_settings import SetRenderSettingsStep -from .new_scene import NewSceneStep -from .invoke_hook import InvokeHookStep - -import bpy - -import typing -import logging - -logger = logging.getLogger(__name__) - - -class ShotBuilder: - def __init__( - self, - context: bpy.types.Context, - production: Production, - task_type: TaskType, - shot_name: str, - ): - self._steps: typing.List[BuildStep] = [] - - shot = production.get_shot(context, shot_name) - assert shot - render_settings = production.get_render_settings(context, shot) - self.build_context = BuildContext( - context=context, - production=production, - shot=shot, - render_settings=render_settings, - task_type=task_type, - ) - - def __find_asset(self, asset_ref: AssetRef) -> typing.Optional[Asset]: - for asset_class in self.build_context.production.assets: - asset = typing.cast(Asset, asset_class()) - logger.debug(f"{asset_ref.name}, {asset.name}") - if asset_ref.name == asset.name: - return asset - return None - - def create_build_steps(self) -> None: - self._steps.append(InitShotStep()) - self._steps.append(NewSceneStep()) - self._steps.append(SetRenderSettingsStep()) - - production = self.build_context.production - task_type = self.build_context.task_type - - # Add global hooks. - for hook in production.hooks.filter(): - self._steps.append(InvokeHookStep(hook)) - - # Add task specific hooks. - for hook in production.hooks.filter(match_task_type=task_type.name): - self._steps.append(InvokeHookStep(hook)) - - context = self.build_context.context - shot = self.build_context.shot - - # Collect assets that should be loaded. - asset_refs = production.get_assets_for_shot(context, shot) - assets = [] - for asset_ref in asset_refs: - asset = self.__find_asset(asset_ref) - if asset is None: - logger.warning(f"cannot determine repository data for {asset_ref}") - continue - assets.append(asset) - - # Sort the assets on asset_type and asset.code). - assets.sort(key=lambda asset: (asset.asset_type, asset.code)) - - # Build asset specific build steps. - for asset in assets: - self._steps.append(InitAssetStep(asset)) - # Add asset specific hooks. - for hook in production.hooks.filter( - match_task_type=task_type.name, match_asset_type=asset.asset_type - ): - self._steps.append(InvokeHookStep(hook)) - - def build(self) -> None: - num_steps = len(self._steps) - step_number = 1 - build_context = self.build_context - window_manager = build_context.context.window_manager - window_manager.progress_begin(min=0, max=num_steps) - for step in self._steps: - logger.info(f"Building step [{step_number}/{num_steps}]: {step} ") - step.execute(build_context=build_context) - window_manager.progress_update(value=step_number) - step_number += 1 - window_manager.progress_end() diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/build_step.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/build_step.py deleted file mode 100644 index 123377b6..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/build_step.py +++ /dev/null @@ -1,38 +0,0 @@ -import bpy -import typing - -from ..project import Production -from ..shot import Shot -from ..task_type import TaskType -from ..render_settings import RenderSettings -from ..asset import Asset - - -class BuildContext: - def __init__(self, context: bpy.types.Context, production: Production, shot: Shot, render_settings: RenderSettings, task_type: TaskType): - self.context = context - self.production = production - self.shot = shot - self.task_type = task_type - self.render_settings = render_settings - self.asset: typing.Optional[Asset] = None - self.scene: typing.Optional[bpy.types.Scene] = None - - def as_dict(self) -> typing.Dict[str, typing.Any]: - return { - 'context': self.context, - 'scene': self.scene, - 'production': self.production, - 'shot': self.shot, - 'task_type': self.task_type, - 'render_settings': self.render_settings, - 'asset': self.asset, - } - - -class BuildStep: - def __str__(self) -> str: - return "unnamed build step" - - def execute(self, build_context: BuildContext) -> None: - raise NotImplementedError() diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_asset.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_asset.py deleted file mode 100644 index e0f36731..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_asset.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -from ..asset import * -from ..project import * -from ..shot import * - -import bpy - -import logging - -logger = logging.getLogger(__name__) - - -class InitAssetStep(BuildStep): - def __init__(self, asset: Asset): - self.__asset = asset - - def __str__(self) -> str: - return f"init asset \"{self.__asset.name}\"" - - def execute(self, build_context: BuildContext) -> None: - build_context.asset = self.__asset - self.__asset.path = self.__asset.path.format_map(build_context.as_dict()) - self.__asset.collection = self.__asset.collection.format_map( - build_context.as_dict() - ) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_shot.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_shot.py deleted file mode 100644 index eaee45fa..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/init_shot.py +++ /dev/null @@ -1,19 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -from ..asset import * -from ..project import * -from ..shot import * - -import bpy - -import logging - -logger = logging.getLogger(__name__) - - -class InitShotStep(BuildStep): - def __str__(self) -> str: - return "init shot" - - def execute(self, build_context: BuildContext) -> None: - shot = build_context.shot - shot.file_path = shot.file_path_format.format_map(build_context.as_dict()) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/invoke_hook.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/invoke_hook.py deleted file mode 100644 index 0ec908e5..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/invoke_hook.py +++ /dev/null @@ -1,21 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -from ..hooks import HookFunction -import bpy - -import typing -import types -import logging - -logger = logging.getLogger(__name__) - - -class InvokeHookStep(BuildStep): - def __init__(self, hook: HookFunction): - self._hook = hook - - def __str__(self) -> str: - return f"invoke hook [{self._hook.__name__}]" - - def execute(self, build_context: BuildContext) -> None: - params = build_context.as_dict() - self._hook(**params) # type: ignore diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/new_scene.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/new_scene.py deleted file mode 100644 index 1d4c4b94..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/new_scene.py +++ /dev/null @@ -1,30 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -from ..render_settings import RenderSettings -import bpy - -import logging - -logger = logging.getLogger(__name__) - - -class NewSceneStep(BuildStep): - def __str__(self) -> str: - return f"new scene" - - def execute(self, build_context: BuildContext) -> None: - production = build_context.production - scene_name = production.scene_name_format.format_map( - build_context.as_dict()) - logger.debug(f"create scene with name {scene_name}") - scene = bpy.data.scenes.new(name=scene_name) - - bpy.context.window.scene = scene - build_context.scene = scene - - self.__remove_other_scenes(build_context) - - def __remove_other_scenes(self, build_context: BuildContext) -> None: - for scene in bpy.data.scenes: - if scene != build_context.scene: - logger.debug(f"remove scene {scene.name}") - bpy.data.scenes.remove(scene) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/save_file.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/save_file.py deleted file mode 100644 index f4f7c8fb..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/save_file.py +++ /dev/null @@ -1,30 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -from ..asset import * -from ..project import * -from ..shot import * -import pathlib - -import bpy - -import logging - -logger = logging.getLogger(__name__) - - -def save_shot_builder_file(file_path: str): - """Save Shot File within Folder of matching name. - Set Shot File to relative Paths.""" - dir_path = pathlib.Path(file_path) - dir_path.mkdir(parents=True, exist_ok=True) - bpy.ops.wm.save_mainfile(filepath=file_path, relative_remap=True) - - -class SaveFileStep(BuildStep): - def __str__(self) -> str: - return "save file" - - def execute(self, build_context: BuildContext) -> None: - shot = build_context.shot - file_path = pathlib.Path(shot.file_path) - save_shot_builder_file(file_path) - logger.info(f"save file {shot.file_path}") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/builder/set_render_settings.py b/scripts-blender/addons/blender_kitsu/shot_builder/builder/set_render_settings.py deleted file mode 100644 index b9782d20..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/builder/set_render_settings.py +++ /dev/null @@ -1,27 +0,0 @@ -from ..builder.build_step import BuildStep, BuildContext -import bpy - -import typing -import logging - -logger = logging.getLogger(__name__) - - -class SetRenderSettingsStep(BuildStep): - def __str__(self) -> str: - return f"set render settings" - - def execute(self, build_context: BuildContext) -> None: - scene = typing.cast(bpy.types.Scene, build_context.scene) - render_settings = build_context.render_settings - logger.debug( - f"set render resolution to {render_settings.width}x{render_settings.height}") - scene.render.resolution_x = render_settings.width - scene.render.resolution_y = render_settings.height - scene.render.resolution_percentage = 100 - - shot = build_context.shot - scene.frame_start = shot.frame_start - scene.frame_current = shot.frame_start - scene.frame_end = shot.frame_start + shot.frames -1 - logger.debug(f"set frame range to ({scene.frame_start}-{scene.frame_end})") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/__init__.py b/scripts-blender/addons/blender_kitsu/shot_builder/connectors/__init__.py deleted file mode 100644 index b07da1ed..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/connector.py b/scripts-blender/addons/blender_kitsu/shot_builder/connectors/connector.py deleted file mode 100644 index 23bd8f36..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/connector.py +++ /dev/null @@ -1,108 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# -""" -This module contains the Connector class. It is an abstract base class for concrete connectors. -""" - -from ..shot import Shot, ShotRef -from..asset import Asset, AssetRef -from..task_type import TaskType -from..render_settings import RenderSettings -from typing import * - - -if TYPE_CHECKING: - from..project import Production - from..properties import ShotBuilderPreferences - - -class Connector: - """ - A Connector is used to retrieve data from a source. This source can be an external system. - - Connectors can be configured for productions in its `shot-builder/config.py` file. - - # Members - - _production: reference to the production that we want to read data for. - _preference: reference to the add-on preference to read settings for. - Connectors can add settings to the add-on preferences. - - # Class Members - - PRODUCTION_KEYS: Connectors can register production configuration keys that will be loaded from the production config file. - When keys are added the content will be read and stored in the production. - - # Usage - - Concrete connectors only overrides methods that they support. All non-overridden methods will raise an - NotImplementerError. - - - Example of using predefined connectors in a production config file: - ```shot-builder/config.py - from ..connectors.default import DefaultConnector - from ..connectors.kitsu import KitsuConnector - - PRODUCTION_NAME = DefaultConnector - TASK_TYPES = KitsuConnector - KITSU_PROJECT_ID = "...." - ``` - """ - PRODUCTION_KEYS: Set[str] = set() - - def __init__(self, production: 'Production', preferences: 'ShotBuilderPreferences'): - self._production = production - self._preferences = preferences - - def get_name(self) -> str: - """ - Retrieve the production name using the connector. - """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support retrieval of production name") - - def get_task_types(self) -> List[TaskType]: - """ - Retrieve the task types using the connector. - """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support retrieval of task types") - - def get_shots(self) -> List[ShotRef]: - """ - Retrieve the shots using the connector. - """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support retrieval of shots") - - def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]: - """ - Retrieve the sequences using the connector. - """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support retrieval of assets for a shot") - - def get_render_settings(self, shot: Shot) -> RenderSettings: - """ - Retrieve the render settings for the given shot. - """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support retrieval of render settings for a shot") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/default.py b/scripts-blender/addons/blender_kitsu/shot_builder/connectors/default.py deleted file mode 100644 index e655094d..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/default.py +++ /dev/null @@ -1,49 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# -from ..shot import Shot, ShotRef -from ..asset import Asset, AssetRef -from ..task_type import TaskType -from ..render_settings import RenderSettings -from ..connectors.connector import Connector -from typing import * - - -class DefaultConnector(Connector): - """ - Default connector is a connector that returns the defaults for the shot builder add-on. - """ - - def get_name(self) -> str: - return "unnamed production" - - def get_shots(self) -> List[ShotRef]: - return [] - - def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]: - return [] - - def get_task_types(self) -> List[TaskType]: - return [TaskType("anim"), TaskType("lighting"), TaskType("comp"), TaskType("fx")] - - def get_render_settings(self, shot: Shot) -> RenderSettings: - """ - Retrieve the render settings for the given shot. - """ - return RenderSettings(width=1920, height=1080, frames_per_second=24.0) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/kitsu.py b/scripts-blender/addons/blender_kitsu/shot_builder/connectors/kitsu.py deleted file mode 100644 index 8bc50536..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/connectors/kitsu.py +++ /dev/null @@ -1,248 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# -import bpy -from .. import vars -from ..shot import Shot, ShotRef -from ..asset import Asset, AssetRef -from ..task_type import TaskType -from ..render_settings import RenderSettings -from ..connectors.connector import Connector -import requests -from ... import cache -import gazu - -import typing -import logging - -logger = logging.getLogger(__name__) - - -class KitsuException(Exception): - pass - - -class KitsuPreferences(bpy.types.PropertyGroup): - backend: bpy.props.StringProperty( # type: ignore - name="Server URL", - description="Kitsu server address", - default="https://kitsu.blender.cloud/api", - ) - - username: bpy.props.StringProperty( # type: ignore - name="Username", - description="Username to connect to Kitsu", - ) - - password: bpy.props.StringProperty( # type: ignore - name="Password", - description="Password to connect to Kitsu", - subtype='PASSWORD', - ) - - def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - layout.label(text="Kitsu") - layout.prop(self, "backend") - layout.prop(self, "username") - layout.prop(self, "password") - - def _validate(self): - if not (self.backend and self.username and self.password): - raise KitsuException( - "Kitsu connector has not been configured in the add-on preferences" - ) - - -class KitsuDataContainer: - def __init__(self, data: typing.Dict[str, typing.Optional[str]]): - self._data = data - - def get_parent_id(self) -> typing.Optional[str]: - return self._data['parent_id'] - - def get_id(self) -> str: - return str(self._data['id']) - - def get_name(self) -> str: - return str(self._data['name']) - - def get_code(self) -> typing.Optional[str]: - return self._data['code'] - - def get_description(self) -> str: - result = self._data['description'] - if result is None: - return "" - return result - - -class KitsuProject(KitsuDataContainer): - def get_resolution(self) -> typing.Tuple[int, int]: - """ - Get the resolution and decode it to (width, height) - """ - res_str = str(self._data['resolution']) - splitted = res_str.split("x") - return (int(splitted[0]), int(splitted[1])) - - -class KitsuSequenceRef(ShotRef): - def __init__(self, kitsu_id: str, name: str, code: str): - super().__init__(name=name, code=code) - self.kitsu_id = kitsu_id - - def sync_data(self, shot: Shot) -> None: - shot.sequence_code = self.name - - -class KitsuShotRef(ShotRef): - def __init__( - self, - kitsu_id: str, - name: str, - code: str, - frame_start: int, - frames: int, - frame_end: int, - frames_per_second: float, - sequence: KitsuSequenceRef, - ): - super().__init__(name=name, code=code) - self.kitsu_id = kitsu_id - self.frame_start = frame_start - self.frames = frames - self.frame_end = frame_end - self.frames_per_second = frames_per_second - self.sequence = sequence - - def sync_data(self, shot: Shot) -> None: - shot.name = self.name - shot.code = self.code - shot.kitsu_id = self.kitsu_id - shot.frame_start = self.frame_start - shot.frames = self.frames - shot.frame_end = self.frame_end - shot.frames_per_second = self.frames_per_second - self.sequence.sync_data(shot) - - -class KitsuConnector(Connector): - # PRODUCTION_KEYS = {'KITSU_PROJECT_ID'} - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def __get_production_data(self) -> KitsuProject: - production = cache.project_active_get() - project = KitsuProject(typing.cast(typing.Dict[str, typing.Any], production)) - return project - - def get_name(self) -> str: - production = self.__get_production_data() - return production.get_name() - - def get_task_types(self) -> typing.List[TaskType]: - project = cache.project_active_get() - task_types = project.task_types - import pprint - - pprint.pprint(task_types) - return [] - - def get_shots(self) -> typing.List[ShotRef]: - project = cache.project_active_get() - kitsu_sequences = gazu.shot.all_sequences_for_project(project.id) - - sequence_lookup = { - sequence_data['id']: KitsuSequenceRef( - kitsu_id=sequence_data['id'], - name=sequence_data['name'], - code=sequence_data['code'], - ) - for sequence_data in kitsu_sequences - } - - kitsu_shots = gazu.shot.all_shots_for_project(project.id) - shots: typing.List[ShotRef] = [] - - for shot_data in kitsu_shots: - # Initialize default values - frame_start = vars.DEFAULT_FRAME_START - frame_end = 0 - - # shot_data['data'] can be None - if shot_data['data']: - # If 3d_start key not found use default start frame. - frame_start = int( - shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START) - ) - frame_end = ( - int(shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START)) - + shot_data['nb_frames'] - - 1 - ) - - # If 3d_start and 3d_out available use that to calculate frames. - # If not try shot_data['nb_frames'] or 0 -> invalid. - frames = int( - (frame_end - frame_start + 1) - if frame_end - else shot_data['nb_frames'] or 0 - ) - if frames < 0: - logger.error( - "%s duration is negative: %i. Check frame range information on Kitsu", - shot_data['name'], - frames, - ) - frames = 0 - - shots.append( - KitsuShotRef( - kitsu_id=shot_data['id'], - name=shot_data['name'], - code=shot_data['code'], - frame_start=frame_start, - frames=frames, - frame_end=frame_end, - frames_per_second=24.0, - sequence=sequence_lookup[shot_data['parent_id']], - ) - ) - - return shots - - def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]: - kitsu_assets = gazu.asset.all_assets_for_shot(shot.kitsu_id) - - return [ - AssetRef(name=asset_data['name'], code=asset_data['code']) - for asset_data in kitsu_assets - ] - - def get_render_settings(self, shot: Shot) -> RenderSettings: - """ - Retrieve the render settings for the given shot. - """ - project = cache.project_active_get() - return RenderSettings( - width=int(project.resolution.split('x')[0]), - height=int(project.resolution.split('x')[1]), - frames_per_second=project.fps, - ) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/core.py b/scripts-blender/addons/blender_kitsu/shot_builder/core.py new file mode 100644 index 00000000..bd979c4c --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/core.py @@ -0,0 +1,202 @@ +import bpy +from pathlib import Path + +from .. import bkglobals +from ..types import ( + Sequence, + Shot, + TaskType, +) + +from ..cache import Project + +from .. import prefs + +################# +# Constants +################# +CAMERA_NAME = 'CAM-camera' + + +def get_file_dir(seq: Sequence, shot: Shot, task_type: TaskType) -> Path: + """Returns Path to Directory for Current Shot, will ensure that + file path exists if it does not. + + Args: + seq (Sequence): Sequence Class from blender_kitsu.types + shot (Shot): Shot Class from blender_kitsu.types + task_type TaskType Class from blender_kitsu.types + + Returns: + Path: Returns Path for Shot Directory + """ + project_root_dir = prefs.project_root_dir_get(bpy.context) + all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots') + shot_dir = all_shots_dir.joinpath(seq.name).joinpath(shot.name) + if not shot_dir.exists(): + shot_dir.mkdir(parents=True) + return shot_dir + + +def set_render_engine(scene: bpy.types.Scene, engine='CYCLES'): + """ + By default we set Cycles as the renderer. + """ + scene.render.engine = engine + + +def remove_all_data(): + for lib in bpy.data.libraries: + bpy.data.libraries.remove(lib) + + for col in bpy.data.collections: + bpy.data.collections.remove(col) + + for obj in bpy.data.objects: + bpy.data.objects.remove(obj) + + bpy.ops.outliner.orphans_purge( + do_local_ids=True, do_linked_ids=True, do_recursive=True + ) + + +def set_shot_scene(context: bpy.types.Context, scene_name: str) -> bpy.types.Scene: + print(f"create scene with name {scene_name}") + for scene in bpy.data.scenes: + scene.name = 'REMOVE-' + scene.name + + keep_scene = bpy.data.scenes.new(name=scene_name) + for scene in bpy.data.scenes: + if scene.name == scene_name: + continue + print(f"remove scene {scene.name}") + bpy.data.scenes.remove(scene) + + context.window.scene = keep_scene + return keep_scene + + +def set_resolution_and_fps(project: Project, scene: bpy.types.Scene): + scene.render.fps = int(project.fps) # set fps + resolution = project.resolution.split('x') + scene.render.resolution_x = int(resolution[0]) + scene.render.resolution_y = int(resolution[1]) + scene.render.resolution_percentage = 100 + + +def get_3d_start(shot: Shot): + if shot.data and shot.data.get("3d_start"): # shot.data and + return int(shot.data.get("3d_start")) + else: + return bkglobals.FRAME_START + + +def set_frame_range(shot: Shot, scene: bpy.types.Scene): + start_3d = get_3d_start(shot) + scene.frame_start = start_3d + if not shot.nb_frames: + raise Exception(f"{shot.name} has missing frame duration information") + scene.frame_end = start_3d + shot.nb_frames - 1 + + +def link_data_block(file_path: str, data_block_name: str, data_block_type: str): + bpy.ops.wm.link( + filepath=file_path, + directory=file_path + "/" + data_block_type, + filename=data_block_name, + instance_collections=False, + ) + # TODO This doesn't return anything but collections + return bpy.data.collections.get(data_block_name) + + +def link_and_override_collection( + file_path: str, collection_name: str, scene: bpy.types.Scene +) -> bpy.types.Collection: + """_summary_ + + Args: + file_path (str): File Path to .blend file to link from + collection_name (str): Name of collection to link from given filepath + scene (bpy.types.Scene): Current Scene to link collection to + + Returns: + bpy.types.Collection: Overriden Collection linked to Scene Collection + """ + camera_col = link_data_block(file_path, collection_name, "Collection") + override_camera_col = camera_col.override_hierarchy_create( + scene, bpy.context.view_layer, do_fully_editable=True + ) + scene.collection.children.unlink(camera_col) + # Make library override. + return override_camera_col + + +def link_camera_rig( + scene: bpy.types.Scene, + output_collection: bpy.types.Collection, +): + """ + Function to load the camera rig. The rig will be added to the output collection + of the shot and the camera will be set as active camera. + """ + # Load camera rig. + project_path = prefs.project_root_dir_get(bpy.context) + path = f"{project_path}/pro/assets/cam/camera_rig.blend" + + if not Path(path).exists(): + camera_data = bpy.data.cameras.new(name=CAMERA_NAME) + camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data) + scene.collection.objects.link(camera_object) + output_collection.objects.link(camera_object) + return + + collection_name = ( + "CA-camera_rig" # TODO Rename the asset itself, this breaks convention + ) + + override_camera_col = link_and_override_collection( + file_path=path, collection_name=collection_name, scene=scene + ) + output_collection.children.link(override_camera_col) + + # Set the camera of the camera rig as active scene camera. + camera = override_camera_col.objects.get(CAMERA_NAME) + scene.camera = camera + + +def create_task_type_output_collection( + scene: bpy.types.Scene, shot: Shot, task_type: TaskType +) -> bpy.types.Collection: + collections = bpy.data.collections + output_col_name = shot.get_output_collection_name(task_type.get_short_name()) + + if not collections.get(output_col_name): + bpy.data.collections.new(name=output_col_name) + output_collection = collections.get(output_col_name) + + output_collection.use_fake_user = True + if not scene.collection.children.get(output_col_name): + scene.collection.children.link(output_collection) + + for view_layer in scene.view_layers: + view_layer_output_collection = view_layer.layer_collection.children.get( + output_col_name + ) + view_layer_output_collection.exclude = True + return output_collection + + +def link_task_type_output_collections(shot: Shot, task_type: TaskType): + task_type_short_name = task_type.get_short_name() + if bkglobals.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name) == None: + return + for short_name in bkglobals.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name): + external_filepath = shot.get_shot_filepath(bpy.context, short_name) + if not Path(external_filepath).exists(): + print( + f"Unable to link output collection for {Path(external_filepath).name}" + ) + file_path = external_filepath.__str__() + colection_name = shot.get_output_collection_name(short_name) + link_data_block(file_path, colection_name, 'Collection') diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/README.md b/scripts-blender/addons/blender_kitsu/shot_builder/docs/README.md deleted file mode 100644 index 66b55528..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/README.md +++ /dev/null @@ -1,246 +0,0 @@ -# Project Description (DRAFT) - -Shot Builder is an Add-on that helps studios to work with task specific -Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are - -* Build blend files for a specific task and shot. -* Sync data back from work files to places like kitsu, or `edit.blend`. - -## Design Principles - -The main design principles are: - -* The core-tool can be installed as an add-on, but the (production specific) - configuration should be part of the production repository. -* The configuration files are a collection of python files. The API between - the configuration files and the add-on should be easy to use as pipeline - TDs working on the production should be able to work with it. -* TDs/artists should be able to handle issues during building without looking - at how the add-on is structured. -* The tool contains connectors that can be configured to read/write data - from the system/file that is the main location of the data. For example - The start and end time of a shot could be stored in an external production tracking application. - -## Connectors - -Connectors are components that can be used to read or write to files or -systems. The connectors will add flexibility to the add-on so it could be used -in multiple productions or studios. - -In the configuration files the TD can setup the connectors that are used for -the production. Possible connectors would be: - -* Connector for text based config files (json/yaml). -* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html). -* Connector for blend files. - -## Layering & Hooks - -The configuration of the tool is layered. When building a work file for a sequence -there are multiple ways to change the configuration. - -* Configuration for the production. -* Configuration for the asset that is needed. -* Configuration for the asset type of the loaded asset. -* Configuration for the sequence. -* Configuration for the shot. -* Configuration for the task type. - -For any combination of these configurations hooks can be defined. - -``` -@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A') -def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides when Spring is loaded in 02_020A. - """ - -@shot_tools.hook(match_task_type='anim') -def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides for any animation task. - """ -``` - -### Data - -All hooks must have Python’s `**kwargs` parameter. The `kwargs` contains -the context at the moment the hook is invoked. The context can contain the -following items. - -* `production`: `shot_tools.Production`: Include the name of the production - and the location on the filesystem. -* `task`: `shot_tools.Task`: The task (combination of task_type and shot) -* `task_type`: `shot_tools.TaskType`: Is part of the `task`. -* `sequence`: `shot_tools.Sequence`: Is part of `shot`. -* `shot`: `shot_tools.Shot` Is part of `task`. -* `asset`: `shot_tools.Asset`: Only available during asset loading phase. -* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase. - -### Execution Order - -The add-on will internally create a list containing the hooks that needs to be -executed for the command in a sensible order. It will then execute them in that -order. - -By default the next order will be used: - -* Production wide hooks -* Asset Type hooks -* Asset hooks -* Sequence hooks -* Shot hooks -* Task type hooks - -A hook with a single ‘match’ rule will be run in the corresponding phase. A hook with -multiple ‘match’ rules will be run in the last matching phase. For example, a hook with -‘asset’ and ‘task type’ match rules will be run in the ‘task type’ phase. - -#### Events - -Order of execution can be customized by adding the optional `run_before` -or `run_after` parameters. - -``` -@shot_tools.hook(match_task_type='anim', - requires={shot_tools.events.AssetsLoaded, hook_task_other_anim}, - is_required_by={shot_tools.events.ShotOverrides}) -def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None: - """ - Specific overrides for any animation task run after all assets have been loaded. - """ -``` - -Events could be: - -* `shot_tools.events.BuildStart` -* `shot_tools.events.ProductionSettingsLoaded` -* `shot_tools.events.AssetsLoaded` -* `shot_tools.events.AssetTypeOverrides` -* `shot_tools.events.SequenceOverrides` -* `shot_tools.events.ShotOverrides` -* `shot_tools.events.TaskTypeOverrides` -* `shot_tools.events.BuildFinished` -* `shot_tools.events.HookStart` -* `shot_tools.events.HookEnd` - -During usage we should see which one of these or other events are needed. - -`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded` -and `shot_tools.events.HookStart` can only be used in the `run_after` -parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished` -can only be used in the `run_before` parameter. - - -## API - -The shot builder has an API between the add-on and the configuration files. This -API contains convenience functions and classes to hide complexity and makes -sure that the configuration files are easy to maintain. - -``` -register_task_type(task_type="anim") -register_task_type(task_type="lighting") -``` - -``` -# shot_tool/characters.py -class Asset(shot_tool.some_module.Asset): - asset_file = "/{asset_type}/{name}/{name}.blend" - collection = “{class_name}” - name = “{class_name}” - -class Character(Asset): - asset_type = ‘char’ - - -class Ellie(Character): - collection = “{class_name}-{variant_name}” - variants = {‘default’, ‘short_hair’} - -class Victoria(Character): pass -class Rex(Character): pass - -# shot_tool/shots.py -class Shot_01_020_A(shot_tool.some_module.Shot): - shot_id = ‘01_020_A’ - assets = { - characters.Ellie(variant=”short_hair”), - characters.Rex, - sets.LogOverChasm, - } - -class AllHumansShot(shot_tool.some_module.Shot): - assets = { - characters.Ellie(variant=”short_hair”), - characters.Rex, - characters.Victoria, - } - -class Shot_01_035_A(AllHumansShot): - assets = { - sets.Camp, - } - -``` - -This API is structured/implemented in a way that it keeps track of what -is being done. This will be used when an error occurs so a descriptive -error message can be generated that would help the TD to solve the issue more -quickly. The goal would be that the error messages are descriptive enough to -direct the TD into the direction where the actual cause is. And when possible -propose several solutions to fix it. - -## Setting up the tool - -The artist/TD can configure their current local project directory in the add-on preferences. -This can then be used for new blend files. The project associated with an opened (so existing) -blend file can be found automatically by iterating over parent directories until a Shot Builder -configuration file is found. Project-specific settings are not configured/stored in the add-on, -but in this configuration file. - -The add-on will look in the root of the production repository to locate the -main configuration file `/project_root_directory/pro/shot-builder/config.py`. This file contains general -settings about the production, including: - -* The name of the production for reporting back to the user when needed. -* Naming standards to test against when reporting deviations. -* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production. -* Configuration of the needed connectors. - -### Directory Layout -``` bash -└── project-name/ # Project Root Directory - └── pro/ - ├── assets/ - ├── shot-builder/ - │ ├── assets.py - │ ├── config.py - │ ├── hooks.py - │ └── shots.py - └── shots/ -``` - -## Usage - -Any artist can open a shot file via the `File` menu. A modal panel appears -where the user can select the task type and sequence/shot. When the file -already exists, it will be opened. When the file doesn't exist, the file -will be built. - -In the future other use cases will also be accessible, such as: - -* Syncing data back from a work file to the source of the data. -* Report of errors/differences between the shot file and the configuration. - -## Open Issues - -### Security - -* Security keys needed by connectors need to be stored somewhere. The easy - place is to place inside the production repository, but that isn't secure - Anyone with access to the repository could misuse the keys to access the - connector. Other solution might be to use the OS key store or retrieve the - keys from an online service authenticated by the blender cloud add-on. - - We could use `keyring` to access OS key stores. \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/README.md b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/README.md deleted file mode 100644 index d894e6fe..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Example configuration files - -This folder contains an example shot builder configuration. It shows the part -that a TD would do to incorporate the shot builder in a production. \ No newline at end of file diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/assets.py b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/assets.py deleted file mode 100644 index de3619c3..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/assets.py +++ /dev/null @@ -1,30 +0,0 @@ -from blender_kitsu.shot_builder.asset import Asset - - -class ProductionAsset(Asset): - path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend" # Path to most assets - color_tag = "NONE" - - -# Categories -class Character(ProductionAsset): - asset_type = "chars" - collection = "CH-{asset.code}" # Prefix for characters - - -class Prop(ProductionAsset): - asset_type = "props" - collection = "PR-{asset.code}" # Prefix for props - - -# Assets -class MyCharacter(Character): - name = "My Character" # Name on Kitsu Server - code = "mycharacter" # Name of Collection without prefix (e.g. CH-mycharacter) - path = "{production.path}/assets/{asset.asset_type}/mycharacter/publish/mycharacter.v001.blend" # This asset has a custom path - color_tag = "COLOR_01" - - -class MyProp(Prop): - name = "MyProp" - code = "myprop" diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/config.py b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/config.py deleted file mode 100644 index 890e2fc5..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/config.py +++ /dev/null @@ -1,13 +0,0 @@ -from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector - -PRODUCTION_NAME = KitsuConnector -SHOTS = KitsuConnector -ASSETS = KitsuConnector -RENDER_SETTINGS = KitsuConnector - -# Formatting rules -# ---------------- - -# The name of the scene in blender where the shot is build in. -SCENE_NAME_FORMAT = "{shot.name}.{task_type}" -SHOT_NAME_FORMAT = "{shot.name}" diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/hooks.py b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/hooks.py deleted file mode 100644 index 409e9915..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/hooks.py +++ /dev/null @@ -1,170 +0,0 @@ -import bpy -from blender_kitsu.shot_builder.hooks import hook, Wildcard -from blender_kitsu.shot_builder.asset import Asset -from blender_kitsu.shot_builder.shot import Shot -from blender_kitsu.shot_builder.project import Production -from pathlib import Path -import logging - -logger = logging.getLogger(__name__) - -# ---------- Global Hook ---------- - - -CAMERA_NAME = 'CAM-camera' - - -@hook() -def set_cycles_render_engine(scene: bpy.types.Scene, **kwargs): - """ - By default we set Cycles as the renderer. - """ - scene.render.engine = 'CYCLES' - - -# ---------- Overrides for animation files ---------- - - -@hook(match_task_type='anim') -def task_type_anim_set_workbench(scene: bpy.types.Scene, **kwargs): - """ - Override of the render engine to Workbench when building animation files. - """ - scene.render.engine = 'BLENDER_WORKBENCH' - - -# ---------- Create output collection for animation files ---------- - - -def _add_camera_rig( - scene: bpy.types.Scene, - production: Production, - shot: Shot, -): - """ - Function to load the camera rig. The rig will be added to the output collection - of the shot and the camera will be set as active camera. - """ - # Load camera rig. - path = f"{production.path}/assets/cam/camera_rig.blend" - - if not Path(path).exists(): - camera_data = bpy.data.cameras.new(name=CAMERA_NAME) - camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data) - shot.output_collection.objects.link(camera_object) - return - - collection_name = "CA-camera_rig" - bpy.ops.wm.link( - filepath=path, - directory=path + "/Collection", - filename=collection_name, - ) - # Keep the active object name as this would also be the name of the collection after enabling library override. - active_object_name = bpy.context.active_object.name - - # Make library override. - bpy.ops.object.make_override_library() - - # Add camera collection to the output collection - asset_collection = bpy.data.collections[active_object_name] - shot.output_collection.children.link(asset_collection) - - # Set the camera of the camera rig as active scene camera. - camera = bpy.data.objects[CAMERA_NAME] - scene.camera = camera - - -@hook(match_task_type='anim') -def task_type_anim_output_collection( - scene: bpy.types.Scene, production: Production, shot: Shot, task_type: str, **kwargs -): - """ - Animations are stored in an output collection. This collection will be linked - by the lighting file. - - Also loads the camera rig. - """ - output_collection = bpy.data.collections.new( - name=shot.get_output_collection_name(shot=shot, task_type=task_type) - ) - shot.output_collection = output_collection - output_collection.use_fake_user = True - scene.collection.children.link(output_collection) - - _add_camera_rig(scene, production, shot) - - -@hook(match_task_type='lighting') -def link_anim_output_collection( - scene: bpy.types.Scene, production: Production, shot: Shot, **kwargs -): - """ - Link in the animation output collection from the animation file. - """ - anim_collection = bpy.data.collections.new(name="animation") - scene.collection.children.link(anim_collection) - anim_file_path = shot.get_anim_file_path(production, shot) - anim_output_collection_name = shot.get_output_collection_name( - shot=shot, task_type="anim" - ) - result = bpy.ops.wm.link( - filepath=anim_file_path, - directory=anim_file_path + "/Collection", - filename=anim_output_collection_name, - ) - assert result == {'FINISHED'} - - # Move the anim output collection from scene collection to the animation collection. - anim_output_collection = bpy.data.objects[anim_output_collection_name] - anim_collection.objects.link(anim_output_collection) - scene.collection.objects.unlink(anim_output_collection) - - # Use animation camera as active scene camera. - camera = bpy.data.objects['CAM-camera'] - scene.camera = camera - - -# ---------- Asset loading and linking ---------- - - -@hook(match_task_type='anim', match_asset_type=['chars', 'props']) -def link_char_prop_for_anim(scene: bpy.types.Scene, shot: Shot, asset: Asset, **kwargs): - """ - Loading a character or prop for an animation file. - """ - collection_names = [] - if asset.code == 'notepad_pencil': - collection_names.append("PR-pencil") - collection_names.append("PR-notepad") - else: - collection_names.append(asset.collection) - - for collection_name in collection_names: - logger.info("link asset") - bpy.ops.wm.link( - filepath=str(asset.path), - directory=str(asset.path) + "/Collection", - filename=collection_name, - ) - # Keep the active object name as this would also be the name of the collection after enabling library override. - active_object_name = bpy.context.active_object.name - - # Make library override. - bpy.ops.object.make_override_library() - - # Add overridden collection to the output collection. - asset_collection = bpy.data.collections[active_object_name] - shot.output_collection.children.link(asset_collection) - - -@hook(match_task_type=Wildcard, match_asset_type='sets') -def link_set(asset: Asset, **kwargs): - """ - Load the set of the shot. - """ - bpy.ops.wm.link( - filepath=str(asset.path), - directory=str(asset.path) + "/Collection", - filename=asset.collection, - ) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md deleted file mode 100644 index 8d737cdb..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Example configuration files - -This folder contains an example shot builder configuration. It shows the part -that a TD would do to incorporate the shot builder in a production. - - diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shots.py b/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shots.py deleted file mode 100644 index 7aace594..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/docs/examples/shots.py +++ /dev/null @@ -1,41 +0,0 @@ -from blender_kitsu.shot_builder.shot import Shot -from blender_kitsu.shot_builder.project import Production - - -class ProductionShot(Shot): - def get_anim_file_path(self, production: Production, shot: Shot) -> str: - """Get the animation file path for this given shot.""" - return self.file_path_format.format_map( - {'production': production, 'shot': shot, 'task_type': "anim"} - ) - - def get_lighting_file_path(self, production: Production, shot: Shot) -> str: - """Get the lighting file path for this given shot.""" - return self.file_path_format.format_map( - {'production': production, 'shot': shot, 'task_type': "lighting"} - ) - - def get_output_collection_name(self, shot: Shot, task_type: str) -> str: - """Get the collection name where the output is stored.""" - return f"{shot.name}.{task_type}.output" - - def is_valid(self) -> bool: - """Check if this shot contains all data, so it could be selected - for shot building. - """ - if not super().is_valid(): - return False - return True - - # Assuming path to file is in `project_name/svn/pro/shot/sequence_name/shot_name` - # Render Ouput path should be `project_name/shared/shot_frames/sequence_name/shot_name/` - - def get_render_output_dir(self) -> str: - return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.lighting" - - def get_comp_output_dir(self) -> str: - return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.comp" - - -class GenericShot(ProductionShot): - is_generic = True diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/core.py b/scripts-blender/addons/blender_kitsu/shot_builder/editorial.py similarity index 92% rename from scripts-blender/addons/blender_kitsu/shot_builder/editorial/core.py rename to scripts-blender/addons/blender_kitsu/shot_builder/editorial.py index 08109d83..5c445df7 100644 --- a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/core.py +++ b/scripts-blender/addons/blender_kitsu/shot_builder/editorial.py @@ -1,9 +1,8 @@ import bpy -import re +from .. import prefs from pathlib import Path -from typing import Set -from ... import prefs -from ... import cache +import re +from .core import get_3d_start def editorial_export_get_latest( @@ -16,7 +15,7 @@ def editorial_export_get_latest( if not latest_file: return None # Check if Kitsu server returned empty shot - if shot.get("id") == '': + if shot.id == '': return None strip_filepath = latest_file.as_posix() strip_frame_start = addon_prefs.shot_builder_frame_offset @@ -41,8 +40,8 @@ def editorial_export_get_latest( new_strips = [movie_strip, sound_strip] # Update shift frame range prop. - frame_in = shot["data"].get("frame_in") - frame_3d_start = shot["data"].get("3d_start") + frame_in = shot.data.get("frame_in") + frame_3d_start = get_3d_start(shot) frame_3d_offset = frame_3d_start - addon_prefs.shot_builder_frame_offset edit_export_offset = addon_prefs.edit_export_frame_offset diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/__init__.py b/scripts-blender/addons/blender_kitsu/shot_builder/editorial/__init__.py deleted file mode 100644 index 9e92c71a..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -import bpy -from . import ops - - -def register(): - ops.register() - - -def unregister(): - ops.unregister() diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/ops.py b/scripts-blender/addons/blender_kitsu/shot_builder/editorial/ops.py deleted file mode 100644 index c58c5970..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/editorial/ops.py +++ /dev/null @@ -1,42 +0,0 @@ -import bpy -from typing import Set -from .core import editorial_export_get_latest -from ... import cache -import gazu - - -class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator): - bl_idname = "asset_setup.load_latest_editorial" - bl_label = "Load Editorial Export" - bl_description = ( - "Loads latest edit from shot_preview_folder " - "Shifts edit so current shot starts at 3d_start metadata shot key from Kitsu" - ) - - def execute(self, context: bpy.types.Context) -> Set[str]: - cache_shot = cache.shot_active_get() - shot = gazu.shot.get_shot(cache_shot.id) # TODO INEFFICENT TO LOAD SHOT TWICE - strips = editorial_export_get_latest(context, shot) - if strips is None: - self.report( - {"ERROR"}, f"No valid editorial export in editorial export path." - ) - return {"CANCELLED"} - - self.report({"INFO"}, f"Loaded latest edit: {strips[0].name}") - return {"FINISHED"} - - -classes = [ - ANIM_SETUP_OT_load_latest_editorial, -] - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/file_save.py b/scripts-blender/addons/blender_kitsu/shot_builder/file_save.py new file mode 100644 index 00000000..49ac105f --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/file_save.py @@ -0,0 +1,12 @@ +from pathlib import Path +import bpy + + +def save_shot_builder_file(file_path: str): + """Save Shot File within Folder of matching name. + Set Shot File to relative Paths.""" + if Path(file_path).exists(): + raise Exception(f"Cannot Overwrite Existing Shot File {file_path}") + dir_path = Path(file_path).parent + dir_path.mkdir(parents=True, exist_ok=True) + bpy.ops.wm.save_mainfile(filepath=file_path, relative_remap=True) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/hook_examples/hooks.py b/scripts-blender/addons/blender_kitsu/shot_builder/hook_examples/hooks.py new file mode 100644 index 00000000..b9efe7d9 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/hook_examples/hooks.py @@ -0,0 +1,51 @@ +import bpy + +from blender_kitsu.shot_builder.hooks import hook +from blender_kitsu.types import Shot, Asset + +import logging + +''' +Arguments to use in hooks + scene: bpy.types.Scene # current scene + shot: Shot class from blender_kitsu.types.py + prod_path: str # path to production root dir (your_project/svn/) + shot_path: str # path to shot file (your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_task_name}.blend}) + +Notes + matching_task_type = ['anim', 'lighting', 'fx', 'comp'] # either use list or just one string + output_col_name = shot.get_output_collection_name(task_type_short_name="anim") + + + + +''' + +logger = logging.getLogger(__name__) + +# ---------- Global Hook ---------- + + +@hook() +def set_eevee_render_engine(scene: bpy.types.Scene, **kwargs): + """ + By default, we set EEVEE as the renderer. + """ + scene.render.engine = 'BLENDER_EEVEE' + print("HOOK SET RENDER ENGINE") + + +# ---------- Overrides for animation files ---------- + + +@hook(match_task_type='anim') +def test_args( + scene: bpy.types.Scene, shot: Shot, prod_path: str, shot_path: str, **kwargs +): + """ + Set output parameters for animation rendering + """ + print(f"Scene = {scene.name}") + print(f"Shot = {shot.name}") + print(f"Prod Path = {prod_path}") + print(f"Shot Path = {shot_path}") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/hooks.py b/scripts-blender/addons/blender_kitsu/shot_builder/hooks.py index 0dd27478..4496f101 100644 --- a/scripts-blender/addons/blender_kitsu/shot_builder/hooks.py +++ b/scripts-blender/addons/blender_kitsu/shot_builder/hooks.py @@ -1,8 +1,14 @@ +import sys +import pathlib +from typing import * + import typing import types from collections.abc import Iterable - +import importlib +from .. import prefs import logging + logger = logging.getLogger(__name__) @@ -44,6 +50,8 @@ HookFunction = typing.Callable[[typing.Any], None] def _match_hook_parameter( hook_criteria: MatchCriteriaType, match_query: typing.Optional[str] ) -> bool: + if hook_criteria == None: + return True if hook_criteria == DoNotMatch: return match_query is None if hook_criteria == Wildcard: @@ -60,10 +68,6 @@ class Hooks: def __init__(self): self._hooks: typing.List[HookFunction] = [] - def register(self, func: HookFunction) -> None: - logger.info(f"registering hook '{func.__name__}'") - self._hooks.append(func) - def matches( self, hook: HookFunction, @@ -73,7 +77,6 @@ class Hooks: ) -> bool: assert not kwargs rules = typing.cast(MatchingRulesType, getattr(hook, '_shot_builder_rules')) - return all( ( _match_hook_parameter(rules['match_task_type'], match_task_type), @@ -86,32 +89,54 @@ class Hooks: if self.matches(hook=hook, **kwargs): yield hook + def execute_hooks( + self, match_task_type: str = None, match_asset_type: str = None, *args, **kwargs + ) -> None: + for hook in self._hooks: + if self.matches( + hook, match_task_type=match_task_type, match_asset_type=match_asset_type + ): + hook(*args, **kwargs) -def _register_hook(func: types.FunctionType) -> None: - from .project import get_active_production + def load_hooks(self, context): + root_dir = prefs.project_root_dir_get(context) + shot_builder_config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder") + if not shot_builder_config_dir.exists(): + raise Exception("Shot Builder Hooks directory does not exist") + paths = [shot_builder_config_dir.resolve().__str__()] + with SystemPathInclude(paths) as _include: + try: + import hooks as production_hooks - production = get_active_production() - production.hooks.register(func) + importlib.reload(production_hooks) + self.register_hooks(production_hooks) + except ModuleNotFoundError: + raise Exception("Production has no `hooks.py` configuration file") + return False -def register_hooks(module: types.ModuleType) -> None: - """ - Register all hooks inside the given module. - """ - for module_item_str in dir(module): - module_item = getattr(module, module_item_str) - if not isinstance(module_item, types.FunctionType): - continue - if module_item.__module__ != module.__name__: - continue - if not hasattr(module_item, "_shot_builder_rules"): - continue - _register_hook(module_item) + def register(self, func: HookFunction) -> None: + logger.info(f"registering hook '{func.__name__}'") + self._hooks.append(func) + + def register_hooks(self, module: types.ModuleType) -> None: + """ + Register all hooks inside the given module. + """ + for module_item_str in dir(module): + module_item = getattr(module, module_item_str) + if not isinstance(module_item, types.FunctionType): + continue + if module_item.__module__ != module.__name__: + continue + if not hasattr(module_item, "_shot_builder_rules"): + continue + self.register(module_item) def hook( - match_task_type: MatchCriteriaType = DoNotMatch, - match_asset_type: MatchCriteriaType = DoNotMatch, + match_task_type: MatchCriteriaType = None, + match_asset_type: MatchCriteriaType = None, ) -> typing.Callable[[types.FunctionType], types.FunctionType]: """ Decorator to add custom logic when building a shot. @@ -128,3 +153,41 @@ def hook( return func return wrapper + + +class SystemPathInclude: + """ + Resource class to temporary include system paths to `sys.paths`. + + Usage: + ``` + paths = [pathlib.Path("/home/guest/my_python_scripts")] + with SystemPathInclude(paths) as t: + import my_module + reload(my_module) + ``` + + It is possible to nest multiple SystemPathIncludes. + """ + + def __init__(self, paths_to_add: List[pathlib.Path]): + # TODO: Check if all paths exist and are absolute. + self.__paths = paths_to_add + self.__original_sys_path: List[str] = [] + + def __enter__(self): + self.__original_sys_path = sys.path + new_sys_path = [] + for path_to_add in self.__paths: + # Do not add paths that are already in the sys path. + # Report this to the logger as this might indicate wrong usage. + path_to_add_str = str(path_to_add) + if path_to_add_str in self.__original_sys_path: + logger.warn(f"{path_to_add_str} already added to `sys.path`") + continue + new_sys_path.append(path_to_add_str) + new_sys_path.extend(self.__original_sys_path) + sys.path = new_sys_path + + def __exit__(self, exc_type, exc_value, exc_traceback): + sys.path = self.__original_sys_path diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/operators.py b/scripts-blender/addons/blender_kitsu/shot_builder/operators.py deleted file mode 100644 index d566ad74..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/operators.py +++ /dev/null @@ -1,298 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# -import pathlib -from typing import * -import bpy -import gazu -from .shot import ShotRef -from .project import ( - ensure_loaded_production, - get_active_production, -) -from .builder import ShotBuilder -from .task_type import TaskType -from .. import prefs, cache -from .anim_setup.core import ( - animation_workspace_delete_others, - animation_workspace_vse_area_add, -) -from .editorial.core import editorial_export_get_latest -from .builder.save_file import save_shot_builder_file - - -_production_task_type_items: List[Tuple[str, str, str]] = [] - - -def production_task_type_items( - self: Any, context: bpy.types.Context -) -> List[Tuple[str, str, str]]: - global _production_task_type_items - return _production_task_type_items - - -_production_seq_id_items: List[Tuple[str, str, str]] = [] - - -def production_seq_id_items( - self: Any, context: bpy.types.Context -) -> List[Tuple[str, str, str]]: - global _production_seq_id_items - return _production_seq_id_items - - -_production_shots: List[ShotRef] = [] - - -def production_shots( - self: Any, context: bpy.types.Context -) -> List[Tuple[str, str, str]]: - global _production_shots - return _production_shots - - -_production_shot_id_items_for_seq: List[Tuple[str, str, str]] = [] - - -def production_shot_id_items_for_seq( - self: Any, context: bpy.types.Context -) -> List[Tuple[str, str, str]]: - global _production_shot_id_items_for_seq - global _production_shot_id_items - - if not self.seq_id or not _production_shots: - return [] - - shots_for_seq: List[Tuple(str, str, str)] = [ - (s.name, s.name, "") - for s in _production_shots - if s.sequence.name == self.seq_id - ] - - _production_shot_id_items_for_seq.clear() - _production_shot_id_items_for_seq.extend(shots_for_seq) - - return _production_shot_id_items_for_seq - - -def reset_shot_id_enum(self: Any, context: bpy.types.Context) -> None: - production_shot_id_items_for_seq(self, context) - global _production_shot_id_items_for_seq - if _production_shot_id_items_for_seq: - self.shot_id = _production_shot_id_items_for_seq[0][0] - - -class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): - """Build a new shot file""" - - bl_idname = "shotbuilder.new_shot_file" - bl_label = "New Production Shot File" - - _timer = None - _built_shot = False - _add_vse_area = False - _file_path = '' - - production_root: bpy.props.StringProperty( # type: ignore - name="Production Root", description="Root of the production", subtype='DIR_PATH' - ) - - production_name: bpy.props.StringProperty( # type: ignore - name="Production", - description="Name of the production to create a shot file for", - options=set(), - ) - - seq_id: bpy.props.EnumProperty( # type: ignore - name="Sequence ID", - description="Sequence ID of the shot to build", - items=production_seq_id_items, - update=reset_shot_id_enum, - ) - - shot_id: bpy.props.EnumProperty( # type: ignore - name="Shot ID", - description="Shot ID of the shot to build", - items=production_shot_id_items_for_seq, - ) - - task_type: bpy.props.EnumProperty( # type: ignore - name="Task", - description="Task to create the shot file for", - items=production_task_type_items, - ) - auto_save: bpy.props.BoolProperty( - name="Save after building.", - description="Automatically save build file after 'Shot Builder' is complete.", - default=True, - ) - - def modal(self, context, event): - if event.type == 'TIMER' and not self._add_vse_area: - # Show Storyboard/Animatic from VSE - """Running as Modal Event because functions within execute() function like - animation_workspace_delete_others() changed UI context that needs to be refreshed. - https://docs.blender.org/api/current/info_gotcha.html#no-updates-after-changing-ui-context - """ - # TODO this is a hack, should be inherient to above builder - # TODO fix during refactor - if self.task_type == 'anim': - animation_workspace_vse_area_add(context) - self._add_vse_area = True - - if self._built_shot and self._add_vse_area: - if self.auto_save: - file_path = pathlib.Path() - try: - save_shot_builder_file(self._file_path) - self.report( - {"INFO"}, f"Saved Shot{self.shot_id} at {self._file_path}" - ) - return {'FINISHED'} - except FileExistsError: - self.report( - {"ERROR"}, - f"Cannot create a file/folder when that file/folder already exists {file_path}", - ) - return {'CANCELLED'} - self.report({"INFO"}, f"Built Shot {self.shot_id}, file is not saved!") - return {'FINISHED'} - - return {'PASS_THROUGH'} - - def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: - addon_prefs = prefs.addon_prefs_get(bpy.context) - project = cache.project_active_get() - - 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 directiory is configured in 'Blender Kitsu' addon preferences.", - ) - return {'CANCELLED'} - - self.production_root = addon_prefs.project_root_dir - self.production_name = project.name - - if not ensure_loaded_production(context): - self.report( - {'ERROR'}, - "Shot builder configuration files not found in current project directory. \nCheck addon preferences to ensure project root contains shot_builder config.", - ) - return {'CANCELLED'} - - production = get_active_production() - - global _production_task_type_items - _production_task_type_items = production.get_task_type_items(context=context) - - global _production_seq_id_items - _production_seq_id_items = production.get_seq_items(context=context) - - global _production_shots - _production_shots = production.get_shots(context=context) - - return cast( - Set[str], context.window_manager.invoke_props_dialog(self, width=400) - ) - - def execute(self, context: bpy.types.Context) -> Set[str]: - addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences - wm = context.window_manager - self._timer = wm.event_timer_add(0.1, window=context.window) - wm.modal_handler_add(self) - if not self.production_root: - self.report( - {'ERROR'}, - "Shot builder can only be started from the File menu. Shortcuts like CTRL-N don't work", - ) - return {'CANCELLED'} - - if self._built_shot: - return {'RUNNING_MODAL'} - ensure_loaded_production(context) - production = get_active_production() - shot_builder = ShotBuilder( - context=context, - production=production, - shot_name=self.shot_id, - task_type=TaskType(self.task_type), - ) - shot_builder.create_build_steps() - shot_builder.build() - - active_project = cache.project_active_get() - - # Build Kitsu Context - sequence = gazu.shot.get_sequence_by_name(active_project.id, self.seq_id) - shot = gazu.shot.get_shot_by_name(sequence, self.shot_id) - - # TODO this is a hack, should be inherient to above builder - # TODO fix during refactor - if self.task_type == 'anim': - # Load EDIT - editorial_export_get_latest(context, shot) - # Load Anim Workspace - animation_workspace_delete_others() - - # Initilize armatures - for obj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]: - base_name = obj.name.split(addon_prefs.shot_builder_armature_prefix)[-1] - new_action = bpy.data.actions.new( - f"{addon_prefs.shot_builder_action_prefix}{base_name}.{self.shot_id}.v001" - ) - new_action.use_fake_user = True - obj.animation_data.action = new_action - - # Set Shot Frame Range - context.scene.frame_start = int(shot["data"].get("3d_start")) - context.scene.frame_end = ( - int(shot["data"].get("3d_start")) + shot.get('nb_frames') - 1 - ) - - # Run User Script - exec(addon_prefs.user_exec_code) - - self._file_path = shot_builder.build_context.shot.file_path - self._built_shot = True - return {'RUNNING_MODAL'} - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - row = layout.row() - row.enabled = False - row.prop(self, "production_name") - layout.prop(self, "seq_id") - layout.prop(self, "shot_id") - layout.prop(self, "task_type") - layout.prop(self, "auto_save") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/ops.py b/scripts-blender/addons/blender_kitsu/shot_builder/ops.py new file mode 100644 index 00000000..b1b9cdba --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/ops.py @@ -0,0 +1,271 @@ +import bpy +from .. import bkglobals +from pathlib import Path +from typing import List, Any, Tuple, Set, cast +from .. import prefs, cache +from .core import ( + set_render_engine, + link_camera_rig, + create_task_type_output_collection, + set_shot_scene, + set_resolution_and_fps, + set_frame_range, + link_task_type_output_collections, + remove_all_data, +) + +from .editorial import editorial_export_get_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_save_shot_builder_hooks(bpy.types.Operator): + bl_idname = "kitsu.save_shot_builder_hooks" + bl_label = "Save Shot Builder Hook File" + bl_description = "Save hook.py file to `your_project/svn/pro/assets/scripts/shot-builder` directory. Hooks are used to customize shot builder behaviour." + + def execute(self, context: bpy.types.Context): + addon_prefs = prefs.addon_prefs_get(context) + project = cache.project_active_get() + 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 directiory is configured in 'Blender Kitsu' addon preferences.", + ) + return {'CANCELLED'} + + root_dir = prefs.project_root_dir_get(context) + config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder") + if not config_dir.exists(): + config_dir.mkdir(parents=True) + hook_file_path = config_dir.joinpath("hooks.py") + if hook_file_path.exists(): + self.report( + {'WARNING'}, + "File already exists, cannot overwrite", + ) + return {'CANCELLED'} + + config_dir = Path(__file__).parent + example_hooks_path = config_dir.joinpath("hook_examples/hooks.py") + if not example_hooks_path.exists(): + self.report( + {'ERROR'}, + "Cannot find example hook file", + ) + return {'CANCELLED'} + + with example_hooks_path.open() as source: + # Read contents + contents = source.read() + + # Write contents to target file + with hook_file_path.open('w') as target: + target.write(contents) + self.report({'INFO'}, f"Hook File saved to {hook_file_path}") + return {'FINISHED'} + + +class KITSU_OT_build_new_shot(bpy.types.Operator): + bl_idname = "kitsu.build_new_shot" + bl_label = "Build New Shot" + bl_description = "Build a New Shot file, based on infromation from KITSU Server" + bl_options = {"REGISTER"} + + _timer = None + _built_shot = False + _add_vse_area = False + _file_path = '' + production_name: bpy.props.StringProperty( # type: ignore + name="Production", + description="Name of the production to create a shot file for", + options=set(), + ) + + seq_id: bpy.props.EnumProperty( + name="Sequence ID", + description="Sequence ID of the shot to build", + items=cache.get_sequences_enum_list, + ) + + shot_id: bpy.props.EnumProperty( + name="Shot ID", + description="Shot ID of the shot to build", + items=get_shots_for_seq, + ) + + task_type: bpy.props.EnumProperty( + name="Task", + description="Task to create the shot file for", + items=get_tasks_for_shot, + ) + + save_file: bpy.props.BoolProperty( + name="Save after building.", + description="Automatically save build file after 'Shot Builder' is complete.", + default=True, + ) + + def draw(self, context: bpy.types.Context) -> None: + layout = self.layout + row = layout.row() + row.enabled = False + row.prop(self, "production_name") + layout.prop(self, "seq_id") + layout.prop(self, "shot_id") + layout.prop(self, "task_type") + layout.prop(self, "save_file") + + def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: + global active_project + 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 directiory 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 _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 execute(self, context: bpy.types.Context): + # Get Properties + global active_project + seq = active_project.get_sequence(self.seq_id) + shot = active_project.get_shot(self.shot_id) + task_type = self._get_task_type_for_shot(context, shot) + task_type_short_name = task_type.get_short_name() + shot_file_path_str = shot.get_shot_filepath(context, task_type_short_name) + + # Open Template File + replace_workspace_with_template(context, task_type_short_name) + + # Set Up Scene + Naming + shot_task_name = shot.get_shot_task_name(task_type.get_short_name()) + scene = set_shot_scene(context, shot_task_name) + remove_all_data() + set_resolution_and_fps(active_project, scene) + set_frame_range(shot, scene) + + # Set Render Settings + if task_type_short_name == 'anim': # TODO get anim from a constant instead + set_render_engine(context.scene, 'BLENDER_WORKBENCH') + else: + set_render_engine(context.scene) + + # Create Output Collection & Link Camera + if bkglobals.OUTPUT_COL_CREATE.get(task_type_short_name): + output_col = create_task_type_output_collection( + context.scene, shot, task_type + ) + if task_type_short_name == 'anim': + link_camera_rig(context.scene, output_col) + + # Load Assets + get_shot_assets(scene=scene, output_collection=output_col, shot=shot) + + # Link External Output Collections + link_task_type_output_collections(shot, task_type) + + if bkglobals.LOAD_EDITORIAL_REF.get(task_type_short_name): + editorial_export_get_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, + ) + + # Save File + if self.save_file: + save_shot_builder_file(file_path=shot_file_path_str) + + self.report( + {"INFO"}, f"Successfully Built Shot:`{shot.name}` Task: `{task_type.name}`" + ) + return {"FINISHED"} + + +classes = (KITSU_OT_build_new_shot, KITSU_OT_save_shot_builder_hooks) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/project.py b/scripts-blender/addons/blender_kitsu/shot_builder/project.py deleted file mode 100644 index 2cd71cb3..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/project.py +++ /dev/null @@ -1,460 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -import importlib -from collections import defaultdict - -import bpy - -from .task_type import * -from .shot import Shot, ShotRef -from .render_settings import RenderSettings -from .asset import Asset, AssetRef -from .sys_utils import * -from .hooks import Hooks, register_hooks -from .connectors.default import DefaultConnector -from .connectors.connector import Connector -import os - -from .. import prefs -from pathlib import Path - -from typing import * -import types - -logger = logging.getLogger(__name__) - - -class Production: - """ - Class containing data and methods for a production. - - # Data members # - path: contains the path to the root of the production. - task_types: contains a list of `TaskType`s or a Connector to retrieve that are defined for this - production. By default the task_types are prefilled with anim and light. - name: human readable name of the production. - - """ - - __ATTRNAMES_SUPPORTING_CONNECTOR = ['task_types', 'shots', 'name'] - - def __init__(self, production_path: pathlib.Path): - self.path = production_path - self.task_types: List[TaskType] = [] - self.task_types_connector = DefaultConnector - self.shots_connector = DefaultConnector - self.assets: List[type] = [] - self.shots: List[Shot] = [] - self.name = "" - self.name_connector = DefaultConnector - self.render_settings_connector = DefaultConnector - self.config: Dict[str, Any] = {} - self.__shot_lookup: Dict[str, Shot] = {} - self.hooks: Hooks = Hooks() - self.shot_data_synced = False - - self.scene_name_format = "{shot.sequence_code}_{shot.code}.{task_type}" - self.shot_name_format = "{shot.sequence_code}_{shot.code}" - self.file_name_format = ( - "{production.path}shots/{shot.code}/{shot.code}.{task_type}.blend" - ) - - def __create_connector( - self, connector_cls: Type[Connector], context: bpy.types.Context - ) -> Connector: - # TODO: Cache connector - preferences = context.preferences.addons["blender_kitsu"].preferences - return connector_cls(production=self, preferences=preferences) - - def __format_shot_name(self, shot: Shot) -> str: - return self.shot_name_format.format(shot=shot) - - def get_task_type_items( - self, context: bpy.types.Context - ) -> List[Tuple[str, str, str]]: - """ - Get the list of task types items to be used in an item function of a - `bpy.props.EnumProperty` - """ - if not self.task_types: - connector = self.__create_connector( - self.task_types_connector, context=context - ) - self.task_types = connector.get_task_types() - return [ - (task_type.name, task_type.name, task_type.name) - for task_type in self.task_types - ] - - def get_assets_for_shot( - self, context: bpy.types.Context, shot: Shot - ) -> List[AssetRef]: - connector = self.__create_connector(self.shots_connector, context=context) - - return connector.get_assets_for_shot(shot) - - def get_shots(self, context: bpy.types.Context) -> List[ShotRef]: - connector = self.__create_connector(self.shots_connector, context=context) - return connector.get_shots() - - def get_shot(self, context: bpy.types.Context, shot_name: str) -> Optional[Shot]: - self._ensure_shot_data(context) - - for shot in self.shots: - if shot.name == shot_name: - return shot - return None - - def _ensure_shot_data(self, context: bpy.types.Context) -> None: - if self.shot_data_synced: - return - # Find a generic shot definition. This class will be used as template - # when no specific shot definition could be found. - generic_shot_class = None - for shot in self.shots: - if shot.is_generic: - generic_shot_class = shot.__class__ - break - - shot_refs = self.get_shots(context) - for shot_ref in shot_refs: - logger.debug(f"Finding shot definition for {shot_ref.name}") - for shot in self.shots: - if shot.name == shot_ref.name: - logger.debug(f"Shot definition found for {shot_ref.name}") - shot_ref.sync_data(shot) - break - else: - logger.info(f"No shot definition found for {shot_ref.name}") - if generic_shot_class: - logger.info(f"Using generic shot class") - shot = generic_shot_class() - shot_ref.sync_data(shot) - shot.is_generic = False - self.shots.append(shot) - - self.shot_data_synced = True - - def get_render_settings( - self, context: bpy.types.Context, shot: Shot - ) -> RenderSettings: - connector = self.__create_connector(self.shots_connector, context=context) - return connector.get_render_settings(shot) - - def get_shot_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]: - """ - Get the list of shot items to be used in an item function of a - `bpy.props.EnumProperty` to select a shot. - """ - result = [] - self._ensure_shot_data(context) - sequences: Dict[str, List[Shot]] = defaultdict(list) - for shot in self.shots: - if not shot.is_valid(): - continue - sequences[shot.sequence_code].append(shot) - - sorted_sequences = sorted(sequences.keys()) - for sequence in sorted_sequences: - result.append(("", sequence, sequence)) - for shot in sorted(sequences[sequence], key=lambda x: x.name): - result.append((shot.name, self.__format_shot_name(shot), shot.name)) - - return result - - def get_seq_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]: - """ - Get the list of seq items to be used in an item function of a - `bpy.props.EnumProperty` to select a shot. - """ - shots = self.get_shots(context) - sequences = list(set([s.sequence for s in shots])) - sequences.sort(key=lambda seq: seq.name) - - return [(seq.name, seq.name, "") for seq in sequences] - - def get_name(self, context: bpy.types.Context) -> str: - """ - Get the name of the production - """ - if not self.name: - connector = self.__create_connector(self.name_connector, context=context) - self.name = connector.get_name() - return self.name - - # TODO: Use visitor pattern. - def __load_name(self, main_config_mod: types.ModuleType) -> None: - name = getattr(main_config_mod, "PRODUCTION_NAME", None) - if name is None: - return - - # Extract task types from a list of strings - if isinstance(name, str): - self.name = name - return - - if issubclass(name, Connector): - self.name = "" - self.name_connector = name - return - - logger.warn( - "Skip loading of production name. Incorrect configuration detected." - ) - - def __load_task_types(self, main_config_mod: types.ModuleType) -> None: - task_types = getattr(main_config_mod, "TASK_TYPES", None) - if task_types is None: - return - - # Extract task types from a list of strings - if isinstance(task_types, list): - self.task_types = [TaskType(task_type) for task_type in task_types] - return - - if issubclass(task_types, Connector): - self.task_types = task_types - - logger.warn("Skip loading of task_types. Incorrect configuration detected.") - - def __load_shots_connector(self, main_config_mod: types.ModuleType) -> None: - shots = getattr(main_config_mod, "SHOTS", None) - if shots is None: - return - - # Extract task types from a list of strings - if issubclass(shots, Connector): - self.shots_connector = shots - return - - logger.warn("Skip loading of shots. Incorrect configuration detected.") - - def __load_connector_keys(self, main_config_mod: types.ModuleType) -> None: - connectors = set() - for attrname in Production.__ATTRNAMES_SUPPORTING_CONNECTOR: - connector = getattr(self, f"{attrname}_connector") - connectors.add(connector) - - connector_keys = set() - for connector in connectors: - for key in connector.PRODUCTION_KEYS: - connector_keys.add(key) - - for connector_key in connector_keys: - if hasattr(main_config_mod, connector_key): - self.config[connector_key] = getattr(main_config_mod, connector_key) - - def __load_render_settings(self, main_config_mod: types.ModuleType) -> None: - render_settings = getattr(main_config_mod, "RENDER_SETTINGS", None) - if render_settings is None: - return - - if issubclass(render_settings, Connector): - self.render_settings_connector = render_settings - return - - logger.warn("Skip loading of render settings. Incorrect configuration detected") - - def __load_formatting_strings(self, main_config_mod: types.ModuleType) -> None: - self.shot_name_format = getattr( - main_config_mod, "SHOT_NAME_FORMAT", self.scene_name_format - ) - self.scene_name_format = getattr( - main_config_mod, "SCENE_NAME_FORMAT", self.scene_name_format - ) - self.file_name_format = getattr( - main_config_mod, "FILE_NAME_FORMAT", self.file_name_format - ) - - def _load_config(self, main_config_mod: types.ModuleType) -> None: - self.__load_name(main_config_mod) - self.__load_task_types(main_config_mod) - self.__load_shots_connector(main_config_mod) - self.__load_connector_keys(main_config_mod) - self.__load_render_settings(main_config_mod) - self.__load_formatting_strings(main_config_mod) - - def _load_asset_definitions(self, asset_mod: types.ModuleType) -> None: - """ - Load all assets from the given module. - """ - self.assets = [] - for module_item_str in dir(asset_mod): - module_item = getattr(asset_mod, module_item_str) - if module_item.__class__ != type: - continue - if not issubclass(module_item, Asset): - continue - if not hasattr(module_item, "name"): - continue - logger.info(f"loading asset config {module_item}") - self.assets.append(module_item) - # TODO: only add assets that are leaves - - def _load_shot_definitions(self, shot_mod: types.ModuleType) -> None: - """ - Load all assets from the given module. - """ - self.shots = [] - for module_item_str in dir(shot_mod): - module_item = getattr(shot_mod, module_item_str) - if module_item.__class__ != type: - continue - if not issubclass(module_item, Shot): - continue - if not hasattr(module_item, "name"): - continue - logger.info(f"loading shot config {module_item}") - self.shots.append(module_item()) - - -_PRODUCTION: Optional[Production] = None - - -def is_valid_production_root(path: pathlib.Path) -> bool: - """ - Test if the given project path is configured correctly. - - A valid project path contains a subfolder with the name `shot-builder` - holding configuration files. - """ - if not path.is_absolute(): - return False - if not path.exists(): - return False - if not path.is_dir(): - return False - config_file_path = get_production_config_file_path(path) - return config_file_path.exists() - - -def get_production_config_dir_path(path: pathlib.Path) -> pathlib.Path: - """ - Get the production configuration dir path. - """ - return path / "shot-builder" - - -def get_production_config_file_path(path: pathlib.Path) -> pathlib.Path: - """ - Get the production configuration file path. - """ - return get_production_config_dir_path(path) / "config.py" - - -def _find_production_root(path: pathlib.Path) -> Optional[pathlib.Path]: - """ - Given a path try to find the production root - """ - if is_valid_production_root(path): - return path - try: - parent_path = path.parents[0] - return _find_production_root(parent_path) - except IndexError: - return None - - -# TODO: return type is optional -def get_production_root(context: bpy.types.Context) -> Optional[pathlib.Path]: - """ - Determine the project root based on the current file. - When current file isn't part of a project the project root - configured in the add-on will be used. - """ - current_file = pathlib.Path(bpy.data.filepath) - production_root = _find_production_root(current_file) - if production_root: - return production_root - - addon_prefs = prefs.addon_prefs_get(bpy.context) - production_root = Path(addon_prefs.project_root_dir) - if is_valid_production_root(production_root): - return production_root - return None - - -def ensure_loaded_production(context: bpy.types.Context) -> bool: - """ - Ensure that the production of the current context is loaded. - - Returns if the production of for the given context is loaded. - """ - global _PRODUCTION - addon_prefs = prefs.addon_prefs_get(bpy.context) - base_path = Path(addon_prefs.project_root_dir) - production_root = os.path.join( - base_path, "pro" - ) # TODO Fix during refactor should use base_path - if is_valid_production_root(Path(production_root)): - logger.debug(f"loading new production configuration from '{production_root}'.") - __load_production_configuration(context, Path(production_root)) - return True - return False - - -def __load_production_configuration( - context: bpy.types.Context, production_path: pathlib.Path -) -> bool: - global _PRODUCTION - _PRODUCTION = Production(production_path) - paths = [production_path / "shot-builder"] - with SystemPathInclude(paths) as _include: - try: - import config as production_config - - importlib.reload(production_config) - _PRODUCTION._load_config(production_config) - except ModuleNotFoundError: - logger.warning("Production has no `config.py` configuration file") - - try: - import shots as production_shots - - importlib.reload(production_shots) - _PRODUCTION._load_shot_definitions(production_shots) - except ModuleNotFoundError: - logger.warning("Production has no `shots.py` configuration file") - - try: - import assets as production_assets - - importlib.reload(production_assets) - _PRODUCTION._load_asset_definitions(production_assets) - except ModuleNotFoundError: - logger.warning("Production has no `assets.py` configuration file") - - try: - import hooks as production_hooks - - importlib.reload(production_hooks) - register_hooks(production_hooks) - except ModuleNotFoundError: - logger.warning("Production has no `hooks.py` configuration file") - pass - - return False - - -def get_active_production() -> Production: - global _PRODUCTION - assert _PRODUCTION - return _PRODUCTION diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/render_settings.py b/scripts-blender/addons/blender_kitsu/shot_builder/render_settings.py deleted file mode 100644 index 1a396d95..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/render_settings.py +++ /dev/null @@ -1,29 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -from ..shot_builder.asset import Asset -from typing import * - - -class RenderSettings: - def __init__(self, width: int, height: int, frames_per_second: float): - self.width = width - self.height = height - self.frames_per_second = frames_per_second diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/shot.py b/scripts-blender/addons/blender_kitsu/shot_builder/shot.py deleted file mode 100644 index 8416f641..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/shot.py +++ /dev/null @@ -1,64 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -import typing - - -class Shot: - is_generic = False - kitsu_id = "" - sequence_code = "" - name = "" - code = "" - frame_start = 0 - frames = 0 - # Frame_end will be stored for debugging only. - frame_end = 0 - frames_per_second = 24.0 - file_path_format = "{production.path}/shots/{shot.sequence_code}/{shot.name}/{shot.name}.{task_type}.blend" - file_path = "" - - def is_valid(self) -> bool: - """ - Check if this shot contains all data so it could be selected - for shot building. - - When not valid it won't be shown in the shot selection field. - """ - if not self.name: - return False - - if self.frames <= 0: - return False - - return True - - -class ShotRef: - """ - Reference to an asset from an external system. - """ - - def __init__(self, name: str = "", code: str = ""): - self.name = name - self.code = code - - def sync_data(self, shot: Shot) -> None: - pass diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/sys_utils.py b/scripts-blender/addons/blender_kitsu/shot_builder/sys_utils.py deleted file mode 100644 index ee023d33..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/sys_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -import sys -import pathlib -import logging -from typing import * - -logger = logging.getLogger(__name__) - - -class SystemPathInclude: - """ - Resource class to temporary include system paths to `sys.paths`. - - Usage: - ``` - paths = [pathlib.Path("/home/guest/my_python_scripts")] - with SystemPathInclude(paths) as t: - import my_module - reload(my_module) - ``` - - It is possible to nest multiple SystemPathIncludes. - """ - - def __init__(self, paths_to_add: List[pathlib.Path]): - # TODO: Check if all paths exist and are absolute. - self.__paths = paths_to_add - self.__original_sys_path: List[str] = [] - - def __enter__(self): - self.__original_sys_path = sys.path - new_sys_path = [] - for path_to_add in self.__paths: - # Do not add paths that are already in the sys path. - # Report this to the logger as this might indicate wrong usage. - path_to_add_str = str(path_to_add) - if path_to_add_str in self.__original_sys_path: - logger.warn(f"{path_to_add_str} already added to `sys.path`") - continue - new_sys_path.append(path_to_add_str) - new_sys_path.extend(self.__original_sys_path) - sys.path = new_sys_path - - def __exit__(self, exc_type, exc_value, exc_traceback): - sys.path = self.__original_sys_path diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/task_type.py b/scripts-blender/addons/blender_kitsu/shot_builder/task_type.py deleted file mode 100644 index 21fe9b6e..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/task_type.py +++ /dev/null @@ -1,27 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - - -class TaskType: - def __init__(self, task_name: str): - self.name = task_name - - def __str__(self) -> str: - return self.name diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/template.py b/scripts-blender/addons/blender_kitsu/shot_builder/template.py new file mode 100644 index 00000000..f7fb122b --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/template.py @@ -0,0 +1,59 @@ +import bpy +from pathlib import Path +from .core import link_data_block + + +# TODO add ability for custom templates +def get_template_dir() -> Path: + return Path(__file__).absolute().parent.joinpath("templates") + + +def get_template_files() -> list[Path]: + dir = get_template_dir() + return list(dir.glob('*.blend')) + + +def get_template_for_task_type(task_type_short_name: str) -> Path: + for file in get_template_files(): + if file.stem == task_type_short_name: + return file + + +def replace_workspace_with_template( + context: bpy.types.Context, task_type_short_name: str +): + file_path = get_template_for_task_type(task_type_short_name).resolve().absolute() + remove_prefix = "REMOVE-" + if not file_path.exists(): + return + + # Mark Existing Workspaces for Removal + for workspace in bpy.data.workspaces: + if workspace.name.startswith(remove_prefix): + continue + workspace.name = remove_prefix + workspace.name + + file_path_str = file_path.__str__() + with bpy.data.libraries.load(file_path_str) as (data_from, data_to): + for workspace in data_from.workspaces: + bpy.ops.wm.append( + filepath=file_path_str, + directory=file_path_str + "/" + 'WorkSpace', + filename=str(workspace), + ) + + for lib in bpy.data.libraries: + if lib.filepath == file_path_str: + bpy.data.libraries.remove(bpy.data.libraries.get(lib.name)) + break + + workspaces_to_remove = [] + for workspace in bpy.data.workspaces: + if workspace.name.startswith(remove_prefix): + workspaces_to_remove.append(workspace) + + # context.window.workspace = workspace + for workspace in workspaces_to_remove: + with context.temp_override(workspace=workspace): + bpy.ops.workspace.delete() + return True diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/templates/anim.blend b/scripts-blender/addons/blender_kitsu/shot_builder/templates/anim.blend new file mode 100644 index 00000000..4aa9fd43 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/templates/anim.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd32511d5d9cd143192271c82dd64bd75428e1fad5ecf340e59bd192c1e6a938 +size 976636 diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/templates/comp.blend b/scripts-blender/addons/blender_kitsu/shot_builder/templates/comp.blend new file mode 100644 index 00000000..56908010 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/templates/comp.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6516c83b7fdb0b9107ce03a3249887cd321f3559ddf1bad8a0dbe020a678b833 +size 660896 diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/templates/fx.blend b/scripts-blender/addons/blender_kitsu/shot_builder/templates/fx.blend new file mode 100644 index 00000000..51bbc8a2 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/templates/fx.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9191ac4d29d8e8276f08a48e2702cffa66eb02f2ddf27bc47857db6998d87056 +size 667192 diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/templates/lighting.blend b/scripts-blender/addons/blender_kitsu/shot_builder/templates/lighting.blend new file mode 100644 index 00000000..2f341783 --- /dev/null +++ b/scripts-blender/addons/blender_kitsu/shot_builder/templates/lighting.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c658f9c8099fc1a7114cee942d94f2627b5b631abb9c06fba781bee93489ae1e +size 229732 diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/ui.py b/scripts-blender/addons/blender_kitsu/shot_builder/ui.py index 15b72ff8..6b0c701b 100644 --- a/scripts-blender/addons/blender_kitsu/shot_builder/ui.py +++ b/scripts-blender/addons/blender_kitsu/shot_builder/ui.py @@ -1,27 +1,7 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# import bpy -from typing import * -from .operators import * +from typing import Any def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None: layout = self.layout - op = layout.operator(SHOTBUILDER_OT_NewShotFile.bl_idname, text="Shot File") + op = layout.operator("kitsu.build_new_shot", text="Shot File") diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/vars.py b/scripts-blender/addons/blender_kitsu/shot_builder/vars.py deleted file mode 100644 index 397f8302..00000000 --- a/scripts-blender/addons/blender_kitsu/shot_builder/vars.py +++ /dev/null @@ -1 +0,0 @@ -DEFAULT_FRAME_START: int = 101 diff --git a/scripts-blender/addons/blender_kitsu/types.py b/scripts-blender/addons/blender_kitsu/types.py index 683fdd4c..3b147414 100644 --- a/scripts-blender/addons/blender_kitsu/types.py +++ b/scripts-blender/addons/blender_kitsu/types.py @@ -22,9 +22,11 @@ from __future__ import annotations import inspect from dataclasses import asdict, dataclass, field from typing import Any, Dict, List, Optional, Union, Tuple, TypeVar - +from pathlib import Path import gazu from .logger import LoggerFactory +from . import bkglobals +from . import prefs logger = LoggerFactory.getLogger() @@ -558,6 +560,11 @@ class Shot(Entity): def get_all_tasks(self) -> List[Task]: return [Task.from_dict(t) for t in gazu.task.all_tasks_for_shot(asdict(self))] + def get_all_assets(self) -> List[Asset]: + return [ + Asset.from_dict(t) for t in gazu.asset.all_assets_for_shot(asdict(self)) + ] + def get_sequence(self) -> Sequence: return Sequence.from_dict(gazu.shot.get_sequence_from_shot(asdict(self))) @@ -565,6 +572,23 @@ class Shot(Entity): gazu.shot.update_shot(asdict(self)) return self + def get_shot_task_name(self, task_type_short_name: str) -> str: # + return f"{self.name}{bkglobals.FILE_DELIMITER}{task_type_short_name}" + + def get_output_collection_name(self, task_type_short_name: str) -> str: + return f"{self.get_shot_task_name(task_type_short_name)}{bkglobals.FILE_DELIMITER}output" + + def get_shot_dir(self, context) -> str: + project_root_dir = prefs.project_root_dir_get(context) + all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots') + seq = self.get_sequence() + shot_dir = all_shots_dir.joinpath(seq.name).joinpath(self.name) + return shot_dir.__str__() + + def get_shot_filepath(self, context, task_type_short_name: str) -> str: + file_name = self.get_shot_task_name(task_type_short_name) + '.blend' + return Path(self.get_shot_dir(context)).joinpath(file_name).__str__() + def update_data(self, data: Dict[str, Any]) -> Shot: gazu.shot.update_shot_data(asdict(self), data=data) if not self.data: @@ -714,6 +738,11 @@ class TaskType(Entity): if t["for_entity"] == "Sequence" ] + def get_short_name(self) -> str: + for key, value in bkglobals.SHOT_TASK_MAPPING.items(): + if value == self.name: + return key + def __bool__(self) -> bool: return bool(self.id) diff --git a/scripts/index_assets/README.md b/scripts/index_assets/README.md new file mode 100644 index 00000000..3530d05c --- /dev/null +++ b/scripts/index_assets/README.md @@ -0,0 +1,10 @@ +# Index Assets + +This tool will create a dictionary of all data-blocks (collections only) [marked as an Asset](https://docs.blender.org/manual/en/latest/files/asset_libraries/introduction.html#asset-create) and stores this information in a JSON file at `you_project/svn/pro/assets/asset_index.json`. This JSON File is used by other tools like the Blender Kitsu Add-On to quickly discover assets to use in the shot builder. + +To run this tool use the following command, followed by the path to your project's root folder + +```bash +./run_index_assets.py your_project/ +``` + diff --git a/scripts/index_assets/blender_index_assets.py b/scripts/index_assets/blender_index_assets.py new file mode 100644 index 00000000..cbec0086 --- /dev/null +++ b/scripts/index_assets/blender_index_assets.py @@ -0,0 +1,66 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from pathlib import Path +import bpy +import json +import sys + + +def load_json_file(json_file_path: str) -> dict: + """Finds JSON file with asset index if any exist + + Args: + json_file_path (str): Path to existing JSON File + + Returns: + dict: Dictionary with the existing JSON File, else blank + """ + asset_index_json = Path(json_file_path) + if asset_index_json.exists(): + return json.load(open(asset_index_json)) + return {} + + +def dump_json_file(asset_dict: dict, json_file_path: str) -> None: + """Save Asset Index to JSON File at provided path + + Args: + asset_dict (dict): Dictionary of Asset Items + json_file_path (str): Path to Save JSON File + """ + with open(json_file_path, 'w') as json_file: + json.dump(asset_dict, json_file, indent=4) + + +def find_save_assets(): + """Find all collections marked as asset in the current + .blend file, and add them to a dictionary, saved as a JSON""" + + argv = sys.argv + json_file_path = argv[argv.index("--") + 1 :][0] + + asset_dict = load_json_file(json_file_path) + for col in bpy.data.collections: + if col.asset_data: + print(f"Found Asset {col.name}") + asset_dict[col.name] = { + 'type': type(col).bl_rna.name, + 'filepath': bpy.data.filepath, + } + print('Asset Index Completed') + dump_json_file(asset_dict, json_file_path) + + +find_save_assets() diff --git a/scripts/index_assets/run_index_assets.py b/scripts/index_assets/run_index_assets.py new file mode 100755 index 00000000..61d3a0ef --- /dev/null +++ b/scripts/index_assets/run_index_assets.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from pathlib import Path +import argparse +import os +import platform +import subprocess +import sys + + +def cancel_program(message: str) -> None: + """Cancel Execution of this file""" + print(message) + sys.exit(0) + + +parser = argparse.ArgumentParser() +parser.add_argument( + "path", + help="Path to a file(s) or folder(s) on which to perform crawl. In format of '{my_project}/'", +) + + +def get_bbatch_script_path() -> str: + """Returns path to script that runs with bbatch""" + dir = Path(__file__).parent.absolute() + return dir.joinpath("blender_index_assets.py").__str__() + + +def get_blender_path(project_path: Path) -> str: + """Get the path to a project's blender executable + + Args: + project_path (Path): Path Object, containing project's root path + + Returns: + str: Path to blender executable as a string + """ + # TODO get this from the run_blender.py script instead (new logic needs tobe added to run_blender.py first) + local_blender_path = project_path.joinpath('local').joinpath('blender') + system_name = platform.system().lower() + blender_path_base = local_blender_path / system_name + if system_name == 'linux': + blender_path = blender_path_base / 'blender' + elif system_name == 'darwin': + blender_path = ( + blender_path_base / 'Blender.app' / 'Contents' / 'MacOS' / 'Blender' + ) + elif system_name == 'windows': + blender_path = blender_path_base / 'blender.exe' + return blender_path.absolute().__str__() + + +def index_assets(): + """Crawl the Asset Library of a provided Blender Studio Pipeline Project and + index all assets into a dictionary using a script executed by bbatch""" + args = parser.parse_args() + project_path = Path(args.path) + if not project_path.exists(): + cancel_program("Provided Path does not exist") + asset_dir = ( + project_path.joinpath("svn").joinpath("pro").joinpath("assets").absolute() + ) + if not asset_dir.exists(): + cancel_program("Asset Library does not exist at provided path") + asset_dir_path = asset_dir.__str__() + json_file_path = asset_dir.joinpath("asset_index.json").__str__() + script_path = get_bbatch_script_path() + project_blender = get_blender_path(project_path) + print(project_blender) + os.chdir("../bbatch") + cmd_list = ( + 'python', + '-m', + 'bbatch', + asset_dir_path, + '--ask', + '--exec', + project_blender, + "--nosave", + "--recursive", + '--script', + script_path, + "--args", + f'{json_file_path}', + ) + process = subprocess.Popen(cmd_list, shell=False) + if process.wait() != 0: + cancel_program(f"Asset Index Failed!") + print("Asset Index Completed Successfully") + print(f"Index File: '{json_file_path}'") + return 0 + + +if __name__ == "__main__": + index_assets()