From 4e2040505e4616c6754c0a5e2979ab8823f8e96b Mon Sep 17 00:00:00 2001 From: TinyNick Date: Fri, 24 Mar 2023 12:19:11 -0400 Subject: [PATCH 01/25] Intial Commit - Move Shot_builder into Blender_Kitsu - Changes from older branch --- blender_kitsu/__init__.py | 3 + blender_kitsu/prefs.py | 9 + .../shot_builder}/__init__.py | 114 +++-- .../shot_builder}/asset.py | 0 .../shot_builder}/builder/__init__.py | 20 +- .../shot_builder}/builder/build_step.py | 10 +- .../shot_builder}/builder/init_asset.py | 8 +- .../shot_builder}/builder/init_shot.py | 8 +- .../shot_builder}/builder/invoke_hook.py | 4 +- .../shot_builder}/builder/new_scene.py | 4 +- .../shot_builder}/builder/save_file.py | 8 +- .../builder/set_render_settings.py | 2 +- .../shot_builder}/connectors/__init__.py | 0 .../shot_builder}/connectors/connector.py | 16 +- .../shot_builder}/connectors/default.py | 10 +- .../shot_builder}/connectors/kitsu.py | 76 +-- .../shot_builder/docs}/README.md | 466 +++++++++--------- .../shot_builder/docs/examples}/README.md | 8 +- .../shot_builder/docs/examples}/assets.py | 166 +++---- .../shot_builder/docs/examples}/config.py | 28 +- .../shot_builder/docs/examples}/hooks.py | 302 ++++++------ .../docs/examples/shot-builder/README.md | 6 + .../shot_builder/docs/examples}/shots.py | 58 +-- .../shot_builder}/hooks.py | 2 +- .../shot_builder}/operators.py | 35 +- .../shot_builder}/project.py | 26 +- .../shot_builder}/render_settings.py | 2 +- .../shot_builder}/shot.py | 0 .../shot_builder}/sys_utils.py | 0 .../shot_builder}/task_type.py | 0 .../shot_builder}/ui.py | 2 +- .../shot_builder}/vars.py | 0 shot_builder/properties.py | 57 --- 33 files changed, 697 insertions(+), 753 deletions(-) rename {shot_builder => blender_kitsu/shot_builder}/__init__.py (72%) rename {shot_builder => blender_kitsu/shot_builder}/asset.py (100%) rename {shot_builder => blender_kitsu/shot_builder}/builder/__init__.py (81%) rename {shot_builder => blender_kitsu/shot_builder}/builder/build_step.py (77%) rename {shot_builder => blender_kitsu/shot_builder}/builder/init_asset.py (70%) rename {shot_builder => blender_kitsu/shot_builder}/builder/init_shot.py (60%) rename {shot_builder => blender_kitsu/shot_builder}/builder/invoke_hook.py (75%) rename {shot_builder => blender_kitsu/shot_builder}/builder/new_scene.py (85%) rename {shot_builder => blender_kitsu/shot_builder}/builder/save_file.py (69%) rename {shot_builder => blender_kitsu/shot_builder}/builder/set_render_settings.py (91%) rename {shot_builder => blender_kitsu/shot_builder}/connectors/__init__.py (100%) rename {shot_builder => blender_kitsu/shot_builder}/connectors/connector.py (86%) rename {shot_builder => blender_kitsu/shot_builder}/connectors/default.py (83%) rename {shot_builder => blender_kitsu/shot_builder}/connectors/kitsu.py (71%) rename {shot_builder => blender_kitsu/shot_builder/docs}/README.md (96%) rename {shot_builder/docs/examples/shot-builder => blender_kitsu/shot_builder/docs/examples}/README.md (96%) rename {shot_builder/docs/examples/shot-builder => blender_kitsu/shot_builder/docs/examples}/assets.py (91%) rename {shot_builder/docs/examples/shot-builder => blender_kitsu/shot_builder/docs/examples}/config.py (80%) rename {shot_builder/docs/examples/shot-builder => blender_kitsu/shot_builder/docs/examples}/hooks.py (92%) create mode 100644 blender_kitsu/shot_builder/docs/examples/shot-builder/README.md rename {shot_builder/docs/examples/shot-builder => blender_kitsu/shot_builder/docs/examples}/shots.py (84%) rename {shot_builder => blender_kitsu/shot_builder}/hooks.py (98%) rename {shot_builder => blender_kitsu/shot_builder}/operators.py (79%) rename {shot_builder => blender_kitsu/shot_builder}/project.py (95%) rename {shot_builder => blender_kitsu/shot_builder}/render_settings.py (95%) rename {shot_builder => blender_kitsu/shot_builder}/shot.py (100%) rename {shot_builder => blender_kitsu/shot_builder}/sys_utils.py (100%) rename {shot_builder => blender_kitsu/shot_builder}/task_type.py (100%) rename {shot_builder => blender_kitsu/shot_builder}/ui.py (95%) rename {shot_builder => blender_kitsu/shot_builder}/vars.py (100%) delete mode 100644 shot_builder/properties.py diff --git a/blender_kitsu/__init__.py b/blender_kitsu/__init__.py index 770c0cfa..b8494292 100644 --- a/blender_kitsu/__init__.py +++ b/blender_kitsu/__init__.py @@ -20,6 +20,7 @@ import bpy from blender_kitsu import ( + shot_builder, lookdev, bkglobals, types, @@ -92,6 +93,7 @@ def register(): # tasks.register() playblast.register() anim.register() + shot_builder.register() LoggerLevelManager.configure_levels() logger.info("Registered blender-kitsu") @@ -109,6 +111,7 @@ def unregister(): prefs.unregister() lookdev.unregister() playblast.unregister() + shot_builder.unregister() LoggerLevelManager.restore_levels() diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index e4eb2a78..5eb158bb 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -285,6 +285,15 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): type=KITSU_media_update_search_paths ) + production_path: bpy.props.StringProperty( # type: ignore + name="Production Root", + description="The location to load configuration files from when " + "they couldn't be found in any parent folder of the current " + "file. Folder must contain a sub-folder named `shot-builder` " + "that holds the configuration files", + subtype='DIR_PATH', + ) + session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) diff --git a/shot_builder/__init__.py b/blender_kitsu/shot_builder/__init__.py similarity index 72% rename from shot_builder/__init__.py rename to blender_kitsu/shot_builder/__init__.py index e282903b..cb825046 100644 --- a/shot_builder/__init__.py +++ b/blender_kitsu/shot_builder/__init__.py @@ -1,58 +1,56 @@ -# ##### 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.ui import * -from shot_builder.connectors.kitsu import * -from shot_builder.operators import * -from shot_builder.properties import * -import bpy - -# 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, - ShotBuilderPreferences, - SHOTBUILDER_OT_NewShotFile, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler) - - -def unregister(): - bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler) - for cls in classes: - bpy.utils.unregister_class(cls) +# ##### 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 blender_kitsu.shot_builder.ui import * +from blender_kitsu.shot_builder.connectors.kitsu import * +from blender_kitsu.shot_builder.operators import * +import bpy + +# 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, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler) + + +def unregister(): + bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler) + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/shot_builder/asset.py b/blender_kitsu/shot_builder/asset.py similarity index 100% rename from shot_builder/asset.py rename to blender_kitsu/shot_builder/asset.py diff --git a/shot_builder/builder/__init__.py b/blender_kitsu/shot_builder/builder/__init__.py similarity index 81% rename from shot_builder/builder/__init__.py rename to blender_kitsu/shot_builder/builder/__init__.py index 1aa70b6b..056ea561 100644 --- a/shot_builder/builder/__init__.py +++ b/blender_kitsu/shot_builder/builder/__init__.py @@ -1,13 +1,13 @@ -from shot_builder.project import Production -from shot_builder.task_type import TaskType -from shot_builder.asset import Asset, AssetRef -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.builder.init_asset import InitAssetStep -from shot_builder.builder.init_shot import InitShotStep -from shot_builder.builder.set_render_settings import SetRenderSettingsStep -from shot_builder.builder.new_scene import NewSceneStep -from shot_builder.builder.invoke_hook import InvokeHookStep -from shot_builder.builder.save_file import SaveFileStep +from blender_kitsu.shot_builder.project import Production +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.asset import Asset, AssetRef +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.builder.init_asset import InitAssetStep +from blender_kitsu.shot_builder.builder.init_shot import InitShotStep +from blender_kitsu.shot_builder.builder.set_render_settings import SetRenderSettingsStep +from blender_kitsu.shot_builder.builder.new_scene import NewSceneStep +from blender_kitsu.shot_builder.builder.invoke_hook import InvokeHookStep +from blender_kitsu.shot_builder.builder.save_file import SaveFileStep import bpy diff --git a/shot_builder/builder/build_step.py b/blender_kitsu/shot_builder/builder/build_step.py similarity index 77% rename from shot_builder/builder/build_step.py rename to blender_kitsu/shot_builder/builder/build_step.py index dc873546..9bcbf343 100644 --- a/shot_builder/builder/build_step.py +++ b/blender_kitsu/shot_builder/builder/build_step.py @@ -1,11 +1,11 @@ import bpy import typing -from shot_builder.project import Production -from shot_builder.shot import Shot -from shot_builder.task_type import TaskType -from shot_builder.render_settings import RenderSettings -from shot_builder.asset import Asset +from blender_kitsu.shot_builder.project import Production +from blender_kitsu.shot_builder.shot import Shot +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.asset import Asset class BuildContext: diff --git a/shot_builder/builder/init_asset.py b/blender_kitsu/shot_builder/builder/init_asset.py similarity index 70% rename from shot_builder/builder/init_asset.py rename to blender_kitsu/shot_builder/builder/init_asset.py index 962be600..65267f84 100644 --- a/shot_builder/builder/init_asset.py +++ b/blender_kitsu/shot_builder/builder/init_asset.py @@ -1,7 +1,7 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.asset import * -from shot_builder.project import * -from shot_builder.shot import * +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.asset import * +from blender_kitsu.shot_builder.project import * +from blender_kitsu.shot_builder.shot import * import bpy diff --git a/shot_builder/builder/init_shot.py b/blender_kitsu/shot_builder/builder/init_shot.py similarity index 60% rename from shot_builder/builder/init_shot.py rename to blender_kitsu/shot_builder/builder/init_shot.py index 557e43bf..ff2cc8ef 100644 --- a/shot_builder/builder/init_shot.py +++ b/blender_kitsu/shot_builder/builder/init_shot.py @@ -1,7 +1,7 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.asset import * -from shot_builder.project import * -from shot_builder.shot import * +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.asset import * +from blender_kitsu.shot_builder.project import * +from blender_kitsu.shot_builder.shot import * import bpy diff --git a/shot_builder/builder/invoke_hook.py b/blender_kitsu/shot_builder/builder/invoke_hook.py similarity index 75% rename from shot_builder/builder/invoke_hook.py rename to blender_kitsu/shot_builder/builder/invoke_hook.py index 15fcd45b..797c442d 100644 --- a/shot_builder/builder/invoke_hook.py +++ b/blender_kitsu/shot_builder/builder/invoke_hook.py @@ -1,5 +1,5 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.hooks import HookFunction +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.hooks import HookFunction import bpy import typing diff --git a/shot_builder/builder/new_scene.py b/blender_kitsu/shot_builder/builder/new_scene.py similarity index 85% rename from shot_builder/builder/new_scene.py rename to blender_kitsu/shot_builder/builder/new_scene.py index c3024c40..502bf51d 100644 --- a/shot_builder/builder/new_scene.py +++ b/blender_kitsu/shot_builder/builder/new_scene.py @@ -1,5 +1,5 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.render_settings import RenderSettings import bpy import logging diff --git a/shot_builder/builder/save_file.py b/blender_kitsu/shot_builder/builder/save_file.py similarity index 69% rename from shot_builder/builder/save_file.py rename to blender_kitsu/shot_builder/builder/save_file.py index a167d135..0122c915 100644 --- a/shot_builder/builder/save_file.py +++ b/blender_kitsu/shot_builder/builder/save_file.py @@ -1,7 +1,7 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext -from shot_builder.asset import * -from shot_builder.project import * -from shot_builder.shot import * +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.asset import * +from blender_kitsu.shot_builder.project import * +from blender_kitsu.shot_builder.shot import * import pathlib import bpy diff --git a/shot_builder/builder/set_render_settings.py b/blender_kitsu/shot_builder/builder/set_render_settings.py similarity index 91% rename from shot_builder/builder/set_render_settings.py rename to blender_kitsu/shot_builder/builder/set_render_settings.py index f82753fb..a18cb23a 100644 --- a/shot_builder/builder/set_render_settings.py +++ b/blender_kitsu/shot_builder/builder/set_render_settings.py @@ -1,4 +1,4 @@ -from shot_builder.builder.build_step import BuildStep, BuildContext +from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext import bpy import typing diff --git a/shot_builder/connectors/__init__.py b/blender_kitsu/shot_builder/connectors/__init__.py similarity index 100% rename from shot_builder/connectors/__init__.py rename to blender_kitsu/shot_builder/connectors/__init__.py diff --git a/shot_builder/connectors/connector.py b/blender_kitsu/shot_builder/connectors/connector.py similarity index 86% rename from shot_builder/connectors/connector.py rename to blender_kitsu/shot_builder/connectors/connector.py index 08e159c5..1222821a 100644 --- a/shot_builder/connectors/connector.py +++ b/blender_kitsu/shot_builder/connectors/connector.py @@ -21,16 +21,16 @@ This module contains the Connector class. It is an abstract base class for concrete connectors. """ -from shot_builder.shot import Shot, ShotRef -from shot_builder.asset import Asset, AssetRef -from shot_builder.task_type import TaskType -from shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.shot import Shot, ShotRef +from blender_kitsu.shot_builder.asset import Asset, AssetRef +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.render_settings import RenderSettings from typing import * if TYPE_CHECKING: - from shot_builder.project import Production - from shot_builder.properties import ShotBuilderPreferences + from blender_kitsu.shot_builder.project import Production + from blender_kitsu.shot_builder.properties import ShotBuilderPreferences class Connector: @@ -58,8 +58,8 @@ class Connector: Example of using predefined connectors in a production config file: ```shot-builder/config.py - from shot_builder.connectors.default import DefaultConnector - from shot_builder.connectors.kitsu import KitsuConnector + from blender_kitsu.shot_builder.connectors.default import DefaultConnector + from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector PRODUCTION_NAME = DefaultConnector TASK_TYPES = KitsuConnector diff --git a/shot_builder/connectors/default.py b/blender_kitsu/shot_builder/connectors/default.py similarity index 83% rename from shot_builder/connectors/default.py rename to blender_kitsu/shot_builder/connectors/default.py index 6d55e942..9080090a 100644 --- a/shot_builder/connectors/default.py +++ b/blender_kitsu/shot_builder/connectors/default.py @@ -17,11 +17,11 @@ # ##### END GPL LICENSE BLOCK ##### # -from shot_builder.shot import Shot, ShotRef -from shot_builder.asset import Asset, AssetRef -from shot_builder.task_type import TaskType -from shot_builder.render_settings import RenderSettings -from shot_builder.connectors.connector import Connector +from blender_kitsu.shot_builder.shot import Shot, ShotRef +from blender_kitsu.shot_builder.asset import Asset, AssetRef +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.connectors.connector import Connector from typing import * diff --git a/shot_builder/connectors/kitsu.py b/blender_kitsu/shot_builder/connectors/kitsu.py similarity index 71% rename from shot_builder/connectors/kitsu.py rename to blender_kitsu/shot_builder/connectors/kitsu.py index 5caf8798..cb8858d9 100644 --- a/shot_builder/connectors/kitsu.py +++ b/blender_kitsu/shot_builder/connectors/kitsu.py @@ -18,13 +18,16 @@ # import bpy -from shot_builder import vars -from shot_builder.shot import Shot, ShotRef -from shot_builder.asset import Asset, AssetRef -from shot_builder.task_type import TaskType -from shot_builder.render_settings import RenderSettings -from shot_builder.connectors.connector import Connector +from blender_kitsu.shot_builder import vars +from blender_kitsu.shot_builder.shot import Shot, ShotRef +from blender_kitsu.shot_builder.asset import Asset, AssetRef +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.connectors.connector import Connector import requests +from blender_kitsu import cache +from blender_kitsu.gazu.asset import all_assets_for_shot +from blender_kitsu.gazu.shot import all_shots_for_project, all_sequences_for_project import typing import logging @@ -131,47 +134,9 @@ class KitsuConnector(Connector): def __init__(self, **kwargs): super().__init__(**kwargs) - self.__jwt_access_token = "" - self.__validate() - self.__authorize() - - def __validate(self) -> None: - self._preferences.kitsu._validate() - if not self._production.config.get('KITSU_PROJECT_ID'): - raise KitsuException( - "KITSU_PROJECT_ID is not configured in config.py") - - def __authorize(self) -> None: - kitsu_pref = self._preferences.kitsu - backend = kitsu_pref.backend - username = kitsu_pref.username - password = kitsu_pref.password - - logger.info(f"authorize {username} against {backend}") - response = requests.post( - url=f"{backend}/auth/login", data={'email': username, 'password': password}) - if response.status_code != 200: - self.__jwt_access_token = "" - raise KitsuException( - f"unable to authorize (status code={response.status_code})") - json_response = response.json() - self.__jwt_access_token = json_response['access_token'] - - def __api_get(self, api: str) -> typing.Any: - kitsu_pref = self._preferences.kitsu - backend = kitsu_pref.backend - - response = requests.get(url=f"{backend}{api}", headers={ - "Authorization": f"Bearer {self.__jwt_access_token}" - }) - if response.status_code != 200: - raise KitsuException( - f"unable to call kitsu (api={api}, status code={response.status_code})") - return response.json() def __get_production_data(self) -> KitsuProject: - project_id = self._production.config['KITSU_PROJECT_ID'] - production = self.__api_get(f"data/projects/{project_id}") + production = cache.project_active_get() project = KitsuProject(typing.cast( typing.Dict[str, typing.Any], production)) return project @@ -181,22 +146,23 @@ class KitsuConnector(Connector): return production.get_name() def get_task_types(self) -> typing.List[TaskType]: - task_types = self.__api_get(f"data/task_types/") + 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_id = self._production.config['KITSU_PROJECT_ID'] - kitsu_sequences = self.__api_get(f"data/projects/{project_id}/sequences") + project = cache.project_active_get() + kitsu_sequences = 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 = self.__api_get(f"data/projects/{project_id}/shots") - + kitsu_shots = all_shots_for_project(project.id) shots: typing.List[ShotRef] = [] for shot_data in kitsu_shots: @@ -232,8 +198,8 @@ class KitsuConnector(Connector): return shots def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]: - kitsu_assets = self.__api_get( - f"data/shots/{shot.kitsu_id}/assets") + kitsu_assets = all_assets_for_shot(shot.kitsu_id) + return [AssetRef(name=asset_data['name'], code=asset_data['code']) for asset_data in kitsu_assets] @@ -241,7 +207,5 @@ class KitsuConnector(Connector): """ Retrieve the render settings for the given shot. """ - kitsu_project = self.__get_production_data() - resolution = kitsu_project.get_resolution() - frames_per_second = shot.frames_per_second - return RenderSettings(width=resolution[0], height=resolution[1], frames_per_second=frames_per_second) + 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/shot_builder/README.md b/blender_kitsu/shot_builder/docs/README.md similarity index 96% rename from shot_builder/README.md rename to blender_kitsu/shot_builder/docs/README.md index f5b67716..a438b3ee 100644 --- a/shot_builder/README.md +++ b/blender_kitsu/shot_builder/docs/README.md @@ -1,233 +1,233 @@ -# 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 `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. - -## 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. +# 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 `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. + +## 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/shot_builder/docs/examples/shot-builder/README.md b/blender_kitsu/shot_builder/docs/examples/README.md similarity index 96% rename from shot_builder/docs/examples/shot-builder/README.md rename to blender_kitsu/shot_builder/docs/examples/README.md index 7b192c94..601eba80 100644 --- a/shot_builder/docs/examples/shot-builder/README.md +++ b/blender_kitsu/shot_builder/docs/examples/README.md @@ -1,4 +1,4 @@ -# 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. +# 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/shot_builder/docs/examples/shot-builder/assets.py b/blender_kitsu/shot_builder/docs/examples/assets.py similarity index 91% rename from shot_builder/docs/examples/shot-builder/assets.py rename to blender_kitsu/shot_builder/docs/examples/assets.py index 8b81843b..eb40b648 100644 --- a/shot_builder/docs/examples/shot-builder/assets.py +++ b/blender_kitsu/shot_builder/docs/examples/assets.py @@ -1,83 +1,83 @@ -from shot_builder.asset import Asset - - -class SpriteFrightAsset(Asset): - path = "{production.path}/lib/{asset.asset_type}/{asset.code}/{asset.code}.blend" - - -class Character(SpriteFrightAsset): - asset_type = "char" - collection = "CH-{asset.code}" - - -class Ellie(Character): - name = "Ellie" - code = "ellie" - - -class Victoria(Character): - name = "Victoria" - code = "victoria" - - -class Phil(Character): - name = "Phil" - code = "phil" - - -class Rex(Character): - name = "Rex" - code = "rex" - - -class Jay(Character): - name = "Jay" - code = "jay" - -# TODO: Bird character has no asset file yet. -# class Bird(Character): -# name = "Bird" -# code = "bird" - - -class Prop(SpriteFrightAsset): - asset_type = "props" - collection = "PR-{asset.code}" - - -class Boombox(Prop): - name = "Boombox" - code = "boombox" - - -class BBQGrill(Prop): - name = "BBQ Grill" - code = "bbq_grill" - - -# NOTE: NotepadAndPencil is a combined asset. In Kitsu it is defined as a single asset. In the production -# reportitory it is stored as 2 collections in a single file. See `hooks.link_char_prop_for_anim` -# where this is handled. -class NotepadAndPencil(Prop): - name = "Notepad and pencil" - code = "notepad_pencil" - - -class Binoculars(Prop): - name = "Binoculars (Ellie)" - code = "binoculars" - - -class Backpack(Prop): - name = "Backpack (Phil)" - code = "backpack" - - -class Set(SpriteFrightAsset): - asset_type = "sets" - collection = "SE-{asset.code}" - - -class MushroomGrove(Set): - name = "Mushroom grove" - code = "mushroom_grove" +from blender_kitsu.shot_builder.asset import Asset + + +class SpriteFrightAsset(Asset): + path = "{production.path}/lib/{asset.asset_type}/{asset.code}/{asset.code}.blend" + + +class Character(SpriteFrightAsset): + asset_type = "char" + collection = "CH-{asset.code}" + + +class Ellie(Character): + name = "Ellie" + code = "ellie" + + +class Victoria(Character): + name = "Victoria" + code = "victoria" + + +class Phil(Character): + name = "Phil" + code = "phil" + + +class Rex(Character): + name = "Rex" + code = "rex" + + +class Jay(Character): + name = "Jay" + code = "jay" + +# TODO: Bird character has no asset file yet. +# class Bird(Character): +# name = "Bird" +# code = "bird" + + +class Prop(SpriteFrightAsset): + asset_type = "props" + collection = "PR-{asset.code}" + + +class Boombox(Prop): + name = "Boombox" + code = "boombox" + + +class BBQGrill(Prop): + name = "BBQ Grill" + code = "bbq_grill" + + +# NOTE: NotepadAndPencil is a combined asset. In Kitsu it is defined as a single asset. In the production +# reportitory it is stored as 2 collections in a single file. See `hooks.link_char_prop_for_anim` +# where this is handled. +class NotepadAndPencil(Prop): + name = "Notepad and pencil" + code = "notepad_pencil" + + +class Binoculars(Prop): + name = "Binoculars (Ellie)" + code = "binoculars" + + +class Backpack(Prop): + name = "Backpack (Phil)" + code = "backpack" + + +class Set(SpriteFrightAsset): + asset_type = "sets" + collection = "SE-{asset.code}" + + +class MushroomGrove(Set): + name = "Mushroom grove" + code = "mushroom_grove" diff --git a/shot_builder/docs/examples/shot-builder/config.py b/blender_kitsu/shot_builder/docs/examples/config.py similarity index 80% rename from shot_builder/docs/examples/shot-builder/config.py rename to blender_kitsu/shot_builder/docs/examples/config.py index 3d3a8dde..bde157a3 100644 --- a/shot_builder/docs/examples/shot-builder/config.py +++ b/blender_kitsu/shot_builder/docs/examples/config.py @@ -1,14 +1,14 @@ -from shot_builder.connectors.kitsu import KitsuConnector - -PRODUCTION_NAME = KitsuConnector -SHOTS = KitsuConnector -ASSETS = KitsuConnector -RENDER_SETTINGS = KitsuConnector - -KITSU_PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec" - -# Formatting rules -# ---------------- - -# The name of the scene in blender where the shot is build in. -# SCENE_NAME_FORMAT = "{shot.sequence_code}_{shot.code}.{task_type}" +from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector + +PRODUCTION_NAME = KitsuConnector +SHOTS = KitsuConnector +ASSETS = KitsuConnector +RENDER_SETTINGS = KitsuConnector + +KITSU_PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec" + +# Formatting rules +# ---------------- + +# The name of the scene in blender where the shot is build in. +# SCENE_NAME_FORMAT = "{shot.sequence_code}_{shot.code}.{task_type}" diff --git a/shot_builder/docs/examples/shot-builder/hooks.py b/blender_kitsu/shot_builder/docs/examples/hooks.py similarity index 92% rename from shot_builder/docs/examples/shot-builder/hooks.py rename to blender_kitsu/shot_builder/docs/examples/hooks.py index d588dad3..b8af3a91 100644 --- a/shot_builder/docs/examples/shot-builder/hooks.py +++ b/blender_kitsu/shot_builder/docs/examples/hooks.py @@ -1,151 +1,151 @@ -import bpy -from shot_builder.hooks import hook, Wildcard -from shot_builder.asset import Asset -from shot_builder.shot import Shot -from shot_builder.project import Production - -import logging - -logger = logging.getLogger(__name__) - -# ---------- Global Hook ---------- - - -@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}/lib/cam/camera_rig.blend" - 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['CAM-camera'] - 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 - - _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=['char', '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, - ) +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 + +import logging + +logger = logging.getLogger(__name__) + +# ---------- Global Hook ---------- + + +@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}/lib/cam/camera_rig.blend" + 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['CAM-camera'] + 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 + + _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=['char', '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/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md b/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md new file mode 100644 index 00000000..0306e897 --- /dev/null +++ b/blender_kitsu/shot_builder/docs/examples/shot-builder/README.md @@ -0,0 +1,6 @@ +# 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/shot_builder/docs/examples/shot-builder/shots.py b/blender_kitsu/shot_builder/docs/examples/shots.py similarity index 84% rename from shot_builder/docs/examples/shot-builder/shots.py rename to blender_kitsu/shot_builder/docs/examples/shots.py index 898ed223..bc437772 100644 --- a/shot_builder/docs/examples/shot-builder/shots.py +++ b/blender_kitsu/shot_builder/docs/examples/shots.py @@ -1,29 +1,29 @@ -from shot_builder.shot import Shot -from shot_builder.project import Production - - -class SpriteFrightShot(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_output_collection_name(self, shot: Shot, task_type: str) -> str: - """ - Get the collection name where the output is stored. - """ - return f"{shot.sequence_code}_{shot.code}.{task_type}.output" - - -class Sequence_0002(SpriteFrightShot): - sequence_code = "0002" - - -class Shot_0001_0001_A(Sequence_0002): - name = "001" - code = "0001" +from blender_kitsu.shot_builder.shot import Shot +from blender_kitsu.shot_builder.project import Production + + +class SpriteFrightShot(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_output_collection_name(self, shot: Shot, task_type: str) -> str: + """ + Get the collection name where the output is stored. + """ + return f"{shot.sequence_code}_{shot.code}.{task_type}.output" + + +class Sequence_0002(SpriteFrightShot): + sequence_code = "0002" + + +class Shot_0001_0001_A(Sequence_0002): + name = "001" + code = "0001" diff --git a/shot_builder/hooks.py b/blender_kitsu/shot_builder/hooks.py similarity index 98% rename from shot_builder/hooks.py rename to blender_kitsu/shot_builder/hooks.py index b07b8df7..6b8188b0 100644 --- a/shot_builder/hooks.py +++ b/blender_kitsu/shot_builder/hooks.py @@ -78,7 +78,7 @@ class Hooks: def _register_hook(func: types.FunctionType) -> None: - from shot_builder.project import get_active_production + from blender_kitsu.shot_builder.project import get_active_production production = get_active_production() production.hooks.register(func) diff --git a/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py similarity index 79% rename from shot_builder/operators.py rename to blender_kitsu/shot_builder/operators.py index c8f83a97..544684c5 100644 --- a/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -19,10 +19,11 @@ # from typing import * import bpy -from shot_builder.shot import ShotRef -from shot_builder.project import * -from shot_builder.builder import ShotBuilder -from shot_builder.task_type import TaskType +from blender_kitsu.shot_builder.shot import ShotRef +from blender_kitsu.shot_builder.project import ensure_loaded_production, get_active_production +from blender_kitsu.shot_builder.builder import ShotBuilder +from blender_kitsu.shot_builder.task_type import TaskType +from blender_kitsu import prefs, cache _production_task_type_items: List[Tuple[str, str, str]] = [] @@ -103,18 +104,34 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): ) 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() - production_root = get_production_root(context) - if production_root is None: + if addon_prefs.session.is_auth() is False: self.report( - {'WARNING'}, "Operator is cancelled due to inability to determine the production path. Make sure the a default path in configured in the preferences.") + {'ERROR'}, "Must be logged into Kitsu to continue. Check 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. Check 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. Check 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 + ensure_loaded_production(context) production = get_active_production() - self.production_root = str(production.path) - self.production_name = production.get_name(context=context) + + self.production_root = addon_prefs.project_root_dir + self.production_name = project.name global _production_task_type_items _production_task_type_items = production.get_task_type_items( diff --git a/shot_builder/project.py b/blender_kitsu/shot_builder/project.py similarity index 95% rename from shot_builder/project.py rename to blender_kitsu/shot_builder/project.py index 93906a14..d6de5442 100644 --- a/shot_builder/project.py +++ b/blender_kitsu/shot_builder/project.py @@ -23,15 +23,18 @@ from collections import defaultdict import bpy -from shot_builder.task_type import * -from shot_builder.shot import Shot, ShotRef -from shot_builder.render_settings import RenderSettings -from shot_builder.asset import Asset, AssetRef -from shot_builder.sys_utils import * -from shot_builder.hooks import Hooks, register_hooks +from blender_kitsu.shot_builder.task_type import * +from blender_kitsu.shot_builder.shot import Shot, ShotRef +from blender_kitsu.shot_builder.render_settings import RenderSettings +from blender_kitsu.shot_builder.asset import Asset, AssetRef +from blender_kitsu.shot_builder.sys_utils import * +from blender_kitsu.shot_builder.hooks import Hooks, register_hooks -from shot_builder.connectors.default import DefaultConnector -from shot_builder.connectors.connector import Connector +from blender_kitsu.shot_builder.connectors.default import DefaultConnector +from blender_kitsu.shot_builder.connectors.connector import Connector + +from blender_kitsu import prefs +from pathlib import Path from typing import * import types @@ -77,7 +80,7 @@ class Production: connector_cls: Type[Connector], context: bpy.types.Context) -> Connector: # TODO: Cache connector - preferences = context.preferences.addons[__package__].preferences + preferences = context.preferences.addons["blender_kitsu"].preferences return connector_cls(production=self, preferences=preferences) def __format_shot_name(self, shot: Shot) -> str: @@ -382,8 +385,9 @@ def get_production_root(context: bpy.types.Context) -> Optional[pathlib.Path]: production_root = _find_production_root(current_file) if production_root: return production_root - production_root = pathlib.Path( - context.preferences.addons[__package__].preferences.production_path) + + 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 diff --git a/shot_builder/render_settings.py b/blender_kitsu/shot_builder/render_settings.py similarity index 95% rename from shot_builder/render_settings.py rename to blender_kitsu/shot_builder/render_settings.py index f2c0b550..5b79a4a8 100644 --- a/shot_builder/render_settings.py +++ b/blender_kitsu/shot_builder/render_settings.py @@ -18,7 +18,7 @@ # -from shot_builder.asset import Asset +from blender_kitsu.shot_builder.asset import Asset from typing import * diff --git a/shot_builder/shot.py b/blender_kitsu/shot_builder/shot.py similarity index 100% rename from shot_builder/shot.py rename to blender_kitsu/shot_builder/shot.py diff --git a/shot_builder/sys_utils.py b/blender_kitsu/shot_builder/sys_utils.py similarity index 100% rename from shot_builder/sys_utils.py rename to blender_kitsu/shot_builder/sys_utils.py diff --git a/shot_builder/task_type.py b/blender_kitsu/shot_builder/task_type.py similarity index 100% rename from shot_builder/task_type.py rename to blender_kitsu/shot_builder/task_type.py diff --git a/shot_builder/ui.py b/blender_kitsu/shot_builder/ui.py similarity index 95% rename from shot_builder/ui.py rename to blender_kitsu/shot_builder/ui.py index 5d44b5e1..bfb62552 100644 --- a/shot_builder/ui.py +++ b/blender_kitsu/shot_builder/ui.py @@ -19,7 +19,7 @@ # import bpy from typing import * -from shot_builder.operators import * +from blender_kitsu.shot_builder.operators import * def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None: diff --git a/shot_builder/vars.py b/blender_kitsu/shot_builder/vars.py similarity index 100% rename from shot_builder/vars.py rename to blender_kitsu/shot_builder/vars.py diff --git a/shot_builder/properties.py b/shot_builder/properties.py deleted file mode 100644 index ad822df4..00000000 --- a/shot_builder/properties.py +++ /dev/null @@ -1,57 +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 - -import pathlib - -from shot_builder.project import is_valid_production_root -from shot_builder.connectors.kitsu import KitsuPreferences - - -class ShotBuilderPreferences(bpy.types.AddonPreferences): - bl_idname = __package__ - - production_path: bpy.props.StringProperty( # type: ignore - name="Production Root", - description="The location to load configuration files from when " - "they couldn't be found in any parent folder of the current " - "file. Folder must contain a sub-folder named `shot-builder` " - "that holds the configuration files", - subtype='DIR_PATH', - ) - - kitsu: bpy.props.PointerProperty( # type: ignore - name="Kitsu Preferences", - type=KitsuPreferences - ) - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - - is_valid = is_valid_production_root(pathlib.Path(self.production_path)) - layout.prop(self, "production_path", - icon='NONE' if is_valid else 'ERROR') - if not is_valid: - layout.label(text="Folder must contain a sub-folder named " - "`shot-builder` that holds the configuration " - "files.", - icon="ERROR") - sublayout = layout.box() - self.kitsu.draw(sublayout, context) -- 2.30.2 From 853bf6d303b6eebd692dc604bf5159e77e743f49 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Fri, 31 Mar 2023 18:17:07 -0400 Subject: [PATCH 02/25] [Blender_Kits] Use Anim_Setup 'Setup Workspaces' - add operator file - add registration to __init__.py --- blender_kitsu/__init__.py | 5 +++++ blender_kitsu/anim_setup/ops.py | 15 +++++++++++++++ blender_kitsu/shot_builder/operators.py | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 blender_kitsu/anim_setup/ops.py diff --git a/blender_kitsu/__init__.py b/blender_kitsu/__init__.py index b8494292..98d267d4 100644 --- a/blender_kitsu/__init__.py +++ b/blender_kitsu/__init__.py @@ -40,6 +40,8 @@ from blender_kitsu import ( ui, ) +from blender_kitsu.anim_setup import ops #TODO Fix Registraion + from blender_kitsu.logger import LoggerFactory, LoggerLevelManager logger = LoggerFactory.getLogger(__name__) @@ -79,6 +81,7 @@ if _need_reload: context.reload() tasks.reload() anim.reload() + #ops.reload() def register(): @@ -94,6 +97,7 @@ def register(): playblast.register() anim.register() shot_builder.register() + ops.register() LoggerLevelManager.configure_levels() logger.info("Registered blender-kitsu") @@ -112,6 +116,7 @@ def unregister(): lookdev.unregister() playblast.unregister() shot_builder.unregister() + ops.unregister() LoggerLevelManager.restore_levels() diff --git a/blender_kitsu/anim_setup/ops.py b/blender_kitsu/anim_setup/ops.py new file mode 100644 index 00000000..cbe61cf9 --- /dev/null +++ b/blender_kitsu/anim_setup/ops.py @@ -0,0 +1,15 @@ +import bpy +from typing import Set +from blender_kitsu import prefs +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]: + # Remove non anim workspaces. + for ws in bpy.data.workspaces: + if ws.name != "Animation": + bpy.ops.workspace.delete({"workspace": ws}) + self.report({"INFO"}, "Deleted non Animation workspaces") + return {"FINISHED"} diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 544684c5..9f64fe2c 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -158,6 +158,8 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): shot_builder.create_build_steps() shot_builder.build() + # Load Anim Workspace + bpy.ops.anim_setup.setup_workspaces() return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: -- 2.30.2 From 50819b1535f89375231ba3ae3f04a162c1b14734 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Fri, 31 Mar 2023 18:31:55 -0400 Subject: [PATCH 03/25] [Blender_Kitsu] Import video from editorial folder - Adds storyboard ref from editorial inside scene - Re-use logic from anim_setup - import video from editorial folder during shot_build --- blender_kitsu/anim_setup/ops.py | 112 ++++++++++++++++++++++++ blender_kitsu/prefs.py | 7 ++ blender_kitsu/shot_builder/operators.py | 3 +- 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/blender_kitsu/anim_setup/ops.py b/blender_kitsu/anim_setup/ops.py index cbe61cf9..a1eb76aa 100644 --- a/blender_kitsu/anim_setup/ops.py +++ b/blender_kitsu/anim_setup/ops.py @@ -1,6 +1,13 @@ import bpy +from bpy import context +import re +from pathlib import Path from typing import Set from blender_kitsu import prefs +from blender_kitsu import cache +from blender_kitsu import gazu + + class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): bl_idname = "anim_setup.setup_workspaces" bl_label = "Setup Workspace" @@ -13,3 +20,108 @@ class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): bpy.ops.workspace.delete({"workspace": ws}) self.report({"INFO"}, "Deleted non Animation workspaces") return {"FINISHED"} + + +class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): + bl_idname = "asset_setup.load_latest_edit" + bl_label = "Load edit" + bl_description = ( + "Loads latest edit from shot_preview_folder " + "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" + ) + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + return cls.can_load_edit(context) + + @classmethod + def description(cls, context, properties): + if cls.can_load_edit(context): + return "Load latest edit from shared folder" + else: + return "Shared folder not set, or VSE area not available in this workspace" + + def execute(self, context: bpy.types.Context) -> Set[str]: + addon_prefs = prefs.addon_prefs_get(context) + edit_export_path = Path(addon_prefs.edit_export_dir) + strip_channel = 1 + latest_file = self._get_latest_edit(context) + if not latest_file: + self.report( + {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" + ) + strip_filepath = latest_file.as_posix() + strip_frame_start = 101 + + # Needs to be run in sequence editor area. + # area_override = None + scene = context.scene + if not scene.sequence_editor: + scene.sequence_editor_create() + seq_editor = scene.sequence_editor + strip = seq_editor.sequences.new_movie( + strip_filepath, + strip_filepath, + strip_channel + 1, + strip_frame_start, + fit_method="FIT", + ) + sound_strip = seq_editor.sequences.new_sound( + strip_filepath, + strip_filepath, + strip_channel, + strip_frame_start, + ) + + bpy.ops.kitsu.con_detect_context() + shot = cache.shot_active_get() + + + # Update shift frame range prop. + frame_in = shot.frame_in + frame_3d_offset = 101 + + # Set sequence strip start kitsu data. + for strip in scene.sequence_editor.sequences_all: + strip.frame_start = -(frame_in) + frame_3d_offset #TODO CONFIRM LOGIC HERE + + self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") + + return {"FINISHED"} + + def _get_latest_edit(self, context: bpy.types.Context): + addon_prefs = prefs.addon_prefs_get(context) + + edit_export_path = Path(addon_prefs.edit_export_dir) + + files_list = [ + f + for f in edit_export_path.iterdir() + if f.is_file() and self._is_valid_edit_name(f.name) + ] + files_list = sorted(files_list, reverse=True) + + return files_list[0] + + def _is_valid_edit_name(self, filename: str) -> bool: + pattern = r"petprojects_v\d\d\d.mp4" + + match = re.search(pattern, filename) + if match: + return True + return False + + +classes = [ + ANIM_SETUP_OT_setup_workspaces, + ANIM_SETUP_OT_load_latest_edit, +] + + +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/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 5eb158bb..9263b367 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -294,6 +294,13 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): subtype='DIR_PATH', ) + edit_export_dir: bpy.props.StringProperty( # type: ignore + name="Edit Export Directory", + options={"HIDDEN", "SKIP_SAVE"}, + subtype="DIR_PATH", + # TODO ADD DEFAULT + # TODO ADD TO KITSU PREFRENCES UI + ) session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 9f64fe2c..9fa39e44 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -157,7 +157,8 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): context=context, production=production, shot_name=self.shot_id, task_type=TaskType(self.task_type)) shot_builder.create_build_steps() shot_builder.build() - + #Load EDIT + bpy.ops.asset_setup.load_latest_edit() # Load Anim Workspace bpy.ops.anim_setup.setup_workspaces() return {'FINISHED'} -- 2.30.2 From a5fed16d828a6d1b12a2c51cad3744d053c2106e Mon Sep 17 00:00:00 2001 From: TinyNick Date: Fri, 31 Mar 2023 19:17:46 -0400 Subject: [PATCH 04/25] [Blender_Kitsu] Update 'edit_export_dir' Property - rename to 'Editorial Export Directory' - add description with hint to current editorial export path --- blender_kitsu/prefs.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 9263b367..5a7b2096 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -295,11 +295,10 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): ) edit_export_dir: bpy.props.StringProperty( # type: ignore - name="Edit Export Directory", + name="Editorial Export Directory", options={"HIDDEN", "SKIP_SAVE"}, + description="Directory path to editorial's export folder containing storyboard/animatic exports. Path should be similar to '~/shared-pets/editorial/export/'", subtype="DIR_PATH", - # TODO ADD DEFAULT - # TODO ADD TO KITSU PREFRENCES UI ) session: Session = Session() @@ -393,6 +392,11 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box.row().prop(self, "shot_counter_digits") box.row().prop(self, "shot_counter_increment") + # Misc settings. + box = layout.box() + box.label(text="Shot Builder", icon="MOD_BUILD") + box.row().prop(self, "edit_export_dir") + @property def playblast_root_path(self) -> Optional[Path]: if not self.is_playblast_root_valid: -- 2.30.2 From c996cf3a20e527c37cb2721c060a54fcd798c8b8 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sat, 1 Apr 2023 16:48:34 -0400 Subject: [PATCH 05/25] [Blender_Kitsu] Set Actions on Armatures - Find all Armature Objects - Set Action name to match blender-studio convention - Set action to fake user --- blender_kitsu/shot_builder/operators.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 9fa39e44..001b7202 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -161,6 +161,14 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): bpy.ops.asset_setup.load_latest_edit() # Load Anim Workspace bpy.ops.anim_setup.setup_workspaces() + shot = cache.shot_active_get() + + # Initilize armatures + for obj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]: + base_name = obj.name.split('RIG-')[-1] #BLEDNER-STUDIO NAMING CONVENTION + action = bpy.data.actions.new(f"ANI-{base_name}.{shot.name}.v001") + action.use_fake_user = True + obj.animation_data.action = action return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: -- 2.30.2 From ffff5b5ad78a4510a7a9dfae3950b1ac92c59ab3 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sat, 1 Apr 2023 16:50:52 -0400 Subject: [PATCH 06/25] [Blender Kitsu] Set Frame Range during Shot Build --- blender_kitsu/shot_builder/operators.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 001b7202..b61df68b 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -169,6 +169,11 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): action = bpy.data.actions.new(f"ANI-{base_name}.{shot.name}.v001") action.use_fake_user = True obj.animation_data.action = action + + # Set Shot Frame Range + start_frame_offset = 101 #TODO EXPOSE INT IN ADDON PREFERENCES + context.scene.frame_start = start_frame_offset + context.scene.frame_end = shot.nb_frames + start_frame_offset return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: -- 2.30.2 From b46156f636b4fb8e6095c55b27769e133a289845 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sun, 2 Apr 2023 02:26:24 -0400 Subject: [PATCH 07/25] [Blender_Kitsu] Load Editoral Export Path Safely - add edit_export_file_pattern to prefs - expose file pattern in addon preferences - check is_editorial_dir_valid() with function to prefs - Cancel shot_builder if edit_export_dir or edit_export_file_pattern is invalid --- blender_kitsu/prefs.py | 59 ++++++++++++++++++++++++- blender_kitsu/shot_builder/operators.py | 7 +++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 5a7b2096..8ad2090b 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -21,6 +21,7 @@ import hashlib import sys import os +import re from typing import Optional, Any, Set, Tuple, List from pathlib import Path @@ -157,6 +158,28 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): def init_playblast_file_model(self, context: bpy.types.Context) -> None: ops_playblast_data.init_playblast_file_model(context) + + def init_editoral_export_directory(self, context:bpy.types.Context) -> None: + edit_export_path = Path(self.edit_export_dir) + + files_list = [ + f + for f in edit_export_path.iterdir() + if f.is_file() + ] + + files_list = sorted(files_list, reverse=True) + valid_file = False + for item in files_list: + match = re.search(self.edit_export_file_pattern, item._str) + if match: + valid_file = True + if not valid_file: + self.edit_export_file_pattern = "" + logger.error( + "Failed to initialize editorial export file model. Invalid path/pattern. Check addon preferences" + ) + logger.info("Initialized editorial export file model, successfully.") bl_idname = __package__ @@ -297,9 +320,18 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): edit_export_dir: bpy.props.StringProperty( # type: ignore name="Editorial Export Directory", options={"HIDDEN", "SKIP_SAVE"}, - description="Directory path to editorial's export folder containing storyboard/animatic exports. Path should be similar to '~/shared-pets/editorial/export/'", + description="Directory path to editorial's export folder containing storyboard/animatic exports. Path should be similar to '~/shared-{proj_name}/editorial/export/'", subtype="DIR_PATH", ) + + edit_export_file_pattern: bpy.props.StringProperty( # type: ignore + name="Editorial Export File Pattern", + options={"HIDDEN", "SKIP_SAVE"}, + description="File pattern to search for latest editorial export. Typically '{proj_name}_v\d\d\d.mp4'", + default="petprojects_v\d\d\d.mp4", + update=init_editoral_export_directory, + + ) session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) @@ -396,6 +428,7 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box = layout.box() box.label(text="Shot Builder", icon="MOD_BUILD") box.row().prop(self, "edit_export_dir") + box.row().prop(self, "edit_export_file_pattern") @property def playblast_root_path(self) -> Optional[Path]: @@ -438,6 +471,30 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): return False return True + + @property + def is_editorial_dir_valid(self) -> bool: + edit_export_path = Path(self.edit_export_dir) + + files_list = [ + f + for f in edit_export_path.iterdir() + if f.is_file() + ] + + files_list = sorted(files_list, reverse=True) + valid_file = False + for item in files_list: + match = re.search(self.edit_export_file_pattern, item._str) + if match: + valid_file = True + if not valid_file: + logger.error( + "Failed to initialize editorial export file model. Invalid path/pattern. Check addon preferences" + ) + return False + logger.info("Initialized editorial export file model, successfully.") + return True def session_get(context: bpy.types.Context) -> Session: diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index b61df68b..88e6f110 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -150,6 +150,13 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): self.report( {'ERROR'}, "Shot builder can only be started from the File menu. Shortcuts like CTRL-N don't work") return {'CANCELLED'} + addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences + + if not addon_prefs.is_editorial_dir_valid: + self.report( + {'ERROR'}, "Shot builder is dependant on a valid editorial export path and file pattern. Check Preferences, errors appear in console") + return {'CANCELLED'} + ensure_loaded_production(context) production = get_active_production() -- 2.30.2 From d5746a6316786cde04f470b668c5d31ee4454bb4 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sun, 2 Apr 2023 03:59:29 -0400 Subject: [PATCH 08/25] [Blender Kitsu] Expose frame offset in addon prefs - Clear "TODO EXPOSE INT IN ADDON PREFERENCES" - Move shot_builder addon prefs above misc - Expose Start Frame Offset in addon pref - Nest Start Frame under 'show advanced' - Set frame range via ''shot_builder_frame_offset" --- blender_kitsu/prefs.py | 23 ++++++++++++++++++++++- blender_kitsu/shot_builder/operators.py | 8 ++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 8ad2090b..444ec7af 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -272,6 +272,10 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): name="Show Advanced Settings", description="Show advanced settings that should already have good defaults", ) + shot_builder_show_advanced : bpy.props.BoolProperty( # type: ignore + name="Show Advanced Settings", + description="Show advanced settings that should already have good defaults", + ) shot_pattern: bpy.props.StringProperty( # type: ignore name="Shot Pattern", @@ -332,6 +336,12 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): update=init_editoral_export_directory, ) + + shot_builder_frame_offset: bpy.props.IntProperty( # type: ignore + name="Start Frame Offset", + description="All Shots built by 'Shot_builder' should begin at this frame", + default=101, + ) session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) @@ -410,7 +420,18 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): icon="ADD", emboss=False, ) - + + # Shot_Builder settings. + box = layout.box() + box.label(text="Shot Builder", icon="MOD_BUILD") + box.row().prop(self, "edit_export_dir") + box.row().prop(self, "edit_export_file_pattern") + box.row().prop(self, "shot_builder_show_advanced") + if self.shot_builder_show_advanced: + start_frame_row = box.row() + start_frame_row.label(text="Start Frame Offset") + start_frame_row.prop(self, "shot_builder_frame_offset", text="") + # Misc settings. box = layout.box() box.label(text="Miscellaneous", icon="MODIFIER") diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 88e6f110..c75b1a19 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -177,10 +177,10 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): action.use_fake_user = True obj.animation_data.action = action - # Set Shot Frame Range - start_frame_offset = 101 #TODO EXPOSE INT IN ADDON PREFERENCES - context.scene.frame_start = start_frame_offset - context.scene.frame_end = shot.nb_frames + start_frame_offset + # Set Shot Frame Range + context.scene.frame_start = addon_prefs.shot_builder_frame_offset + context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset + return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: -- 2.30.2 From 6fc39c1a5f5c22a4e54be9099599f5e8b9cf5498 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sun, 2 Apr 2023 18:25:08 -0400 Subject: [PATCH 09/25] [Blender_Kitsu] Expose Armature naming convention in addon prefs - expose armature prefix as string in addon pref - expose action prefix as string in addon pref - give both detailed descriptions - use above string props in shot_builder; intilize armatures --- blender_kitsu/prefs.py | 22 ++++++++++++++++------ blender_kitsu/shot_builder/operators.py | 8 ++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 444ec7af..8225d485 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -342,6 +342,19 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): description="All Shots built by 'Shot_builder' should begin at this frame", default=101, ) + + shot_builder_armature_prefix: bpy.props.StringProperty( # type: ignore + name="Armature Prefix", + description="Naming convention prefix that exists on published assets containing armatures. Used to create/name actions during 'Shot_Build'. Armature name example:'{prefix}{base_name}'", + default="RIG-", + ) + + shot_builder_action_prefix: bpy.props.StringProperty( # type: ignore + name="Action Prefix", + description="Naming convention prefix to add to new actions. Actions will be named '{prefix}{base_name}.{shot_name}.v001' and set to fake user during 'Shot_Build'", + default="ANI-", + ) + session: Session = Session() tasks: bpy.props.CollectionProperty(type=KITSU_task) @@ -431,7 +444,9 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): start_frame_row = box.row() start_frame_row.label(text="Start Frame Offset") 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") + # Misc settings. box = layout.box() box.label(text="Miscellaneous", icon="MODIFIER") @@ -445,11 +460,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box.row().prop(self, "shot_counter_digits") box.row().prop(self, "shot_counter_increment") - # Misc settings. - box = layout.box() - box.label(text="Shot Builder", icon="MOD_BUILD") - box.row().prop(self, "edit_export_dir") - box.row().prop(self, "edit_export_file_pattern") @property def playblast_root_path(self) -> Optional[Path]: diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index c75b1a19..1c84b89f 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -172,10 +172,10 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): # Initilize armatures for obj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]: - base_name = obj.name.split('RIG-')[-1] #BLEDNER-STUDIO NAMING CONVENTION - action = bpy.data.actions.new(f"ANI-{base_name}.{shot.name}.v001") - action.use_fake_user = True - obj.animation_data.action = action + 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}.{shot.name}.v001") + new_action.use_fake_user = True + obj.animation_data.action = new_action # Set Shot Frame Range context.scene.frame_start = addon_prefs.shot_builder_frame_offset -- 2.30.2 From 3b7e20091e1f73d6fcfbdb44f579426b087c11be Mon Sep 17 00:00:00 2001 From: TinyNick Date: Sun, 2 Apr 2023 19:00:00 -0400 Subject: [PATCH 10/25] [Blender_Kitsu] Nest anim_setup in Shot_builder - Move anim_setup module to shot_builder - Refactor anim_setup functions into core functions that are called by operators, to easily re-use functions in shot_builder - Warn user before execution if editorial export directory is invalid (don't used prop update) use @property decorator instead - Centralize logic to check if path is valid in anim_setup module - remove duplicate code - replace bpy.ops.anim_setup with new function calls in shot_builder - use frame offset in editorial_export_get_latest - only report if workspace was deleted once in `ANIM_SETUP_OT_setup_workspaces` --- blender_kitsu/__init__.py | 6 +- blender_kitsu/anim_setup/ops.py | 127 ------------------ blender_kitsu/prefs.py | 43 +----- blender_kitsu/shot_builder/__init__.py | 3 + blender_kitsu/shot_builder/anim_setup/core.py | 83 ++++++++++++ blender_kitsu/shot_builder/anim_setup/ops.py | 49 +++++++ blender_kitsu/shot_builder/operators.py | 18 +-- 7 files changed, 149 insertions(+), 180 deletions(-) delete mode 100644 blender_kitsu/anim_setup/ops.py create mode 100644 blender_kitsu/shot_builder/anim_setup/core.py create mode 100644 blender_kitsu/shot_builder/anim_setup/ops.py diff --git a/blender_kitsu/__init__.py b/blender_kitsu/__init__.py index 98d267d4..7c8a4122 100644 --- a/blender_kitsu/__init__.py +++ b/blender_kitsu/__init__.py @@ -40,7 +40,7 @@ from blender_kitsu import ( ui, ) -from blender_kitsu.anim_setup import ops #TODO Fix Registraion + from blender_kitsu.logger import LoggerFactory, LoggerLevelManager @@ -81,7 +81,6 @@ if _need_reload: context.reload() tasks.reload() anim.reload() - #ops.reload() def register(): @@ -97,7 +96,7 @@ def register(): playblast.register() anim.register() shot_builder.register() - ops.register() + LoggerLevelManager.configure_levels() logger.info("Registered blender-kitsu") @@ -116,7 +115,6 @@ def unregister(): lookdev.unregister() playblast.unregister() shot_builder.unregister() - ops.unregister() LoggerLevelManager.restore_levels() diff --git a/blender_kitsu/anim_setup/ops.py b/blender_kitsu/anim_setup/ops.py deleted file mode 100644 index a1eb76aa..00000000 --- a/blender_kitsu/anim_setup/ops.py +++ /dev/null @@ -1,127 +0,0 @@ -import bpy -from bpy import context -import re -from pathlib import Path -from typing import Set -from blender_kitsu import prefs -from blender_kitsu import cache -from blender_kitsu import gazu - - -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]: - # Remove non anim workspaces. - for ws in bpy.data.workspaces: - if ws.name != "Animation": - bpy.ops.workspace.delete({"workspace": ws}) - self.report({"INFO"}, "Deleted non Animation workspaces") - return {"FINISHED"} - - -class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): - bl_idname = "asset_setup.load_latest_edit" - bl_label = "Load edit" - bl_description = ( - "Loads latest edit from shot_preview_folder " - "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - return cls.can_load_edit(context) - - @classmethod - def description(cls, context, properties): - if cls.can_load_edit(context): - return "Load latest edit from shared folder" - else: - return "Shared folder not set, or VSE area not available in this workspace" - - def execute(self, context: bpy.types.Context) -> Set[str]: - addon_prefs = prefs.addon_prefs_get(context) - edit_export_path = Path(addon_prefs.edit_export_dir) - strip_channel = 1 - latest_file = self._get_latest_edit(context) - if not latest_file: - self.report( - {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" - ) - strip_filepath = latest_file.as_posix() - strip_frame_start = 101 - - # Needs to be run in sequence editor area. - # area_override = None - scene = context.scene - if not scene.sequence_editor: - scene.sequence_editor_create() - seq_editor = scene.sequence_editor - strip = seq_editor.sequences.new_movie( - strip_filepath, - strip_filepath, - strip_channel + 1, - strip_frame_start, - fit_method="FIT", - ) - sound_strip = seq_editor.sequences.new_sound( - strip_filepath, - strip_filepath, - strip_channel, - strip_frame_start, - ) - - bpy.ops.kitsu.con_detect_context() - shot = cache.shot_active_get() - - - # Update shift frame range prop. - frame_in = shot.frame_in - frame_3d_offset = 101 - - # Set sequence strip start kitsu data. - for strip in scene.sequence_editor.sequences_all: - strip.frame_start = -(frame_in) + frame_3d_offset #TODO CONFIRM LOGIC HERE - - self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") - - return {"FINISHED"} - - def _get_latest_edit(self, context: bpy.types.Context): - addon_prefs = prefs.addon_prefs_get(context) - - edit_export_path = Path(addon_prefs.edit_export_dir) - - files_list = [ - f - for f in edit_export_path.iterdir() - if f.is_file() and self._is_valid_edit_name(f.name) - ] - files_list = sorted(files_list, reverse=True) - - return files_list[0] - - def _is_valid_edit_name(self, filename: str) -> bool: - pattern = r"petprojects_v\d\d\d.mp4" - - match = re.search(pattern, filename) - if match: - return True - return False - - -classes = [ - ANIM_SETUP_OT_setup_workspaces, - ANIM_SETUP_OT_load_latest_edit, -] - - -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/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 8225d485..35a3d780 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -40,6 +40,8 @@ from blender_kitsu.auth.ops import ( ) from blender_kitsu.context.ops import KITSU_OT_con_productions_load from blender_kitsu.lookdev.prefs import LOOKDEV_preferences +from blender_kitsu.shot_builder.anim_setup.core import editorial_export_check_latest + logger = LoggerFactory.getLogger() @@ -158,28 +160,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): def init_playblast_file_model(self, context: bpy.types.Context) -> None: ops_playblast_data.init_playblast_file_model(context) - - def init_editoral_export_directory(self, context:bpy.types.Context) -> None: - edit_export_path = Path(self.edit_export_dir) - - files_list = [ - f - for f in edit_export_path.iterdir() - if f.is_file() - ] - - files_list = sorted(files_list, reverse=True) - valid_file = False - for item in files_list: - match = re.search(self.edit_export_file_pattern, item._str) - if match: - valid_file = True - if not valid_file: - self.edit_export_file_pattern = "" - logger.error( - "Failed to initialize editorial export file model. Invalid path/pattern. Check addon preferences" - ) - logger.info("Initialized editorial export file model, successfully.") bl_idname = __package__ @@ -333,7 +313,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): options={"HIDDEN", "SKIP_SAVE"}, description="File pattern to search for latest editorial export. Typically '{proj_name}_v\d\d\d.mp4'", default="petprojects_v\d\d\d.mp4", - update=init_editoral_export_directory, ) @@ -505,29 +484,13 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): @property def is_editorial_dir_valid(self) -> bool: - edit_export_path = Path(self.edit_export_dir) - - files_list = [ - f - for f in edit_export_path.iterdir() - if f.is_file() - ] - - files_list = sorted(files_list, reverse=True) - valid_file = False - for item in files_list: - match = re.search(self.edit_export_file_pattern, item._str) - if match: - valid_file = True - if not valid_file: + if editorial_export_check_latest(bpy.context) is None: logger.error( "Failed to initialize editorial export file model. Invalid path/pattern. Check addon preferences" ) return False - logger.info("Initialized editorial export file model, successfully.") return True - def session_get(context: bpy.types.Context) -> Session: """ Shortcut to get session from blender_kitsu addon preferences diff --git a/blender_kitsu/shot_builder/__init__.py b/blender_kitsu/shot_builder/__init__.py index cb825046..f632ff83 100644 --- a/blender_kitsu/shot_builder/__init__.py +++ b/blender_kitsu/shot_builder/__init__.py @@ -22,6 +22,7 @@ from blender_kitsu.shot_builder.ui import * from blender_kitsu.shot_builder.connectors.kitsu import * from blender_kitsu.shot_builder.operators import * import bpy +from blender_kitsu.shot_builder.anim_setup import ops #TODO Fix Registraion # import logging # logging.basicConfig(level=logging.DEBUG) @@ -48,9 +49,11 @@ def 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(): 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/blender_kitsu/shot_builder/anim_setup/core.py b/blender_kitsu/shot_builder/anim_setup/core.py new file mode 100644 index 00000000..2ac95bb6 --- /dev/null +++ b/blender_kitsu/shot_builder/anim_setup/core.py @@ -0,0 +1,83 @@ +import bpy +import re +from pathlib import Path +from typing import Set +from blender_kitsu import prefs +from blender_kitsu import cache + + +def animation_workspace_delete_others(self, context:bpy.types.Context): + """Delete any workspace that is not an animation workspace""" + for ws in bpy.data.workspaces: + if ws.name != "Animation": + bpy.ops.workspace.delete({"workspace": ws}) + self.report({"INFO"}, "Deleted non Animation workspaces") + +def editorial_export_get_latest(self, context:bpy.types.Context): + """Loads latest export from editorial department""" + addon_prefs = prefs.addon_prefs_get(context) + edit_export_path = Path(addon_prefs.edit_export_dir) + strip_channel = 1 + latest_file = editorial_export_check_latest(context) + if not latest_file: + self.report( + {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" + ) + strip_filepath = latest_file.as_posix() + strip_frame_start = addon_prefs.shot_builder_frame_offset + + scene = context.scene + if not scene.sequence_editor: + scene.sequence_editor_create() + seq_editor = scene.sequence_editor + movie_strip = seq_editor.sequences.new_movie( + strip_filepath, + strip_filepath, + strip_channel + 1, + strip_frame_start, + fit_method="FIT", + ) + sound_strip = seq_editor.sequences.new_sound( + strip_filepath, + strip_filepath, + strip_channel, + strip_frame_start, + ) + shot = cache.shot_active_get() + + + # Update shift frame range prop. + frame_in = shot.frame_in + frame_3d_offset = addon_prefs.shot_builder_frame_offset + + # Set sequence strip start kitsu data. + for strip in scene.sequence_editor.sequences_all: + strip.frame_start = -(frame_in) + frame_3d_offset #TODO CONFIRM LOGIC HERE + + self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") + + +def editorial_export_check_latest(context: bpy.types.Context): + """Find latest export in editorial export directory""" + addon_prefs = prefs.addon_prefs_get(context) + + edit_export_path = Path(addon_prefs.edit_export_dir) + + files_list = [ + f + for f in edit_export_path.iterdir() + if f.is_file() and editorial_export_is_valid_edit_name(addon_prefs.edit_export_file_pattern, f.name) + ] + if len(files_list) >= 1: + files_list = sorted(files_list, reverse=True) + return files_list[0] + return None + + +def editorial_export_is_valid_edit_name(file_pattern:str, filename: str) -> bool: + """Verify file name matches file pattern set in preferences""" + match = re.search(file_pattern, filename) + if match: + return True + return False + diff --git a/blender_kitsu/shot_builder/anim_setup/ops.py b/blender_kitsu/shot_builder/anim_setup/ops.py new file mode 100644 index 00000000..71eba306 --- /dev/null +++ b/blender_kitsu/shot_builder/anim_setup/ops.py @@ -0,0 +1,49 @@ +import bpy +from typing import Set +from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, split_viewport + + +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) + return {"FINISHED"} + +class ANIM_SETUP_OT_split_viewport(bpy.types.Operator): + bl_idname = "anim_setup.split_viewport" + bl_label = "Split Viewport" + bl_description = "Split smallest 3D View in current workspace" + + def execute(self, context: bpy.types.Context) -> Set[str]: + split_viewport(self, context) + return {"FINISHED"} + +class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): + bl_idname = "asset_setup.load_latest_edit" + bl_label = "Load edit" + bl_description = ( + "Loads latest edit from shot_preview_folder " + "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" + ) + + def execute(self, context: bpy.types.Context) -> Set[str]: + editorial_export_get_latest(self, context) + return {"FINISHED"} + +classes = [ + ANIM_SETUP_OT_setup_workspaces, + ANIM_SETUP_OT_load_latest_edit, + ANIM_SETUP_OT_split_viewport +] + + +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/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 1c84b89f..c0044306 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -24,6 +24,7 @@ from blender_kitsu.shot_builder.project import ensure_loaded_production, get_act from blender_kitsu.shot_builder.builder import ShotBuilder from blender_kitsu.shot_builder.task_type import TaskType from blender_kitsu import prefs, cache +from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others _production_task_type_items: List[Tuple[str, str, str]] = [] @@ -122,6 +123,11 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): {'ERROR'}, "Operator is not able to determine the project root directory. Check project root directiory is configured in 'Blender Kitsu' addon preferences.") return {'CANCELLED'} + if not addon_prefs.is_editorial_dir_valid: + self.report( + {'ERROR'}, "Shot builder is dependant on a valid editorial export path and file pattern. Check Preferences, errors appear in console") + return {'CANCELLED'} + self.production_root = addon_prefs.project_root_dir self.production_name = project.name @@ -151,13 +157,6 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): {'ERROR'}, "Shot builder can only be started from the File menu. Shortcuts like CTRL-N don't work") return {'CANCELLED'} addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences - - if not addon_prefs.is_editorial_dir_valid: - self.report( - {'ERROR'}, "Shot builder is dependant on a valid editorial export path and file pattern. Check Preferences, errors appear in console") - return {'CANCELLED'} - - ensure_loaded_production(context) production = get_active_production() shot_builder = ShotBuilder( @@ -165,9 +164,10 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): shot_builder.create_build_steps() shot_builder.build() #Load EDIT - bpy.ops.asset_setup.load_latest_edit() + bpy.ops.kitsu.con_detect_context() #TODO CONFIRM AND CHECK IF OVERRIDE IS NEEDED + editorial_export_get_latest(self, context) # Load Anim Workspace - bpy.ops.anim_setup.setup_workspaces() + animation_workspace_delete_others(self, context) shot = cache.shot_active_get() # Initilize armatures -- 2.30.2 From 8bc00a44b2382e040b039148b29d959eaac57114 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 10:00:47 -0400 Subject: [PATCH 11/25] [Blender Kitsu] add VSE area to animation workspace WIP - Add animation_workspace_vse_area_add() function - Comment out function call in operator - Add TODO to fix UI dependant call --- blender_kitsu/shot_builder/anim_setup/core.py | 14 ++++++++++++++ blender_kitsu/shot_builder/anim_setup/ops.py | 10 +++++----- blender_kitsu/shot_builder/operators.py | 5 ++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/blender_kitsu/shot_builder/anim_setup/core.py b/blender_kitsu/shot_builder/anim_setup/core.py index 2ac95bb6..0796db4e 100644 --- a/blender_kitsu/shot_builder/anim_setup/core.py +++ b/blender_kitsu/shot_builder/anim_setup/core.py @@ -6,6 +6,20 @@ from blender_kitsu import prefs from blender_kitsu import cache +def animation_workspace_vse_area_add(self, 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" + print(f"splitting viewpoert in workspace {context.workspace.name}") #USING THIS TO DEBUG + def animation_workspace_delete_others(self, context:bpy.types.Context): """Delete any workspace that is not an animation workspace""" for ws in bpy.data.workspaces: diff --git a/blender_kitsu/shot_builder/anim_setup/ops.py b/blender_kitsu/shot_builder/anim_setup/ops.py index 71eba306..ee9c11f3 100644 --- a/blender_kitsu/shot_builder/anim_setup/ops.py +++ b/blender_kitsu/shot_builder/anim_setup/ops.py @@ -1,6 +1,6 @@ import bpy from typing import Set -from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, split_viewport +from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, animation_workspace_vse_area_add class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): @@ -12,13 +12,13 @@ class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): animation_workspace_delete_others(self, context) return {"FINISHED"} -class ANIM_SETUP_OT_split_viewport(bpy.types.Operator): - bl_idname = "anim_setup.split_viewport" +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]: - split_viewport(self, context) + animation_workspace_vse_area_add(self, context) return {"FINISHED"} class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): @@ -36,7 +36,7 @@ class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): classes = [ ANIM_SETUP_OT_setup_workspaces, ANIM_SETUP_OT_load_latest_edit, - ANIM_SETUP_OT_split_viewport + ANIM_SETUP_OT_animation_workspace_vse_area_add ] diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index c0044306..abefa3bf 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -24,7 +24,7 @@ from blender_kitsu.shot_builder.project import ensure_loaded_production, get_act from blender_kitsu.shot_builder.builder import ShotBuilder from blender_kitsu.shot_builder.task_type import TaskType from blender_kitsu import prefs, cache -from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others +from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, animation_workspace_vse_area_add _production_task_type_items: List[Tuple[str, str, str]] = [] @@ -181,6 +181,9 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): context.scene.frame_start = addon_prefs.shot_builder_frame_offset context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset + # Show Storyboard/Animatic from VSE + #animation_workspace_vse_area_add(self, context) # TODO FIX BECAUSE THIS DOESN"T RUN https://docs.blender.org/api/current/info_gotcha.html#no-updates-after-changing-ui-context + return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: -- 2.30.2 From eaafcb5644085eb87f0fd9ea90dbfd92be9b8c19 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 12:16:26 -0400 Subject: [PATCH 12/25] [Blender Kitsu] Fix 'add vse area' by making operator modal - main shot_builder function to modal --- blender_kitsu/shot_builder/operators.py | 81 +++++++++++++++---------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index abefa3bf..3b4d36a5 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -74,6 +74,10 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): bl_idname = "shotbuilder.new_shot_file" bl_label = "New Production Shot File" + _timer = None + _built_shot = False + _add_vse_area = False + production_root: bpy.props.StringProperty( # type: ignore name="Production Root", description="Root of the production", @@ -104,6 +108,16 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): items=production_task_type_items ) + 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""" + animation_workspace_vse_area_add(self, context) + self._add_vse_area = True + 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() @@ -152,39 +166,42 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): return cast(Set[str], context.window_manager.invoke_props_dialog(self, width=400)) def execute(self, context: bpy.types.Context) -> Set[str]: - 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'} - addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences - 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() - #Load EDIT - bpy.ops.kitsu.con_detect_context() #TODO CONFIRM AND CHECK IF OVERRIDE IS NEEDED - editorial_export_get_latest(self, context) - # Load Anim Workspace - animation_workspace_delete_others(self, context) - shot = cache.shot_active_get() + wm = context.window_manager + self._timer = wm.event_timer_add(0.1, window=context.window) + wm.modal_handler_add(self) + if not self._built_shot: + 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'} + addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences + 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() + #Load EDIT + bpy.ops.kitsu.con_detect_context() #TODO CONFIRM AND CHECK IF OVERRIDE IS NEEDED + editorial_export_get_latest(self, context) + # Load Anim Workspace + animation_workspace_delete_others(self, context) + shot = cache.shot_active_get() - # 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}.{shot.name}.v001") - new_action.use_fake_user = True - obj.animation_data.action = new_action - - # Set Shot Frame Range - context.scene.frame_start = addon_prefs.shot_builder_frame_offset - context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset - - # Show Storyboard/Animatic from VSE - #animation_workspace_vse_area_add(self, context) # TODO FIX BECAUSE THIS DOESN"T RUN https://docs.blender.org/api/current/info_gotcha.html#no-updates-after-changing-ui-context - - return {'FINISHED'} + # 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}.{shot.name}.v001") + new_action.use_fake_user = True + obj.animation_data.action = new_action + + # Set Shot Frame Range + context.scene.frame_start = addon_prefs.shot_builder_frame_offset + context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset + self._built_shot = True + return {'RUNNING_MODAL'} + if self._built_shot and self._add_vse_area: + return {'FINISHED'} def draw(self, context: bpy.types.Context) -> None: layout = self.layout -- 2.30.2 From 6829a61ea4c4b148769750a9baa5884314ab4cc6 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 12:30:55 -0400 Subject: [PATCH 13/25] [Blender_Kitsu] move sequence strip, restore logic - restore original formula to offset sequence strip - restore original variables to offset sequence strip --- blender_kitsu/shot_builder/anim_setup/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blender_kitsu/shot_builder/anim_setup/core.py b/blender_kitsu/shot_builder/anim_setup/core.py index 0796db4e..5ef556ce 100644 --- a/blender_kitsu/shot_builder/anim_setup/core.py +++ b/blender_kitsu/shot_builder/anim_setup/core.py @@ -62,11 +62,12 @@ def editorial_export_get_latest(self, context:bpy.types.Context): # Update shift frame range prop. frame_in = shot.frame_in - frame_3d_offset = addon_prefs.shot_builder_frame_offset + frame_3d_in = shot.data["3d_in"] + frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset # Set sequence strip start kitsu data. for strip in scene.sequence_editor.sequences_all: - strip.frame_start = -(frame_in) + frame_3d_offset #TODO CONFIRM LOGIC HERE + strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") -- 2.30.2 From f1f815a648726d39e5636a57fe099d882c20b77f Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 13:24:42 -0400 Subject: [PATCH 14/25] [Blender_Kitsu] create editorial sub-module - move functions related to importing references into editorial module from the anim_setup module - nest editorial under shot_builder module --- blender_kitsu/prefs.py | 2 +- blender_kitsu/shot_builder/__init__.py | 11 ++- blender_kitsu/shot_builder/anim_setup/core.py | 69 ----------------- blender_kitsu/shot_builder/anim_setup/ops.py | 18 +---- .../shot_builder/editorial/__init__.py | 30 ++++++++ blender_kitsu/shot_builder/editorial/core.py | 75 +++++++++++++++++++ blender_kitsu/shot_builder/editorial/ops.py | 28 +++++++ blender_kitsu/shot_builder/operators.py | 4 +- 8 files changed, 148 insertions(+), 89 deletions(-) create mode 100644 blender_kitsu/shot_builder/editorial/__init__.py create mode 100644 blender_kitsu/shot_builder/editorial/core.py create mode 100644 blender_kitsu/shot_builder/editorial/ops.py diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index 35a3d780..ea86e567 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -40,7 +40,7 @@ from blender_kitsu.auth.ops import ( ) from blender_kitsu.context.ops import KITSU_OT_con_productions_load from blender_kitsu.lookdev.prefs import LOOKDEV_preferences -from blender_kitsu.shot_builder.anim_setup.core import editorial_export_check_latest +from blender_kitsu.shot_builder.editorial.core import editorial_export_check_latest logger = LoggerFactory.getLogger() diff --git a/blender_kitsu/shot_builder/__init__.py b/blender_kitsu/shot_builder/__init__.py index f632ff83..19cd677e 100644 --- a/blender_kitsu/shot_builder/__init__.py +++ b/blender_kitsu/shot_builder/__init__.py @@ -22,7 +22,8 @@ from blender_kitsu.shot_builder.ui import * from blender_kitsu.shot_builder.connectors.kitsu import * from blender_kitsu.shot_builder.operators import * import bpy -from blender_kitsu.shot_builder.anim_setup import ops #TODO Fix Registraion +from blender_kitsu.shot_builder.anim_setup import ops as anim_setup_ops #TODO Fix Registraion +from blender_kitsu.shot_builder.editorial import ops as editorial_ops #TODO Fix Registraion # import logging # logging.basicConfig(level=logging.DEBUG) @@ -46,14 +47,18 @@ classes = ( 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/blender_kitsu/shot_builder/anim_setup/core.py b/blender_kitsu/shot_builder/anim_setup/core.py index 5ef556ce..0992547d 100644 --- a/blender_kitsu/shot_builder/anim_setup/core.py +++ b/blender_kitsu/shot_builder/anim_setup/core.py @@ -18,7 +18,6 @@ def animation_workspace_vse_area_add(self, context:bpy.types.Context): 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" - print(f"splitting viewpoert in workspace {context.workspace.name}") #USING THIS TO DEBUG def animation_workspace_delete_others(self, context:bpy.types.Context): """Delete any workspace that is not an animation workspace""" @@ -27,72 +26,4 @@ def animation_workspace_delete_others(self, context:bpy.types.Context): bpy.ops.workspace.delete({"workspace": ws}) self.report({"INFO"}, "Deleted non Animation workspaces") -def editorial_export_get_latest(self, context:bpy.types.Context): - """Loads latest export from editorial department""" - addon_prefs = prefs.addon_prefs_get(context) - edit_export_path = Path(addon_prefs.edit_export_dir) - strip_channel = 1 - latest_file = editorial_export_check_latest(context) - if not latest_file: - self.report( - {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" - ) - strip_filepath = latest_file.as_posix() - strip_frame_start = addon_prefs.shot_builder_frame_offset - - scene = context.scene - if not scene.sequence_editor: - scene.sequence_editor_create() - seq_editor = scene.sequence_editor - movie_strip = seq_editor.sequences.new_movie( - strip_filepath, - strip_filepath, - strip_channel + 1, - strip_frame_start, - fit_method="FIT", - ) - sound_strip = seq_editor.sequences.new_sound( - strip_filepath, - strip_filepath, - strip_channel, - strip_frame_start, - ) - shot = cache.shot_active_get() - - - # Update shift frame range prop. - frame_in = shot.frame_in - frame_3d_in = shot.data["3d_in"] - frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset - - # Set sequence strip start kitsu data. - for strip in scene.sequence_editor.sequences_all: - strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset - - self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") - - -def editorial_export_check_latest(context: bpy.types.Context): - """Find latest export in editorial export directory""" - addon_prefs = prefs.addon_prefs_get(context) - - edit_export_path = Path(addon_prefs.edit_export_dir) - - files_list = [ - f - for f in edit_export_path.iterdir() - if f.is_file() and editorial_export_is_valid_edit_name(addon_prefs.edit_export_file_pattern, f.name) - ] - if len(files_list) >= 1: - files_list = sorted(files_list, reverse=True) - return files_list[0] - return None - - -def editorial_export_is_valid_edit_name(file_pattern:str, filename: str) -> bool: - """Verify file name matches file pattern set in preferences""" - match = re.search(file_pattern, filename) - if match: - return True - return False diff --git a/blender_kitsu/shot_builder/anim_setup/ops.py b/blender_kitsu/shot_builder/anim_setup/ops.py index ee9c11f3..dc7e761b 100644 --- a/blender_kitsu/shot_builder/anim_setup/ops.py +++ b/blender_kitsu/shot_builder/anim_setup/ops.py @@ -1,8 +1,7 @@ import bpy from typing import Set -from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, animation_workspace_vse_area_add - - +from blender_kitsu.shot_builder.anim_setup.core import animation_workspace_delete_others, animation_workspace_vse_area_add +from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): bl_idname = "anim_setup.setup_workspaces" bl_label = "Setup Workspace" @@ -21,21 +20,10 @@ class ANIM_SETUP_OT_animation_workspace_vse_area_add(bpy.types.Operator): animation_workspace_vse_area_add(self, context) return {"FINISHED"} -class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): - bl_idname = "asset_setup.load_latest_edit" - bl_label = "Load edit" - bl_description = ( - "Loads latest edit from shot_preview_folder " - "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" - ) - - def execute(self, context: bpy.types.Context) -> Set[str]: - editorial_export_get_latest(self, context) - return {"FINISHED"} +# classes = [ ANIM_SETUP_OT_setup_workspaces, - ANIM_SETUP_OT_load_latest_edit, ANIM_SETUP_OT_animation_workspace_vse_area_add ] diff --git a/blender_kitsu/shot_builder/editorial/__init__.py b/blender_kitsu/shot_builder/editorial/__init__.py new file mode 100644 index 00000000..527a061a --- /dev/null +++ b/blender_kitsu/shot_builder/editorial/__init__.py @@ -0,0 +1,30 @@ +# ##### 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 blender_kitsu.shot_builder.editorial import ops + + +def register(): + ops.register() + + +def unregister(): + ops.unregister() diff --git a/blender_kitsu/shot_builder/editorial/core.py b/blender_kitsu/shot_builder/editorial/core.py new file mode 100644 index 00000000..a4585c97 --- /dev/null +++ b/blender_kitsu/shot_builder/editorial/core.py @@ -0,0 +1,75 @@ +import bpy +import re +from pathlib import Path +from typing import Set +from blender_kitsu import prefs +from blender_kitsu import cache + +def editorial_export_get_latest(self, context:bpy.types.Context): + """Loads latest export from editorial department""" + addon_prefs = prefs.addon_prefs_get(context) + edit_export_path = Path(addon_prefs.edit_export_dir) + strip_channel = 1 + latest_file = editorial_export_check_latest(context) + if not latest_file: + self.report( + {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" + ) + strip_filepath = latest_file.as_posix() + strip_frame_start = addon_prefs.shot_builder_frame_offset + + scene = context.scene + if not scene.sequence_editor: + scene.sequence_editor_create() + seq_editor = scene.sequence_editor + movie_strip = seq_editor.sequences.new_movie( + strip_filepath, + strip_filepath, + strip_channel + 1, + strip_frame_start, + fit_method="FIT", + ) + sound_strip = seq_editor.sequences.new_sound( + strip_filepath, + strip_filepath, + strip_channel, + strip_frame_start, + ) + shot = cache.shot_active_get() + + + # Update shift frame range prop. + frame_in = shot.frame_in + frame_3d_in = shot.data["3d_in"] + frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset + + # Set sequence strip start kitsu data. + for strip in scene.sequence_editor.sequences_all: + strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset + + self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") + + +def editorial_export_check_latest(context: bpy.types.Context): + """Find latest export in editorial export directory""" + addon_prefs = prefs.addon_prefs_get(context) + + edit_export_path = Path(addon_prefs.edit_export_dir) + + files_list = [ + f + for f in edit_export_path.iterdir() + if f.is_file() and editorial_export_is_valid_edit_name(addon_prefs.edit_export_file_pattern, f.name) + ] + if len(files_list) >= 1: + files_list = sorted(files_list, reverse=True) + return files_list[0] + return None + + +def editorial_export_is_valid_edit_name(file_pattern:str, filename: str) -> bool: + """Verify file name matches file pattern set in preferences""" + match = re.search(file_pattern, filename) + if match: + return True + return False diff --git a/blender_kitsu/shot_builder/editorial/ops.py b/blender_kitsu/shot_builder/editorial/ops.py new file mode 100644 index 00000000..ffa6dd03 --- /dev/null +++ b/blender_kitsu/shot_builder/editorial/ops.py @@ -0,0 +1,28 @@ +import bpy +from typing import Set +from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest + +class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): + bl_idname = "asset_setup.load_latest_edit" + bl_label = "Load edit" + bl_description = ( + "Loads latest edit from shot_preview_folder " + "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" + ) + + def execute(self, context: bpy.types.Context) -> Set[str]: + editorial_export_get_latest(self, context) + return {"FINISHED"} + +classes = [ + ANIM_SETUP_OT_load_latest_edit, +] + + +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/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 3b4d36a5..7d5c316f 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -24,7 +24,9 @@ from blender_kitsu.shot_builder.project import ensure_loaded_production, get_act from blender_kitsu.shot_builder.builder import ShotBuilder from blender_kitsu.shot_builder.task_type import TaskType from blender_kitsu import prefs, cache -from blender_kitsu.shot_builder.anim_setup.core import editorial_export_get_latest, animation_workspace_delete_others, animation_workspace_vse_area_add +from blender_kitsu.shot_builder.anim_setup.core import animation_workspace_delete_others, animation_workspace_vse_area_add +from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest + _production_task_type_items: List[Tuple[str, str, str]] = [] -- 2.30.2 From 44cb913195552d9831de13fc7f48ddc246a32f51 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 13:29:20 -0400 Subject: [PATCH 15/25] [Blender_Kitsu] Editorial: improve error messages - move report outside of core function - return none if no shot is found but server is connected --- blender_kitsu/shot_builder/editorial/core.py | 23 ++++++++++---------- blender_kitsu/shot_builder/editorial/ops.py | 17 ++++++++++----- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/blender_kitsu/shot_builder/editorial/core.py b/blender_kitsu/shot_builder/editorial/core.py index a4585c97..befb9edf 100644 --- a/blender_kitsu/shot_builder/editorial/core.py +++ b/blender_kitsu/shot_builder/editorial/core.py @@ -5,16 +5,18 @@ from typing import Set from blender_kitsu import prefs from blender_kitsu import cache -def editorial_export_get_latest(self, context:bpy.types.Context): +def editorial_export_get_latest(context:bpy.types.Context) -> list[bpy.types.Sequence]: """Loads latest export from editorial department""" addon_prefs = prefs.addon_prefs_get(context) edit_export_path = Path(addon_prefs.edit_export_dir) strip_channel = 1 latest_file = editorial_export_check_latest(context) if not latest_file: - self.report( - {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" - ) + return None + shot = cache.shot_active_get() + # Check if Kitsu server returned empty shot + if shot.id == '': + return None strip_filepath = latest_file.as_posix() strip_frame_start = addon_prefs.shot_builder_frame_offset @@ -23,31 +25,30 @@ def editorial_export_get_latest(self, context:bpy.types.Context): scene.sequence_editor_create() seq_editor = scene.sequence_editor movie_strip = seq_editor.sequences.new_movie( - strip_filepath, + latest_file.name, strip_filepath, strip_channel + 1, strip_frame_start, fit_method="FIT", ) sound_strip = seq_editor.sequences.new_sound( - strip_filepath, + latest_file.name, strip_filepath, strip_channel, strip_frame_start, ) - shot = cache.shot_active_get() - - + new_strips = [movie_strip, sound_strip] + # Update shift frame range prop. frame_in = shot.frame_in frame_3d_in = shot.data["3d_in"] frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset # Set sequence strip start kitsu data. - for strip in scene.sequence_editor.sequences_all: + for strip in new_strips: strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset + return new_strips - self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") def editorial_export_check_latest(context: bpy.types.Context): diff --git a/blender_kitsu/shot_builder/editorial/ops.py b/blender_kitsu/shot_builder/editorial/ops.py index ffa6dd03..e3470bef 100644 --- a/blender_kitsu/shot_builder/editorial/ops.py +++ b/blender_kitsu/shot_builder/editorial/ops.py @@ -2,20 +2,27 @@ import bpy from typing import Set from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest -class ANIM_SETUP_OT_load_latest_edit(bpy.types.Operator): - bl_idname = "asset_setup.load_latest_edit" - bl_label = "Load edit" +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_in metadata shot key from Kitsu" ) def execute(self, context: bpy.types.Context) -> Set[str]: - editorial_export_get_latest(self, context) + strips = editorial_export_get_latest(self, context) + 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_edit, + ANIM_SETUP_OT_load_latest_editorial, ] -- 2.30.2 From 2376381b2867f28be09ab8b6a312953dc82bbc41 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 13:46:33 -0400 Subject: [PATCH 16/25] [Blender_Kitsu] anim_setup improve error messages --- blender_kitsu/shot_builder/anim_setup/core.py | 6 +++--- blender_kitsu/shot_builder/anim_setup/ops.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blender_kitsu/shot_builder/anim_setup/core.py b/blender_kitsu/shot_builder/anim_setup/core.py index 0992547d..3e6a2054 100644 --- a/blender_kitsu/shot_builder/anim_setup/core.py +++ b/blender_kitsu/shot_builder/anim_setup/core.py @@ -6,7 +6,7 @@ from blender_kitsu import prefs from blender_kitsu import cache -def animation_workspace_vse_area_add(self, context:bpy.types.Context): +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 @@ -19,11 +19,11 @@ def animation_workspace_vse_area_add(self, context:bpy.types.Context): small_view_3d.ui_type = "SEQUENCE_EDITOR" small_view_3d.spaces[0].view_type = "PREVIEW" -def animation_workspace_delete_others(self, context:bpy.types.Context): +def animation_workspace_delete_others(): """Delete any workspace that is not an animation workspace""" for ws in bpy.data.workspaces: if ws.name != "Animation": bpy.ops.workspace.delete({"workspace": ws}) - self.report({"INFO"}, "Deleted non Animation workspaces") + diff --git a/blender_kitsu/shot_builder/anim_setup/ops.py b/blender_kitsu/shot_builder/anim_setup/ops.py index dc7e761b..75b4dd31 100644 --- a/blender_kitsu/shot_builder/anim_setup/ops.py +++ b/blender_kitsu/shot_builder/anim_setup/ops.py @@ -1,7 +1,6 @@ import bpy from typing import Set from blender_kitsu.shot_builder.anim_setup.core import animation_workspace_delete_others, animation_workspace_vse_area_add -from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): bl_idname = "anim_setup.setup_workspaces" bl_label = "Setup Workspace" @@ -9,6 +8,7 @@ class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator): 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): -- 2.30.2 From 1175584c49f73bcc5a9b67799c3a9a1fffb6541b Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 13:53:55 -0400 Subject: [PATCH 17/25] [Blender_Kitsu] Improve Shot_Builder op and add success message --- blender_kitsu/shot_builder/operators.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 7d5c316f..218860b2 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -111,13 +111,19 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): ) 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""" - animation_workspace_vse_area_add(self, context) + animation_workspace_vse_area_add(context) self._add_vse_area = True + + if self._built_shot and self._add_vse_area: + self.report({"INFO"}, f"Created Shot {self.shot_id}") + return {'FINISHED'} + return {'PASS_THROUGH'} def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: @@ -185,9 +191,9 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): shot_builder.build() #Load EDIT bpy.ops.kitsu.con_detect_context() #TODO CONFIRM AND CHECK IF OVERRIDE IS NEEDED - editorial_export_get_latest(self, context) + editorial_export_get_latest(context) # Load Anim Workspace - animation_workspace_delete_others(self, context) + animation_workspace_delete_others() shot = cache.shot_active_get() # Initilize armatures @@ -202,8 +208,7 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset self._built_shot = True return {'RUNNING_MODAL'} - if self._built_shot and self._add_vse_area: - return {'FINISHED'} + def draw(self, context: bpy.types.Context) -> None: layout = self.layout -- 2.30.2 From 2e58de49c28a4d33347726c8452095dddafc438c Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 15:26:05 -0400 Subject: [PATCH 18/25] [Blender_Kitsu] Shot Builder, save only at end of operation - don't save in the middle of the function call - extract save logic and move to main operator - build gazu context manually without relying on file path context - use gazu.shot in editorial standalone operator - reorganize main operator --- .../shot_builder/builder/__init__.py | 3 - blender_kitsu/shot_builder/editorial/core.py | 9 +-- blender_kitsu/shot_builder/editorial/ops.py | 5 +- blender_kitsu/shot_builder/operators.py | 74 +++++++++++-------- 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/blender_kitsu/shot_builder/builder/__init__.py b/blender_kitsu/shot_builder/builder/__init__.py index 056ea561..a072e65d 100644 --- a/blender_kitsu/shot_builder/builder/__init__.py +++ b/blender_kitsu/shot_builder/builder/__init__.py @@ -7,7 +7,6 @@ from blender_kitsu.shot_builder.builder.init_shot import InitShotStep from blender_kitsu.shot_builder.builder.set_render_settings import SetRenderSettingsStep from blender_kitsu.shot_builder.builder.new_scene import NewSceneStep from blender_kitsu.shot_builder.builder.invoke_hook import InvokeHookStep -from blender_kitsu.shot_builder.builder.save_file import SaveFileStep import bpy @@ -76,8 +75,6 @@ class ShotBuilder: for hook in production.hooks.filter(match_task_type=task_type.name, match_asset_type=asset.asset_type): self._steps.append(InvokeHookStep(hook)) - self._steps.append(SaveFileStep()) - def build(self) -> None: num_steps = len(self._steps) step_number = 1 diff --git a/blender_kitsu/shot_builder/editorial/core.py b/blender_kitsu/shot_builder/editorial/core.py index befb9edf..757adbb0 100644 --- a/blender_kitsu/shot_builder/editorial/core.py +++ b/blender_kitsu/shot_builder/editorial/core.py @@ -5,7 +5,7 @@ from typing import Set from blender_kitsu import prefs from blender_kitsu import cache -def editorial_export_get_latest(context:bpy.types.Context) -> list[bpy.types.Sequence]: +def editorial_export_get_latest(context:bpy.types.Context, shot) -> list[bpy.types.Sequence]: #TODO add info to shot """Loads latest export from editorial department""" addon_prefs = prefs.addon_prefs_get(context) edit_export_path = Path(addon_prefs.edit_export_dir) @@ -13,9 +13,8 @@ def editorial_export_get_latest(context:bpy.types.Context) -> list[bpy.types.Seq latest_file = editorial_export_check_latest(context) if not latest_file: return None - shot = cache.shot_active_get() # Check if Kitsu server returned empty shot - if shot.id == '': + if shot.get("id") == '': return None strip_filepath = latest_file.as_posix() strip_frame_start = addon_prefs.shot_builder_frame_offset @@ -40,8 +39,8 @@ def editorial_export_get_latest(context:bpy.types.Context) -> list[bpy.types.Seq new_strips = [movie_strip, sound_strip] # Update shift frame range prop. - frame_in = shot.frame_in - frame_3d_in = shot.data["3d_in"] + frame_in = shot["data"].get("frame_in") + frame_3d_in = shot["data"].get("3d_in") frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset # Set sequence strip start kitsu data. diff --git a/blender_kitsu/shot_builder/editorial/ops.py b/blender_kitsu/shot_builder/editorial/ops.py index e3470bef..30bd4530 100644 --- a/blender_kitsu/shot_builder/editorial/ops.py +++ b/blender_kitsu/shot_builder/editorial/ops.py @@ -1,6 +1,7 @@ import bpy from typing import Set from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest +from blender_kitsu import cache, gazu class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator): bl_idname = "asset_setup.load_latest_editorial" @@ -11,7 +12,9 @@ class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator): ) def execute(self, context: bpy.types.Context) -> Set[str]: - strips = editorial_export_get_latest(self, context) + cache_shot = cache.shot_active_get() + shot = gazu.shot.get_shot(cache_shot.id) + strips = editorial_export_get_latest(context, shot) if strips is None: self.report( {"ERROR"}, f"No valid editorial export in editorial export path." diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 218860b2..0e1d9d10 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -17,13 +17,14 @@ # ##### END GPL LICENSE BLOCK ##### # +import pathlib from typing import * import bpy from blender_kitsu.shot_builder.shot import ShotRef from blender_kitsu.shot_builder.project import ensure_loaded_production, get_active_production from blender_kitsu.shot_builder.builder import ShotBuilder from blender_kitsu.shot_builder.task_type import TaskType -from blender_kitsu import prefs, cache +from blender_kitsu import prefs, cache, gazu from blender_kitsu.shot_builder.anim_setup.core import animation_workspace_delete_others, animation_workspace_vse_area_add from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest @@ -79,6 +80,7 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): _timer = None _built_shot = False _add_vse_area = False + _file_path = '' production_root: bpy.props.StringProperty( # type: ignore name="Production Root", @@ -121,6 +123,9 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): self._add_vse_area = True if self._built_shot and self._add_vse_area: + file_path = pathlib.Path(self._file_path) + file_path.mkdir(parents=True, exist_ok=True) + bpy.ops.wm.save_mainfile(filepath=self._file_path, relative_remap=True) self.report({"INFO"}, f"Created Shot {self.shot_id}") return {'FINISHED'} @@ -177,37 +182,44 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): wm = context.window_manager self._timer = wm.event_timer_add(0.1, window=context.window) wm.modal_handler_add(self) - if not self._built_shot: - 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'} - addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences - 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() - #Load EDIT - bpy.ops.kitsu.con_detect_context() #TODO CONFIRM AND CHECK IF OVERRIDE IS NEEDED - editorial_export_get_latest(context) - # Load Anim Workspace - animation_workspace_delete_others() - shot = cache.shot_active_get() - - # 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}.{shot.name}.v001") - new_action.use_fake_user = True - obj.animation_data.action = new_action - - # Set Shot Frame Range - context.scene.frame_start = addon_prefs.shot_builder_frame_offset - context.scene.frame_end = shot.nb_frames + addon_prefs.shot_builder_frame_offset - self._built_shot = True + 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'} + addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences + 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() + + # Build Kitsu Context + sequence = gazu.shot.get_sequence_by_name(production.config['KITSU_PROJECT_ID'], self.seq_id) + shot = gazu.shot.get_shot_by_name(sequence, self.shot_id) + + #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 + frame_length = shot.get('nb_frames') + context.scene.frame_start = addon_prefs.shot_builder_frame_offset + context.scene.frame_end = frame_length + addon_prefs.shot_builder_frame_offset + + 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: -- 2.30.2 From 6fe881e3a6a3d7d2e4bc440ddae4b881e3e7fcef Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 15:33:07 -0400 Subject: [PATCH 19/25] [Blender_Kitsu] Improve file_Save feedback for shot_builder - Add option to skip save - Add feedback if file already exists error - Move logic back to save_fily.py and make it callable from other functions --- .../shot_builder/builder/save_file.py | 10 ++++++--- blender_kitsu/shot_builder/editorial/ops.py | 2 +- blender_kitsu/shot_builder/operators.py | 21 +++++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/blender_kitsu/shot_builder/builder/save_file.py b/blender_kitsu/shot_builder/builder/save_file.py index 0122c915..c9881b8b 100644 --- a/blender_kitsu/shot_builder/builder/save_file.py +++ b/blender_kitsu/shot_builder/builder/save_file.py @@ -11,6 +11,12 @@ import logging logger = logging.getLogger(__name__) + +def save_shot_builder_file(file_path): + file_path = pathlib.Path(file_path) + file_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" @@ -18,7 +24,5 @@ class SaveFileStep(BuildStep): def execute(self, build_context: BuildContext) -> None: shot = build_context.shot file_path = pathlib.Path(shot.file_path) - file_path.mkdir(parents=True, exist_ok=True) - + save_shot_builder_file(file_path) logger.info(f"save file {shot.file_path}") - bpy.ops.wm.save_mainfile(filepath=shot.file_path, relative_remap=True) diff --git a/blender_kitsu/shot_builder/editorial/ops.py b/blender_kitsu/shot_builder/editorial/ops.py index 30bd4530..d25f18f0 100644 --- a/blender_kitsu/shot_builder/editorial/ops.py +++ b/blender_kitsu/shot_builder/editorial/ops.py @@ -13,7 +13,7 @@ class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator): def execute(self, context: bpy.types.Context) -> Set[str]: cache_shot = cache.shot_active_get() - shot = gazu.shot.get_shot(cache_shot.id) + 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( diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 0e1d9d10..7de2ab6e 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -27,6 +27,7 @@ from blender_kitsu.shot_builder.task_type import TaskType from blender_kitsu import prefs, cache, gazu from blender_kitsu.shot_builder.anim_setup.core import animation_workspace_delete_others, animation_workspace_vse_area_add from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest +from blender_kitsu.shot_builder.builder.save_file import save_shot_builder_file _production_task_type_items: List[Tuple[str, str, str]] = [] @@ -111,6 +112,11 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): 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): @@ -123,10 +129,16 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): self._add_vse_area = True if self._built_shot and self._add_vse_area: - file_path = pathlib.Path(self._file_path) - file_path.mkdir(parents=True, exist_ok=True) - bpy.ops.wm.save_mainfile(filepath=self._file_path, relative_remap=True) - self.report({"INFO"}, f"Created Shot {self.shot_id}") + if self.auto_save: + file_path = pathlib.Path(self._file_path) + try: + save_shot_builder_file(file_path) + self.report({"INFO"}, f"Saved Shot{self.shot_id} at {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'} @@ -230,3 +242,4 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): layout.prop(self, "seq_id") layout.prop(self, "shot_id") layout.prop(self, "task_type") + layout.prop(self, "auto_save") -- 2.30.2 From b23172c9c23b5dbb6d1f33e792c5f8c86b88fb4b Mon Sep 17 00:00:00 2001 From: TinyNick Date: Mon, 3 Apr 2023 16:39:07 -0400 Subject: [PATCH 20/25] [Blender_Kistsu] shot_builder allow user to supply commands to execute - create text box that user can supply commands into incase there is customization for the production to be done --- blender_kitsu/prefs.py | 7 +++++++ blender_kitsu/shot_builder/operators.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index ea86e567..be6b08d7 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -334,6 +334,12 @@ 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) @@ -425,6 +431,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") # Misc settings. box = layout.box() diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index 7de2ab6e..abc12a9a 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -229,6 +229,9 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): context.scene.frame_start = addon_prefs.shot_builder_frame_offset context.scene.frame_end = frame_length + addon_prefs.shot_builder_frame_offset + # 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'} -- 2.30.2 From 6e32f66e8bacaa3544bbb0ee7c64b60e4d0b4e30 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Tue, 4 Apr 2023 13:13:19 -0400 Subject: [PATCH 21/25] [Blender Kitsu] Fix File Save durign Shot_build --- blender_kitsu/shot_builder/builder/save_file.py | 8 +++++--- blender_kitsu/shot_builder/operators.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/blender_kitsu/shot_builder/builder/save_file.py b/blender_kitsu/shot_builder/builder/save_file.py index c9881b8b..abedb044 100644 --- a/blender_kitsu/shot_builder/builder/save_file.py +++ b/blender_kitsu/shot_builder/builder/save_file.py @@ -12,9 +12,11 @@ logger = logging.getLogger(__name__) -def save_shot_builder_file(file_path): - file_path = pathlib.Path(file_path) - file_path.mkdir(parents=True, exist_ok=True) +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): diff --git a/blender_kitsu/shot_builder/operators.py b/blender_kitsu/shot_builder/operators.py index abc12a9a..03e2eb70 100644 --- a/blender_kitsu/shot_builder/operators.py +++ b/blender_kitsu/shot_builder/operators.py @@ -130,10 +130,10 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator): if self._built_shot and self._add_vse_area: if self.auto_save: - file_path = pathlib.Path(self._file_path) + file_path = pathlib.Path() try: - save_shot_builder_file(file_path) - self.report({"INFO"}, f"Saved Shot{self.shot_id} at {file_path}") + 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}") -- 2.30.2 From 26c89e3ef8aea039d15684fb2cf6775b436e9794 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Tue, 4 Apr 2023 13:15:53 -0400 Subject: [PATCH 22/25] [Blender_Kitsu] Clean Whitespace in anim_setup module --- blender_kitsu/shot_builder/anim_setup/ops.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/blender_kitsu/shot_builder/anim_setup/ops.py b/blender_kitsu/shot_builder/anim_setup/ops.py index 75b4dd31..a1438ce5 100644 --- a/blender_kitsu/shot_builder/anim_setup/ops.py +++ b/blender_kitsu/shot_builder/anim_setup/ops.py @@ -20,8 +20,6 @@ class ANIM_SETUP_OT_animation_workspace_vse_area_add(bpy.types.Operator): animation_workspace_vse_area_add(self, context) return {"FINISHED"} -# - classes = [ ANIM_SETUP_OT_setup_workspaces, ANIM_SETUP_OT_animation_workspace_vse_area_add -- 2.30.2 From 3249cc86d12668682255fca961828a462d632754 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Tue, 4 Apr 2023 13:16:10 -0400 Subject: [PATCH 23/25] [Blender_Kitsu] Editorial, remove no-op code --- blender_kitsu/shot_builder/editorial/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/blender_kitsu/shot_builder/editorial/core.py b/blender_kitsu/shot_builder/editorial/core.py index 757adbb0..526fc57d 100644 --- a/blender_kitsu/shot_builder/editorial/core.py +++ b/blender_kitsu/shot_builder/editorial/core.py @@ -8,7 +8,6 @@ from blender_kitsu import cache def editorial_export_get_latest(context:bpy.types.Context, shot) -> list[bpy.types.Sequence]: #TODO add info to shot """Loads latest export from editorial department""" addon_prefs = prefs.addon_prefs_get(context) - edit_export_path = Path(addon_prefs.edit_export_dir) strip_channel = 1 latest_file = editorial_export_check_latest(context) if not latest_file: -- 2.30.2 From f17de25a564b71ca012d88e2a38d5c3162969139 Mon Sep 17 00:00:00 2001 From: TinyNick Date: Tue, 4 Apr 2023 13:46:00 -0400 Subject: [PATCH 24/25] [Blender_Kitsu] Allow user to offset editorial reference - Create Editorial Export Offset property - Use property when placing VSE reference - Expose Property in UI --- blender_kitsu/prefs.py | 7 +++++++ blender_kitsu/shot_builder/editorial/core.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/blender_kitsu/prefs.py b/blender_kitsu/prefs.py index be6b08d7..af504f39 100644 --- a/blender_kitsu/prefs.py +++ b/blender_kitsu/prefs.py @@ -316,6 +316,12 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): ) + edit_export_frame_offset: bpy.props.IntProperty( # type: ignore + name="Editorial Export Offset", + description="Shift Editorial Export by this frame-range after set-up.", + default=-102, #HARD CODED FOR PET PROJECTS BLENDER FILM + ) + shot_builder_frame_offset: bpy.props.IntProperty( # type: ignore name="Start Frame Offset", description="All Shots built by 'Shot_builder' should begin at this frame", @@ -424,6 +430,7 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences): box.label(text="Shot Builder", icon="MOD_BUILD") box.row().prop(self, "edit_export_dir") box.row().prop(self, "edit_export_file_pattern") + box.row().prop(self, "edit_export_frame_offset") box.row().prop(self, "shot_builder_show_advanced") if self.shot_builder_show_advanced: start_frame_row = box.row() diff --git a/blender_kitsu/shot_builder/editorial/core.py b/blender_kitsu/shot_builder/editorial/core.py index 526fc57d..6d6fd7fd 100644 --- a/blender_kitsu/shot_builder/editorial/core.py +++ b/blender_kitsu/shot_builder/editorial/core.py @@ -41,10 +41,11 @@ def editorial_export_get_latest(context:bpy.types.Context, shot) -> list[bpy.typ frame_in = shot["data"].get("frame_in") frame_3d_in = shot["data"].get("3d_in") frame_3d_offset = frame_3d_in - addon_prefs.shot_builder_frame_offset + edit_export_offset = addon_prefs.edit_export_frame_offset # Set sequence strip start kitsu data. for strip in new_strips: - strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset + strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset + edit_export_offset return new_strips -- 2.30.2 From 6a26d54e8437d08df3a493bda566993ef36ff6dd Mon Sep 17 00:00:00 2001 From: TinyNick Date: Tue, 4 Apr 2023 14:24:22 -0400 Subject: [PATCH 25/25] [Blender_Kitsu] Remove old `Anim_Setup` Module --- anim_setup/.gitignore | 112 ------ anim_setup/README.md | 24 -- anim_setup/__init__.py | 57 --- anim_setup/asglobals.py | 17 - anim_setup/kitsu.py | 315 --------------- anim_setup/log.py | 16 - anim_setup/ops.py | 835 ---------------------------------------- anim_setup/opsdata.py | 344 ----------------- anim_setup/prefs.py | 176 --------- anim_setup/props.py | 34 -- anim_setup/ui.py | 134 ------- 11 files changed, 2064 deletions(-) delete mode 100644 anim_setup/.gitignore delete mode 100644 anim_setup/README.md delete mode 100644 anim_setup/__init__.py delete mode 100644 anim_setup/asglobals.py delete mode 100644 anim_setup/kitsu.py delete mode 100644 anim_setup/log.py delete mode 100644 anim_setup/ops.py delete mode 100644 anim_setup/opsdata.py delete mode 100644 anim_setup/prefs.py delete mode 100644 anim_setup/props.py delete mode 100644 anim_setup/ui.py diff --git a/anim_setup/.gitignore b/anim_setup/.gitignore deleted file mode 100644 index d197a693..00000000 --- a/anim_setup/.gitignore +++ /dev/null @@ -1,112 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -.venv* -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -# IDE settings -.vscode/ - -# utility bat files: -*jump_in_venv.bat - -#local tests -tests/local* \ No newline at end of file diff --git a/anim_setup/README.md b/anim_setup/README.md deleted file mode 100644 index dd849ec9..00000000 --- a/anim_setup/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# anim-setup -anim-setup is a Blender Add-on that automates the setup of animation scenes for the Sprite-Fright project. -## Installation -Download or clone this repository. -In the root project folder you will find the 'anim_setup' folder. Place this folder in your Blender addons directory or create a sym link to it. - -After install you need to configure the addon in the addon preferences. - -## Features -The addon relies on the correct naming of asset and camera actions in the corresponding previs file of the shot. -Check the Animation Setup Checklist. - -Operators of the addon: -- Setup Workspace for animation -- Load latest edit from edit export directory -- Import camera action from the previs file -- Import actions for found assets from previs file -- Shift animation of camera and asset actions to start at layout cut in -- Create missing actions for found assets in scene - -## Development -In the project root you will find a `pyproject.toml` and `peotry.lock` file. -With `poetry` you can easily generate a virtual env for the project which should get you setup quickly. -Basic Usage: https://python-poetry.org/docs/basic-usage/ diff --git a/anim_setup/__init__.py b/anim_setup/__init__.py deleted file mode 100644 index 6ee4240b..00000000 --- a/anim_setup/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -import bpy - -from . import asglobals -from . import prefs -from . import kitsu -from . import props -from . import opsdata -from . import ops -from . import ui -from .log import LoggerFactory - -logger = LoggerFactory.getLogger(__name__) - -bl_info = { - "name": "Anim Setup", - "author": "Paul Golter", - "description": "Blender addon to setup animation scenes for the spritefright project", - "blender": (3, 0, 0), - "version": (0, 1, 0), - "location": "View3D", - "warning": "", - "doc_url": "", - "tracker_url": "", - "category": "Generic", -} - -_need_reload = "ops" in locals() - -if _need_reload: - import importlib - - asglobals = importlib.reload(asglobals) - prefs = importlib.reload(prefs) - kitsu = importlib.reload(kitsu) - props = importlib.reload(props) - opsdata = importlib.reload(opsdata) - ops = importlib.reload(ops) - ui = importlib.reload(ui) - - -def register(): - prefs.register() - props.register() - ops.register() - ui.register() - logger.info("Registered anim-setup") - - -def unregister(): - ui.unregister() - ops.unregister() - props.unregister() - prefs.unregister() - - -if __name__ == "__main__": - register() diff --git a/anim_setup/asglobals.py b/anim_setup/asglobals.py deleted file mode 100644 index f69ee76b..00000000 --- a/anim_setup/asglobals.py +++ /dev/null @@ -1,17 +0,0 @@ -PROJECT_NAME = "SpriteFright" -PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec" -ACTION_ASSETS = [ - "CH-ellie", - "CH-jay", - "CH-phil", - "CH-rex", - "CH-elder_sprite", - "CH-victoria", - "CH-bird", - "PR-bbq_grill", - "PR-boombox", - "PR-tree_chasm", - "PR-log_bridge_trunk" -] -MULTI_ASSETS = ["CH-sprite"] -HIDE_COLLS = ["mushrooms_center", "treetop_leaves"] diff --git a/anim_setup/kitsu.py b/anim_setup/kitsu.py deleted file mode 100644 index 2463df8d..00000000 --- a/anim_setup/kitsu.py +++ /dev/null @@ -1,315 +0,0 @@ -from __future__ import annotations -import requests - -from dataclasses import asdict, dataclass, field -from typing import Any, Dict, List, Optional, Union - -from .log import LoggerFactory - -logger = LoggerFactory.getLogger() - - -class KitsuException(Exception): - pass - - -class KitsuConnector: - def __init__(self, preferences: "AS_AddonPreferences"): - self._preferences = preferences - self.__access_token = "" - self.__validate() - self.__authorize() - - def __validate(self) -> None: - self._preferences.kitsu._validate() - - def __authorize(self) -> None: - kitsu_pref = self._preferences.kitsu - backend = kitsu_pref.backend - email = kitsu_pref.email - password = kitsu_pref.password - - logger.info(f"authorize {email} against {backend}") - response = requests.post( - url=f"{backend}/auth/login", data={"email": email, "password": password} - ) - if response.status_code != 200: - self.__access_token = "" - raise KitsuException( - f"unable to authorize (status code={response.status_code})" - ) - json_response = response.json() - self.__access_token = json_response["access_token"] - - def api_get(self, api: str) -> Any: - kitsu_pref = self._preferences.kitsu - backend = kitsu_pref.backend - - response = requests.get( - url=f"{backend}{api}", - headers={"Authorization": f"Bearer {self.__access_token}"}, - ) - if response.status_code != 200: - raise KitsuException( - f"unable to call kitsu (api={api}, status code={response.status_code})" - ) - return response.json() - - @classmethod - def fetch_first( - cls, json_response: Dict[str, Any], filter: Dict[str, Any] - ) -> Optional[Dict[str, Any]]: - - if not isinstance(json_response, list): - raise ValueError( - f"Failed to fetch one, excpected list object: {json_response}" - ) - - for item in json_response: - matches = 0 - for f in filter: - if f in item and item[f] == filter[f]: - matches += 1 - - if matches == len(filter): - return item - - logger.error("Filter had no match %s on json response.", str(filter)) - return None - - @classmethod - def fetch_all( - cls, json_response: Dict[str, Any], filter: Dict[str, Any] - ) -> List[Dict[str, Any]]: - - if not isinstance(json_response, list): - raise ValueError( - f"Failed to fetch all, excpected list object: {json_response}" - ) - - valid_items: List[Dict[str, Any]] = [] - - for item in json_response: - matches = 0 - for f in filter: - if f in item and item[f] == filter[f]: - matches += 1 - - if matches == len(filter): - valid_items.append(item) - - return valid_items - - -class ProjectList(KitsuConnector): - """ - Class to get object oriented representation of backend productions data structure. - """ - - def __init__(self): - self._projects: List[Project] = [] - self._init_projects() - - @property - def names(self) -> List[str]: - return [p.name for p in self._projects] - - @property - def projects(self) -> List[Project]: - return self._projects - - def _init_projects(self) -> None: - api_url = "data/projects" - - for project in self.api_get(api_url): - self._projects.append(Project(**project)) - - -@dataclass -class Project(KitsuConnector): - """ - Class to get object oriented representation of backend project data structure. - Can shortcut some functions from gazu api because active project is given through class instance. - Has multiple constructor functions (by_name, by_id, init>by_dict) - """ - - id: str = "" - created_at: str = "" - updated_at: str = "" - name: str = "" - code: Optional[str] = None - description: Optional[str] = None - shotgun_id: Optional[str] = None - data: None = None - has_avatar: bool = False - fps: Optional[str] = None - ratio: Optional[str] = None - resolution: Optional[str] = None - production_type: str = "" - start_date: Optional[str] = None - end_date: Optional[str] = None - man_days: Optional[str] = None - nb_episodes: int = 0 - episode_span: int = 0 - project_status_id: str = "" - type: str = "" - project_status_name: str = "" - file_tree: Dict[str, Any] = field(default_factory=dict) - team: List[Any] = field(default_factory=list) - asset_types: List[Any] = field(default_factory=list) - task_types: List[Any] = field(default_factory=list) - task_statuses: List[Any] = field(default_factory=list) - - @classmethod - def by_id(cls, connector: KitsuConnector, project_id: str) -> Project: - api_url = f"data/projects/{project_id}" - project_dict = connector.api_get(api_url) - return cls(**project_dict) - - # SEQUENCES - # --------------- - - def get_sequence(self, connector: KitsuConnector, seq_id: str) -> Sequence: - return Sequence.by_id(connector, seq_id) - - def get_sequence_by_name( - self, connector: KitsuConnector, seq_name: str - ) -> Optional[Sequence]: - return Sequence.by_name(connector, self, seq_name) - - def get_sequences_all(self, connector: KitsuConnector) -> List[Sequence]: - api_url = f"data/projects/{self.id}/sequences" - seq_dicts = connector.api_get(api_url) - - sequences = [Sequence(**s) for s in seq_dicts] - return sorted(sequences, key=lambda x: x.name) - - # SHOT - # --------------- - - def get_shot(self, connector: KitsuConnector, shot_id: str) -> Shot: - return Shot.by_id(connector, shot_id) - - def get_shots_all(self, connector: KitsuConnector) -> List[Shot]: - api_url = f"data/projects/{self.id}/shots" - shot_dicts = connector.api_get(api_url) - - shots = [Shot(**s) for s in shot_dicts] - return sorted(shots, key=lambda x: x.name) - - def get_shot_by_name( - self, connector: KitsuConnector, sequence: Sequence, name: str - ) -> Optional[Shot]: - all_shots = self.get_shots_all(connector) - return Shot.by_name(connector, sequence, name) - - def __bool__(self): - return bool(self.id) - - -@dataclass -class Sequence(KitsuConnector): - """ - Class to get object oriented representation of backend sequence data structure. - Has multiple constructor functions (by_name, by_id, init>by_dict) - """ - - id: str = "" - created_at: str = "" - updated_at: str = "" - name: str = "" - code: Optional[str] = None - description: Optional[str] = None - shotgun_id: Optional[str] = None - canceled: bool = False - nb_frames: Optional[int] = None - project_id: str = "" - entity_type_id: str = "" - parent_id: str = "" - source_id: Optional[str] = None - preview_file_id: Optional[str] = None - data: Optional[Dict[str, Any]] = None - type: str = "" - project_name: str = "" - - @classmethod - def by_id(cls, connector: KitsuConnector, seq_id: str) -> Sequence: - api_url = f"data/sequences/{seq_id}" - seq_dict = connector.api_get(seq_id) - return cls(**seq_dict) - - @classmethod - def by_name( - cls, connector: KitsuConnector, project: Project, seq_name: str - ) -> Optional[Sequence]: - api_url = f"data/projects/{project.id}/sequences" - seq_dicts = connector.api_get(api_url) - seq_dict = connector.fetch_first(seq_dicts, {"name": seq_name}) - - # Can be None if name not found. - if not seq_dict: - return None - - return cls(**seq_dict) - - def __bool__(self): - return bool(self.id) - - -@dataclass -class Shot(KitsuConnector): - """ - Class to get object oriented representation of backend shot data structure. - Has multiple constructor functions (by_name, by_id, init>by_dict - """ - - id: str = "" - created_at: str = "" - updated_at: str = "" - name: str = "" - canceled: bool = False - code: Optional[str] = None - description: Optional[str] = None - entity_type_id: str = "" - episode_id: Optional[str] = None - episode_name: str = "" - fps: str = "" - frame_in: str = "" - frame_out: str = "" - nb_frames: int = 0 - parent_id: str = "" - preview_file_id: Optional[str] = None - project_id: str = "" - project_name: str = "" - sequence_id: str = "" - sequence_name: str = "" - source_id: Optional[str] = None - shotgun_id: Optional[str] = None - type: str = "" - data: Dict[str, Any] = field(default_factory=dict) - tasks: List[Dict[str, Any]] = field(default_factory=list) - - @classmethod - def by_id(cls, connector: KitsuConnector, shot_id: str) -> Shot: - api_url = f"data/shots/{shot_id}" - shot_dict = connector.api_get(shot_id) - return cls(**shot_dict) - - @classmethod - def by_name( - cls, connector: KitsuConnector, sequence: Sequence, shot_name: str - ) -> Optional[Shot]: - api_url = f"data/projects/{sequence.project_id}/shots" - shot_dicts = connector.api_get(api_url) - shot_dict = connector.fetch_first( - shot_dicts, {"parent_id": sequence.id, "name": shot_name} - ) - - # Can be None if name not found. - if not shot_dict: - return None - - return cls(**shot_dict) - - def __bool__(self): - return bool(self.id) diff --git a/anim_setup/log.py b/anim_setup/log.py deleted file mode 100644 index de426b41..00000000 --- a/anim_setup/log.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -import sys -from typing import List, Tuple - - -class LoggerFactory: - - """ - Utility class to streamline logger creation - """ - - @staticmethod - def getLogger(name=__name__): - name = name - logger = logging.getLogger(name) - return logger diff --git a/anim_setup/ops.py b/anim_setup/ops.py deleted file mode 100644 index 53143969..00000000 --- a/anim_setup/ops.py +++ /dev/null @@ -1,835 +0,0 @@ -import re -from pathlib import Path -import types -from typing import Container, Dict, List, Set, Optional - -import bpy - -from .log import LoggerFactory -from .kitsu import KitsuConnector, Shot, Project, Sequence -from . import opsdata, prefs, asglobals - -logger = LoggerFactory.getLogger() - - -def ui_redraw() -> None: - """Forces blender to redraw the UI.""" - for screen in bpy.data.screens: - for area in screen.areas: - area.tag_redraw() - - -class AS_OT_create_actions(bpy.types.Operator): - bl_idname = "as.create_action" - bl_label = "Create action" - bl_description = ( - "Creates action for all found assets that have no assigned yet. " - "Names them following the blender-studio convention" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - act_coll = context.view_layer.active_layer_collection.collection - return bool(bpy.data.filepath and act_coll) - - def execute(self, context: bpy.types.Context) -> Set[str]: - assigned: List[bpy.types.Action] = [] - created: List[bpy.types.Action] = [] - failed: List[bpy.types.Collection] = [] - collections = opsdata.get_valid_collections(context) - exists: List[bpy.types.Collection] = [] - - if not collections: - self.report({"WARNING"}, "No valid collections available") - return {"CANCELLED"} - - for coll in collections: - print("\n") - rig = opsdata.find_rig(coll) - - if not rig: - logger.warning(f"{coll.name} contains no rig.") - failed.append(coll) - continue - - # Create animation data if not existent. - if not rig.animation_data: - rig.animation_data_create() - logger.info("%s created animation data", rig.name) - - # If action already exists check for fake user and then continue. - if rig.animation_data.action: - logger.info("%s already has an action assigned", rig.name) - - if not rig.animation_data.action.use_fake_user: - rig.animation_data.action.use_fake_user = True - logger.info("%s assigned existing action fake user", rig.name) - exists.append(coll) - continue - - # Create new action. - action_name_new = opsdata.gen_action_name(coll) - try: - action = bpy.data.actions[action_name_new] - except KeyError: - action = bpy.data.actions.new(action_name_new) - logger.info("Created action: %s", action.name) - created.append(action) - else: - logger.info("Action %s already exists. Will take that.", action.name) - - # Assign action. - rig.animation_data.action = action - logger.info("%s assigned action %s", rig.name, action.name) - - # Add fake user. - action.use_fake_user = True - assigned.append(action) - - self.report( - {"INFO"}, - "Actions: Created %s | Assigned %s | Exists %s | Failed %s" - % (len(created), len(assigned), len(exists), len(failed)), - ) - return {"FINISHED"} - - -class AS_OT_setup_workspaces(bpy.types.Operator): - bl_idname = "as.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]: - - # Remove non anim workspaces. - for ws in bpy.data.workspaces: - if ws.name != "Animation": - bpy.ops.workspace.delete({"workspace": ws}) - - self.report({"INFO"}, "Deleted non Animation workspaces") - - return {"FINISHED"} - - -class AS_OT_load_latest_edit(bpy.types.Operator): - bl_idname = "as.load_latest_edit" - bl_label = "Load edit" - bl_description = ( - "Loads latest edit from shot_preview_folder " - "Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu" - ) - - @classmethod - def can_load_edit(cls, context: bpy.types.Context) -> bool: - """Check if shared dir and VSE area are available""" - addon_prefs = prefs.addon_prefs_get(context) - edit_export_path = Path(addon_prefs.edit_export_path) - - # Needs to be run in sequence editor area - # TODO: temporarily create a VSE area if not available. - area_override = None - for area in bpy.context.screen.areas: - if area.type == "SEQUENCE_EDITOR": - area_override = area - - return bool(area_override and edit_export_path) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - return cls.can_load_edit(context) - - @classmethod - def description(cls, context, properties): - if cls.can_load_edit(context): - return "Load latest edit from shared folder" - else: - return "Shared folder not set, or VSE area not available in this workspace" - - def execute(self, context: bpy.types.Context) -> Set[str]: - - addon_prefs = prefs.addon_prefs_get(context) - edit_export_path = Path(addon_prefs.edit_export_path) - strip_channel = 1 - latest_file = self._get_latest_edit(context) - if not latest_file: - self.report( - {"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}" - ) - strip_filepath = latest_file.as_posix() - strip_frame_start = 101 - - # Needs to be run in sequence editor area. - area_override = None - for area in bpy.context.screen.areas: - if area.type == "SEQUENCE_EDITOR": - area_override = area - - if not area_override: - self.report({"ERROR"}, "No sequence editor are found") - return {"CANCELLED"} - - override = bpy.context.copy() - override["area"] = area_override - - bpy.ops.sequencer.movie_strip_add( - override, - filepath=strip_filepath, - relative_path=False, - frame_start=strip_frame_start, - channel=strip_channel, - fit_method="FIT", - ) - - # Get sequence name. - seqname = opsdata.get_sequence_from_file() - if not seqname: - self.report({"ERROR"}, "Failed to retrieve seqname from current file.") - return {"CANCELLED"} - - # Get shotname. - shotname = opsdata.get_shot_name_from_file() - if not shotname: - self.report({"ERROR"}, "Failed to retrieve shotname from current file.") - return {"CANCELLED"} - - # Setup connector and get data from kitsu. - connector = KitsuConnector(addon_prefs) - project = Project.by_id(connector, addon_prefs.kitsu.project_id) - sequence = project.get_sequence_by_name(connector, seqname) - - if not sequence: - self.report({"ERROR"}, f"Failed to find {seqname} on kitsu.") - return {"CANCELLED"} - - shot = project.get_shot_by_name(connector, sequence, shotname) - - if not shot: - self.report({"ERROR"}, f"Failed to find shot {shotname} on kitsu.") - return {"CANCELLED"} - - # Update shift frame range prop. - frame_in = shot.data["frame_in"] - frame_out = shot.data["frame_out"] - frame_3d_in = shot.data["3d_in"] - frame_3d_offset = frame_3d_in - 101 - - if not frame_in: - self.report( - {"ERROR"}, f"On kitsu 'frame_in' is not defined for shot {shotname}." - ) - return {"CANCELLED"} - - # Set sequence strip start kitsu data. - for strip in context.scene.sequence_editor.sequences_all: - strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset - - self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}") - - return {"FINISHED"} - - def _get_latest_edit(self, context: bpy.types.Context): - addon_prefs = prefs.addon_prefs_get(context) - - edit_export_path = Path(addon_prefs.edit_export_path) - - files_list = [ - f - for f in edit_export_path.iterdir() - if f.is_file() and self._is_valid_edit_name(f.name) - ] - files_list = sorted(files_list, reverse=True) - - return files_list[0] - - def _is_valid_edit_name(self, filename: str) -> bool: - pattern = r"sf-edit-v\d\d\d.mp4" - - match = re.search(pattern, filename) - if match: - return True - return False - - -class AS_OT_import_camera(bpy.types.Operator): - bl_idname = "as.import_camera" - bl_label = "Import Camera" - bl_description = "Imports camera rig and makes library override" - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - addon_prefs = prefs.addon_prefs_get(context) - return bool(addon_prefs.is_project_root_valid and bpy.data.filepath) - - def execute(self, context: bpy.types.Context) -> Set[str]: - - addon_prefs = prefs.addon_prefs_get(context) - - # Import camera rig and make override. - camera_rig_path = addon_prefs.camera_rig_path - if not camera_rig_path: - self.report({"ERROR"}, "Failed to import camera rig") - return {"CANCELLED"} - - cam_lib_coll = opsdata.import_data_from_lib( - "collections", - "CA-camera_rig", - camera_rig_path, - ) - opsdata.instance_coll_to_scene_and_override(context, cam_lib_coll) - cam_coll = bpy.data.collections[cam_lib_coll.name, None] - - self.report({"INFO"}, f"Imported camera: {cam_coll.name}") - return {"FINISHED"} - - -class AS_OT_import_camera_action(bpy.types.Operator): - bl_idname = "as.import_camera_action" - bl_label = "Import Camera Action" - bl_description = ( - "Imports camera action of previs file that matches current shot and assigns it" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - addon_prefs = prefs.addon_prefs_get(context) - return bool(addon_prefs.is_project_root_valid and bpy.data.filepath) - - def execute(self, context: bpy.types.Context) -> Set[str]: - - try: - cam_coll = bpy.data.collections["CA-camera_rig", None] - except KeyError: - self.report({"ERROR"}, f"Camera collection CA-camera_rig is not imported") - return {"CANCELELD"} - - # Import camera action from previz file. - - # Get shotname and previs filepath. - shotname = opsdata.get_shot_name_from_file() - if not shotname: - self.report({"ERROR"}, "Failed to retrieve shotname from current file.") - return {"CANCELLED"} - - previs_path = opsdata.get_previs_file(context) - if not previs_path: - self.report({"ERROR"}, "Failed to find previz file") - return {"CANCELLED"} - - # Check if cam action name exists in previs library. - cam_action_name_new = opsdata.get_cam_action_name_from_lib( - shotname, previs_path - ) - if not cam_action_name_new: - self.report( - {"ERROR"}, - f"Camera action: {cam_action_name_new} not found in lib: {previs_path.name}", - ) - return {"CANCELLED"} - - # Import cam action data block. - cam_action = opsdata.import_data_from_lib( - "actions", cam_action_name_new, previs_path, link=False - ) - - # Find rig to assing action to. - rig = opsdata.find_rig(cam_coll) - if not rig: - self.report({"WARNING"}, f"{cam_coll.name} contains no rig.") - return {"CANCELLED"} - - # Assign action. - rig.animation_data.action = cam_action - logger.info("%s assigned action %s", rig.name, cam_action.name) - - # Add fake user. - cam_action.use_fake_user = True - - # Ensure version suffix to action data bloc. - opsdata.ensure_name_version_suffix(cam_action) - - self.report({"INFO"}, f"{rig.name} imported camera action: {cam_action.name}") - return {"FINISHED"} - - -class AS_OT_import_asset_actions(bpy.types.Operator): - """Imports asset action of previs file that matches current shot and assigns it""" - - bl_idname = "as.import_asset_actions" - bl_label = "Import Asset Actions" - bl_description = ( - "For each found asset tries to find action in previs file. " - "Imports it to current file, renames it, adds fake user and assigns it" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - addon_prefs = prefs.addon_prefs_get(context) - return bool(addon_prefs.is_project_root_valid and bpy.data.filepath) - - def execute(self, context: bpy.types.Context) -> Set[str]: - - succeeded = [] - failed = [] - actions_imported = [] - renamed_actions = [] - - # Get shotname and previs filepath. - shotname = opsdata.get_shot_name_from_file() - if not shotname: - self.report({"ERROR"}, "Failed to retrieve shotname from current file.") - return {"CANCELLED"} - - previs_path = opsdata.get_previs_file(context) - if not previs_path: - self.report({"ERROR"}, "Failed to find previz file") - return {"CANCELLED"} - - # Check if cam action name exists in previs library. - action_candidates: Dict[str, List[str]] = {} - asset_colls = [] - - with bpy.data.libraries.load( - previs_path.as_posix(), relative=True, link=False - ) as ( - data_from, - data_to, - ): - - for asset in asglobals.ACTION_ASSETS: - - # Check if asset is in current scene. - try: - coll = bpy.data.collections[asset] - except KeyError: - # can continue here if not in scene we - # cant load action anyway - continue - else: - logger.info("Found asset in scene: %s", coll.name) - asset_colls.append(coll) - - # Find if actions exists for that asset in previs file. - asset_name = opsdata.find_asset_name(asset) - for action in data_from.actions: - if action.startswith(f"ANI-{asset_name}."): - - # Create key if not existent yet. - if asset not in action_candidates: - action_candidates[asset] = [] - - # Append action to that asset. - action_candidates[asset].append(action) - - # Load and assign actions for asset colls. - for coll in asset_colls: - - # Find rig. - rig = opsdata.find_rig(coll) - if not rig: - logger.warning("%s contains no rig.", coll.name) - continue - - # Check if action was found in previs file for that asset. - if not coll.name in action_candidates: - logger.warning("%s no action found in previs file", coll.name) - continue - else: - logger.info( - "%s found actions in previs file: %s", - asset, - str(action_candidates[coll.name]), - ) - - # Check if multiple actions are in the prvis file for that asset. - if len(action_candidates[coll.name]) > 1: - logger.warning( - "%s Multiple actions found in previs file: %s", - coll.name, - str(action_candidates[coll.name]), - ) - continue - - # Import action from previs file. - actions = action_candidates[coll.name] - action = opsdata.import_data_from_lib( - "actions", actions[0], previs_path, link=False - ) - if not action: - continue - - actions_imported.append(action) - - # Create animation data if not existent. - if not rig.animation_data: - rig.animation_data_create() - logger.info("%s created animation data", rig.name) - - # Assign action. - rig.animation_data.action = action - logger.info("%s assigned action %s", rig.name, action.name) - - # Add fake user. - action.use_fake_user = True - - # Rename actions. - action_name_new = opsdata.gen_action_name(coll) - try: - action_existing = bpy.data.actions[action_name_new] - except KeyError: - # Action does not exists can rename. - old_name = action.name - action.name = action_name_new - logger.info("Renamed action: %s to %s", old_name, action.name) - renamed_actions.append(action) - else: - # Action name already exists in this scene. - logger.info( - "Failed to rename action action %s to %s. Already exists", - action.name, - action_name_new, - ) - continue - - self.report( - {"INFO"}, - f"Found Assets: {len(asset_colls)} | Imported Actions: {len(actions_imported)} | Renamed Actions: {len(renamed_actions)}", - ) - return {"FINISHED"} - - -class AS_OT_import_multi_assets(bpy.types.Operator): - bl_idname = "as.import_multi_assets" - bl_label = "Import Multi Assets" - bl_description = ( - "For each found multi asset tries to find action in previs file. " - "Imports it to current file, renames it, adds fake user and assigns it" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - addon_prefs = prefs.addon_prefs_get(context) - return bool(addon_prefs.is_project_root_valid and bpy.data.filepath) - - def execute(self, context: bpy.types.Context) -> Set[str]: - actions_imported = [] - new_colls = [] - - # Get shotname and previs filepath. - shotname = opsdata.get_shot_name_from_file() - if not shotname: - self.report({"ERROR"}, "Failed to retrieve shotname from current file.") - return {"CANCELLED"} - - previs_path = opsdata.get_previs_file(context) - if not previs_path: - self.report({"ERROR"}, "Failed to find previz file") - return {"CANCELLED"} - - # Check if cam action name exists in previs library. - action_candidates: Dict[str, List[str]] = {} - asset_colls: List[bpy.types.Collection] = [] - - with bpy.data.libraries.load( - previs_path.as_posix(), relative=True, link=False - ) as ( - data_from, - data_to, - ): - data_from_actions: List[str] = data_from.actions - data_from_actions.sort() - - # Find all sprites actions. - for asset in asglobals.MULTI_ASSETS: - # Check if asset is in current scene. - try: - coll = bpy.data.collections[asset] - except KeyError: - # Can continue here if not in scene we - # cant load action anyway. - continue - else: - logger.info("Found asset in scene: %s", coll.name) - asset_colls.append(coll) - - # Find if actions exists for that asset in previs file. - asset_name = opsdata.find_asset_name(asset) - for action in data_from_actions: - if action.startswith(f"ANI-{asset_name}"): - - # Create key if not existent yet. - if asset not in action_candidates: - action_candidates[asset] = [] - - # Append action to that asset. - action_candidates[asset].append(action) - - # Load and assign actions for asset colls. - color_tag: str = "" - for coll in asset_colls: - - # Check if action was found in previs file for that asset. - if not coll.name in action_candidates: - logger.warning("%s no action found in previs file", coll.name) - continue - else: - logger.info( - "%s found actions in previs file: %s", - asset, - str(action_candidates[coll.name]), - ) - - # Create duplicate for each action. - for idx, action_candidate in enumerate(action_candidates[coll.name]): - - # First index use existing collection that was already created by shot builder. - if idx == 0: - new_coll = bpy.data.collections[asset, None] - logger.info("First index will use existing coll: %s", new_coll.name) - color_tag = new_coll.color_tag # Take color from first collection. - else: - ref_coll = opsdata.get_ref_coll(coll) - new_coll = ref_coll.override_hierarchy_create( - context.scene, context.view_layer, reference=coll - ) - new_coll.color_tag = color_tag - logger.info("Created new override collection: %s", new_coll.name) - new_colls.append(new_coll) - - # Find rig of new coll. - rig = opsdata.find_rig(new_coll) - if not rig: - logger.warning("%s contains no rig.", coll.name) - continue - - # Import action. - action = opsdata.import_data_from_lib( - "actions", action_candidate, previs_path, link=False - ) - if not action: - continue - - actions_imported.append(action) - - # Create animation data if not existent. - if not rig.animation_data: - rig.animation_data_create() - logger.info("%s created animation data", rig.name) - - # Assign action. - rig.animation_data.action = action - logger.info("%s assigned action %s", rig.name, action.name) - - self.report( - {"INFO"}, - f"Found Assets: {len(asset_colls)} | Imported Actions: {len(actions_imported)} | New collections: {len(new_colls)}", - ) - return {"FINISHED"} - - -class AS_OT_shift_anim(bpy.types.Operator): - bl_idname = "as.shift_anim" - bl_label = "Shift Anim" - bl_description = ( - "Shifts the animation of found assets by number of frames. " - "It also shifts the camera animation as well as its modifier values" - ) - - multi_assets: bpy.props.BoolProperty(name="Do Multi Assets") - - def execute(self, context: bpy.types.Context) -> Set[str]: - # Define the frame offset by: - # Subtracting the layout cut in frame (to set the 0) - # Adding 101 (the animation start for a shot) - # For example, layout frame 520 becomes frames_offset -520 + 101 = -419. - - frames_offset = -context.scene.anim_setup.layout_cut_in + 101 - rigs: List[bpy.types.Armature] = [] - - if not self.multi_assets: - # Get cam coll. - try: - rig = bpy.data.objects["RIG-camera", None] - except KeyError: - logger.warning("Failed to find camera object 'RIG-camera'") - else: - rigs.append(rig) - - # Find assets. - for asset in asglobals.ACTION_ASSETS: - - # Check if asset is in current scene. - try: - coll = bpy.data.collections[asset] - except KeyError: - # Can continue here if not in scene we - # cant load action anyway. - continue - else: - logger.info("Found asset in scene: %s", coll.name) - # Find rig. - rig = opsdata.find_rig(coll) - if not rig: - logger.warning("%s contains no rig.", coll.name) - continue - rigs.append(rig) - else: - for asset in asglobals.MULTI_ASSETS: - for coll in bpy.data.collections: - - if not opsdata.is_item_lib_override(coll): - continue - - if not coll.name.startswith(asset): - continue - - logger.info("Found asset in scene: %s", coll.name) - # Find rig. - rig = opsdata.find_rig(coll) - if not rig: - logger.warning("%s contains no rig.", coll.name) - continue - rigs.append(rig) - - if not rigs: - self.report( - {"ERROR"}, "Failed to find any assets or cameras to shift animation." - ) - return {"CANCELLED"} - - for rig in rigs: - for fcurve in rig.animation_data.action.fcurves: - - # Shift all keyframes. - for point in fcurve.keyframe_points: - # Print(f"{fcurve.data_path}|{fcurve.array_index}: {point.co.x}|{point.co.y}"). - point.co.x += frames_offset - # Don't forget the keyframe's handles:. - point.handle_left.x += frames_offset - point.handle_right.x += frames_offset - - # Shift all noise modififers values. - for m in fcurve.modifiers: - if not m.type == "NOISE": - continue - - m.offset += frames_offset - - if m.use_restricted_range: - frame_start = m.frame_start - frame_end = m.frame_end - m.frame_start = frame_start + (frames_offset) - m.frame_end = frame_end + (frames_offset) - - logger.info( - "%s shifted %s modifier values by %i frames", - m.id_data.name, - m.type.lower(), - frames_offset, - ) - logger.info( - "%s: %s shifted all keyframes by %i frames", - rig.name, - rig.animation_data.action.name, - frames_offset, - ) - - self.report( - {"INFO"}, f"Shifted animation of {len(rigs)} actions by {frames_offset}" - ) - return {"FINISHED"} - - -class AS_OT_apply_additional_settings(bpy.types.Operator): - - bl_idname = "as.apply_additional_settings" - bl_label = "Apply Additional Settings" - bl_description = ( - "Apply some additional settings that are important " "for animation scenes" - ) - - @classmethod - def poll(cls, context: bpy.types.Context) -> bool: - sqe_area = cls._get_sqe_area(context) - return bool(sqe_area) - - def execute(self, context: bpy.types.Context) -> Set[str]: - - sqe_area = self._get_sqe_area(context) - - sqe_area.spaces.active.use_proxies = False - sqe_area.spaces.active.proxy_render_size = "PROXY_100" - - self.report({"INFO"}, "Set: use_proxies | proxy_render_size") - return {"FINISHED"} - - @classmethod - def _get_sqe_area(cls, context: bpy.types.Context): - for window in context.window_manager.windows: - screen = window.screen - - for area in screen.areas: - if area.type == "SEQUENCE_EDITOR": - return area - - return None - - -class AS_OT_exclude_colls(bpy.types.Operator): - """Excludes Collections that are not needed for animation""" - - bl_idname = "as.exclude_colls" - bl_label = "Exclude Collections" - bl_description = ( - "Exclude some collections by name that are not needed in animation scenes" - ) - - def execute(self, context: bpy.types.Context) -> Set[str]: - view_layer_colls = opsdata.get_all_view_layer_colls(context) - - excluded = [] - for coll_name in asglobals.HIDE_COLLS: - # Find view layer collection, if same collection is linked in in 2 different colls in same scene, these - # are 2 different view layer colls, we need to grab all. - valid_view_layer_colls = [ - vc for vc in view_layer_colls if vc.name == coll_name - ] - - if not valid_view_layer_colls: - logger.info("No view layer collections named: %s", coll_name) - continue - - for view_layer_coll in valid_view_layer_colls: - view_layer_coll.exclude = True - logger.info("Excluded view layer collection: %s", view_layer_coll.name) - excluded.append(view_layer_coll) - - self.report( - {"INFO"}, f"Excluded Collections: {list([v.name for v in excluded])}" - ) - return {"FINISHED"} - - -# ---------REGISTER ----------. - -classes = [ - AS_OT_create_actions, - AS_OT_setup_workspaces, - AS_OT_load_latest_edit, - AS_OT_import_camera, - AS_OT_import_camera_action, - AS_OT_shift_anim, - AS_OT_apply_additional_settings, - AS_OT_import_asset_actions, - AS_OT_exclude_colls, - AS_OT_import_multi_assets, -] - - -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/anim_setup/opsdata.py b/anim_setup/opsdata.py deleted file mode 100644 index 86062e10..00000000 --- a/anim_setup/opsdata.py +++ /dev/null @@ -1,344 +0,0 @@ -import re -from pathlib import Path -from typing import Optional, Dict, Union, Any, List, Generator -import bpy -from bpy.types import Key - -from . import prefs - - -from .log import LoggerFactory - -logger = LoggerFactory.getLogger() - - -def get_shot_name_from_file() -> Optional[str]: - if not bpy.data.filepath: - return None - - # Default 110_0030_A.anim.blend. - return Path(bpy.data.filepath).name.split(".")[0] - - -def get_sequence_from_file() -> Optional[str]: - if not bpy.data.filepath: - return None - - # ./spritefright/pro/shots/110_rextoria/110_0010_A/110_0010_A.anim.blend. - return Path(bpy.data.filepath).parents[1].name - - -def get_seqeunce_short_from_shot_name(shotname: str) -> str: - return shotname.split("_")[0] - - -def get_cam_action_name_from_shot(shotname: str) -> str: - # ANI-camera.070_0010_A. - return f"ANI-camera.{shotname}" - - -def get_cam_action_name_from_lib(shotname: str, libpath: Path) -> Optional[str]: - - valid_actions = [] - - with bpy.data.libraries.load(libpath.as_posix(), relative=True) as ( - data_from, - data_to, - ): - - for action in data_from.actions: - if action.startswith(get_cam_action_name_from_shot(shotname)): - valid_actions.append(action) - - if not valid_actions: - return None - - return sorted(valid_actions, reverse=True)[0] - - -def get_previs_file(context: bpy.types.Context) -> Optional[Path]: - - addon_prefs = prefs.addon_prefs_get(context) - - shotname = get_shot_name_from_file() - if not shotname: - return None - - seqname = get_seqeunce_short_from_shot_name(shotname) - previs_path = Path(addon_prefs.previs_root_path) - - # Catch custom cases when previs files are split up for specific shots. - if shotname == "020_0010_A": - return previs_path / "020_grove.020_0010_A.blend" - - elif shotname == "020_0020_A" or shotname == "020_0050_A": - return previs_path / "020_grove.shove.blend" - - elif shotname in ["020_0060_A", "020_0070_A"]: - return previs_path / "020_grove.crowdcamping_alt.blend" - - elif shotname in ["020_0160_A", "020_0170_A", "020_0173_A", "020_0176_A"]: - return previs_path / "020_grove.weenie_alt.blend" - - else: - for f in previs_path.iterdir(): - if f.is_file() and f.suffix == ".blend" and f.name.startswith(seqname): - if len(f.name.split(".")) > 2: - continue - return f - return None - - -def traverse_collection_tree( - collection: bpy.types.Collection, -) -> Generator[bpy.types.Collection, None, None]: - yield collection - for child in collection.children: - yield from traverse_collection_tree(child) - - -def import_data_from_lib( - data_category: str, - data_name: str, - libpath: Path, - link: bool = True, -): - - noun = "Appended" - if link: - noun = "Linked" - - with bpy.data.libraries.load(libpath.as_posix(), relative=True, link=link) as ( - data_from, - data_to, - ): - - if data_name not in eval(f"data_from.{data_category}"): - logger.error( - "Failed to import %s %s from %s. Doesn't exist in file.", - data_category, - data_name, - libpath.as_posix(), - ) - return None - - # Check if datablock with same name already exists in blend file. - try: - eval(f"bpy.data.{data_category}['{data_name}']") - except KeyError: - pass - else: - logger.info( - "%s already in bpy.data.%s of this blendfile.", data_name, data_category - ) - return None - - # Append data block. - eval(f"data_to.{data_category}.append('{data_name}')") - logger.info( - "%s: %s from library: %s", - noun, - data_name, - libpath.as_posix(), - ) - - if link: - return eval( - f"bpy.data.{data_category}['{data_name}', '{bpy.path.relpath(libpath.as_posix())}']" - ) - - return eval(f"bpy.data.{data_category}['{data_name}']") - - -def instance_coll_to_scene_and_override( - context: bpy.types.Context, source_collection: bpy.types.Collection -) -> bpy.types.Collection: - instance_obj = _create_collection_instance(source_collection) - _make_library_override(context, instance_obj) - return bpy.data.collections[source_collection.name, None] - - -def _create_collection_instance( - source_collection: bpy.types.Collection, -) -> bpy.types.Object: - - # Name has no effect how the overwritten library collection in the end - # use empty to instance source collection. - instance_obj = bpy.data.objects.new(name="", object_data=None) - instance_obj.instance_collection = source_collection - instance_obj.instance_type = "COLLECTION" - - parent_collection = bpy.context.view_layer.active_layer_collection - parent_collection.collection.objects.link(instance_obj) - - logger.info( - "Instanced collection: %s as: %s", - source_collection.name, - instance_obj.name, - ) - - return instance_obj - - -def _make_library_override( - context: bpy.types.Context, - instance_obj: bpy.types.Object, -) -> None: - log_name = instance_obj.name - # Deselect all. - bpy.ops.object.select_all(action="DESELECT") - - # Needs active object (coll instance). - context.view_layer.objects.active = instance_obj - instance_obj.select_set(True) - - # Add library override. - bpy.ops.object.make_override_library() - - logger.info( - "%s make library override.", - log_name, - ) - - -def find_asset_name(name: str) -> str: - - if name.endswith("_rig"): - name = name[:-4] - return name.split("-")[-1] # CH-rex -> 'rex' - - -def find_rig(coll: bpy.types.Collection) -> Optional[bpy.types.Armature]: - - coll_suffix = find_asset_name(coll.name) - - valid_rigs = [] - - for obj in coll.all_objects: - # Default rig name: 'RIG-rex' / 'RIG-Rex'. - if obj.type != "ARMATURE": - continue - - if not obj.name.startswith("RIG"): - continue - - valid_rigs.append(obj) - - if not valid_rigs: - return None - - elif len(valid_rigs) == 1: - logger.info("Found rig: %s", valid_rigs[0].name) - return valid_rigs[0] - else: - logger.error("%s found multiple rigs %s", coll.name, str(valid_rigs)) - return None - - -def ensure_name_version_suffix(datablock: Any) -> Any: - version_pattern = r"v\d\d\d" - match = re.search(version_pattern, datablock.name) - - if not match: - datablock.name = datablock.name + ".v001" - - return datablock - - -def get_valid_collections(context: bpy.types.Context) -> List[bpy.types.Collection]: - valid_prefixes = ["CH-", "PR-"] - valid_colls: List[bpy.types.Collection] = [] - - for coll in context.scene.collection.children: - if coll.name[:3] not in valid_prefixes: - continue - valid_colls.append(coll) - - return valid_colls - - -def is_multi_asset(asset_name: str) -> bool: - if asset_name.startswith("thorn"): - return True - multi_assets = ["sprite", "snail", "spider"] - if asset_name.lower() in multi_assets: - return True - return False - - -def gen_action_name(coll: bpy.types.Collection): - action_prefix = "ANI" - asset_name = find_asset_name(coll.name).lower() - asset_name = asset_name.replace(".", "_") - version = "v001" - shot_name = get_shot_name_from_file() - - action_name_new = f"{action_prefix}-{asset_name}.{shot_name}.{version}" - - if is_multi_asset(asset_name): - action_name_new = f"{action_prefix}-{asset_name}_A.{shot_name}.{version}" - - return action_name_new - - -def set_layer_coll_exlcude( - layer_collections: List[bpy.types.LayerCollection], exclude: bool -) -> None: - - noun = "Excluded" if exclude else "Included" - - for lcoll in layer_collections: - - if exclude: - if lcoll.exclude: - continue - - lcoll.exclude = True - - else: - if not lcoll.exclude: - continue - - lcoll.exclude = False - - logger.info("%s %s", noun, lcoll.name) - - -def get_all_view_layer_colls( - context: bpy.types.Context, -) -> List[bpy.types.LayerCollection]: - return list(traverse_collection_tree(context.view_layer.layer_collection)) - - -def get_ref_coll(coll: bpy.types.Collection) -> bpy.types.Collection: - if not coll.override_library: - return coll - - return coll.override_library.reference - - -def is_item_local( - item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera] -) -> bool: - # Local collection of blend file. - if not item.override_library and not item.library: - return True - return False - - -def is_item_lib_override( - item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera] -) -> bool: - # Collection from libfile and overwritten. - if item.override_library and not item.library: - return True - return False - - -def is_item_lib_source( - item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera] -) -> bool: - # Source collection from libfile not overwritten. - if not item.override_library and item.library: - return True - return False diff --git a/anim_setup/prefs.py b/anim_setup/prefs.py deleted file mode 100644 index bf1f4654..00000000 --- a/anim_setup/prefs.py +++ /dev/null @@ -1,176 +0,0 @@ -import os -from pathlib import Path -from typing import Union, Optional, Any, Dict, Set - -import bpy - -from .kitsu import KitsuException -from . import asglobals - - -class KitsuPreferences(bpy.types.PropertyGroup): - backend: bpy.props.StringProperty( # type: ignore - name="Server URL", - description="Kitsu server address", - default="https://kitsu.blender.cloud/api", - ) - - email: bpy.props.StringProperty( # type: ignore - name="Email", - description="Email to connect to Kitsu", - ) - - password: bpy.props.StringProperty( # type: ignore - name="Password", - description="Password to connect to Kitsu", - subtype="PASSWORD", - ) - - project_id: bpy.props.StringProperty( # type: ignore - name="Project ID", - description="Server Id that refers to the last active project", - default=asglobals.PROJECT_ID, - options={"HIDDEN", "SKIP_SAVE"}, - ) - - def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - box = layout.box() - box.label(text="Kitsu") - box.prop(self, "backend") - box.prop(self, "email") - box.prop(self, "password") - box.prop(self, "project_id") - - def _validate(self): - if not (self.backend and self.email and self.password and self.project_id): - raise KitsuException( - "Kitsu connector has not been configured in the add-on preferences" - ) - - -class AS_AddonPreferences(bpy.types.AddonPreferences): - bl_idname = __package__ - - project_root: bpy.props.StringProperty( # type: ignore - name="Project Root", - default="", - options={"HIDDEN", "SKIP_SAVE"}, - subtype="DIR_PATH", - ) - edit_export_dir: bpy.props.StringProperty( # type: ignore - name="Edit Export Directory", - default="", - options={"HIDDEN", "SKIP_SAVE"}, - subtype="DIR_PATH", - ) - - kitsu: bpy.props.PointerProperty( # type: ignore - name="Kitsu Preferences", type=KitsuPreferences - ) - - def draw(self, context: bpy.types.Context) -> None: - layout = self.layout - box = layout.box() - box.row().prop(self, "project_root") - - if not self.project_root: - row = box.row() - row.label(text="Please specify the project root directory.", icon="ERROR") - - if not bpy.data.filepath and self.project_root.startswith("//"): - row = box.row() - row.label( - text="In order to use a relative path as root cache directory the current file needs to be saved.", - icon="ERROR", - ) - - box.row().prop(self, "edit_export_dir") - - if not self.edit_export_dir: - row = box.row() - row.label(text="Please specify the edit edxport directory.", icon="ERROR") - - if not bpy.data.filepath and self.edit_export_dir.startswith("//"): - row = box.row() - row.label( - text="In order to use a relative path as edit export directory the current file needs to be saved.", - icon="ERROR", - ) - - self.kitsu.draw(layout, context) - - @property - def project_root_path(self) -> Optional[Path]: - if not self.is_project_root_valid: - return None - return Path(os.path.abspath(bpy.path.abspath(self.project_root))) - - @property - def is_project_root_valid(self) -> bool: - - # Check if file is saved. - if not self.project_root: - return False - - if not bpy.data.filepath and self.project_root.startswith("//"): - return False - - return True - - @property - def is_editorial_valid(self) -> bool: - if not self.edit_export_dir: - return False - - return Path(self.edit_export_dir).exists() - - @property - def edit_export_path(self) -> Optional[Path]: - if not self.is_editorial_valid: - return None - - return Path(self.edit_export_dir) - - @property - def previs_root_path(self) -> Optional[Path]: - if not self.is_project_root_valid: - return None - - previs_path = self.project_root_path / "previz" - - if not previs_path.exists(): - return None - - return previs_path - - @property - def camera_rig_path(self) -> Optional[Path]: - if not self.is_project_root_valid: - return None - - camera_rig_path = self.project_root_path / "pro/lib/cam/camera_rig.blend" - - if not camera_rig_path.exists(): - return None - - return camera_rig_path - - -def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences: - """Shortcut to get cache_manager addon preferences""" - return context.preferences.addons["anim_setup"].preferences - - -# ---------REGISTER ----------. - -classes = [KitsuPreferences, AS_AddonPreferences] - - -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/anim_setup/props.py b/anim_setup/props.py deleted file mode 100644 index 93ac9e0d..00000000 --- a/anim_setup/props.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List, Any, Generator, Optional - -import bpy - - -class CM_property_group_scene(bpy.types.PropertyGroup): - - layout_cut_in: bpy.props.IntProperty( - name="Layout Cut In", - description="Frame where the camera marker is set for the shot, in the layout file", - default=0, - step=1, - ) - - -# ---------REGISTER ----------. - -classes: List[Any] = [ - CM_property_group_scene, -] - - -def register(): - - for cls in classes: - bpy.utils.register_class(cls) - - # Scene Properties. - bpy.types.Scene.anim_setup = bpy.props.PointerProperty(type=CM_property_group_scene) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/anim_setup/ui.py b/anim_setup/ui.py deleted file mode 100644 index 7764c29c..00000000 --- a/anim_setup/ui.py +++ /dev/null @@ -1,134 +0,0 @@ -import bpy - -from . import opsdata - -from .ops import ( - AS_OT_create_actions, - AS_OT_setup_workspaces, - AS_OT_load_latest_edit, - AS_OT_import_camera, - AS_OT_import_camera_action, - AS_OT_shift_anim, - AS_OT_apply_additional_settings, - AS_OT_import_asset_actions, - AS_OT_exclude_colls, - AS_OT_import_multi_assets -) - - -class AS_PT_view3d_general(bpy.types.Panel): - """ - Animation Setup general operators. - """ - - bl_category = "Anim Setup" - bl_label = "General" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_order = 10 - - def draw(self, context: bpy.types.Context) -> None: - valid_colls = opsdata.get_valid_collections(context) - layout = self.layout - col = layout.column(align=True) - - # Workspace. - col.operator(AS_OT_setup_workspaces.bl_idname) - - # Load edit. - col.operator(AS_OT_load_latest_edit.bl_idname) - - - -class AS_PT_view3d_animation_and_actions(bpy.types.Panel): - """ - Animation Setup main operators and properties. - """ - - bl_category = "Anim Setup" - bl_label = "Animation and Actions" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_order = 12 - - def draw(self, context: bpy.types.Context) -> None: - - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - layout.label(text=f"Previs file: {opsdata.get_previs_file(context)}") - - col = layout.column(align=True) - - # Import camera action. - col.operator(AS_OT_import_camera_action.bl_idname) - - # Import action. - col.operator( - AS_OT_import_asset_actions.bl_idname, text=f"Import Char Actions" - ) - - col.operator( - AS_OT_import_multi_assets.bl_idname, text=f"Import Multi Asset Actions" - ) - - col.separator() - col = layout.column() - - # Shift animation. - col.prop(context.scene.anim_setup, "layout_cut_in") - col.separator() - split = col.split(factor=0.5, align=True) - split.operator(AS_OT_shift_anim.bl_idname, text="Shift Char/Cam") - split.operator(AS_OT_shift_anim.bl_idname, text="Shift Multi").multi_assets = True - - col.separator() - - # Create actions. - valid_collections_count = len(opsdata.get_valid_collections(context)) - row = col.row(align=True) - row.operator( - AS_OT_create_actions.bl_idname, text=f"Create {valid_collections_count} actions" - ) - - -class AS_PT_view3d_scene(bpy.types.Panel): - """ - Animation Setup scene operators. - """ - - bl_category = "Anim Setup" - bl_label = "Scene" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_order = 13 - - def draw(self, context: bpy.types.Context) -> None: - - layout = self.layout - - # Exclude collections. - row = layout.row(align=True) - row.operator( - AS_OT_exclude_colls.bl_idname, text="Exclude Collections" - ) - - -# ---------REGISTER ----------. - -classes = [ - AS_PT_view3d_general, - AS_PT_view3d_animation_and_actions, - AS_PT_view3d_scene, - ] - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) -- 2.30.2