Blender Kitsu: Refactor Shot Builder #183
@ -24,7 +24,6 @@ dependencies.preload_modules()
|
||||
|
||||
from . import (
|
||||
shot_builder,
|
||||
shot_builder_2,
|
||||
lookdev,
|
||||
bkglobals,
|
||||
types,
|
||||
@ -99,7 +98,6 @@ def register():
|
||||
playblast.register()
|
||||
anim.register()
|
||||
shot_builder.register()
|
||||
shot_builder_2.register()
|
||||
|
||||
LoggerLevelManager.configure_levels()
|
||||
logger.info("Registered blender-kitsu")
|
||||
@ -118,7 +116,6 @@ def unregister():
|
||||
lookdev.unregister()
|
||||
playblast.unregister()
|
||||
shot_builder.unregister()
|
||||
shot_builder_2.unregister()
|
||||
LoggerLevelManager.restore_levels()
|
||||
|
||||
|
||||
|
@ -40,7 +40,7 @@ from .auth.ops import (
|
||||
)
|
||||
from .context.ops import KITSU_OT_con_productions_load
|
||||
from .lookdev.prefs import LOOKDEV_preferences
|
||||
from .shot_builder.editorial.core import editorial_export_check_latest
|
||||
from .shot_builder.editorial import editorial_export_check_latest
|
||||
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
@ -339,12 +339,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
|
||||
default="ANI-",
|
||||
)
|
||||
|
||||
user_exec_code: bpy.props.StringProperty( # type: ignore
|
||||
name="Post Execution Command",
|
||||
description="Run this command after shot_builder is complete, but before the file is saved.",
|
||||
default="",
|
||||
)
|
||||
|
||||
session: Session = Session()
|
||||
|
||||
tasks: bpy.props.CollectionProperty(type=KITSU_task)
|
||||
@ -439,7 +433,7 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
|
||||
start_frame_row.prop(self, "shot_builder_frame_offset", text="")
|
||||
box.row().prop(self, "shot_builder_armature_prefix")
|
||||
box.row().prop(self, "shot_builder_action_prefix")
|
||||
box.row().prop(self, "user_exec_code")
|
||||
box.operator("kitsu.save_shot_builder_hooks", icon='FILE_SCRIPT')
|
||||
|
||||
# Misc settings.
|
||||
box = layout.box()
|
||||
|
@ -1,62 +1,15 @@
|
||||
# ##### 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 #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
from .ui import *
|
||||
from .connectors.kitsu import *
|
||||
from .operators import *
|
||||
import bpy
|
||||
from .anim_setup import ops as anim_setup_ops # TODO Fix Registraion
|
||||
from .editorial import ops as editorial_ops # TODO Fix Registraion
|
||||
|
||||
# import logging
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
# bl_info = {
|
||||
# 'name': 'Shot Builder',
|
||||
# "author": "Jeroen Bakker",
|
||||
# 'version': (0, 1),
|
||||
# 'blender': (2, 90, 0),
|
||||
# 'location': 'Addon Preferences panel and file new menu',
|
||||
# 'description': 'Shot builder production tool.',
|
||||
# 'category': 'Studio',
|
||||
# }
|
||||
|
||||
|
||||
classes = (
|
||||
KitsuPreferences,
|
||||
SHOTBUILDER_OT_NewShotFile,
|
||||
)
|
||||
from . import ops, ui
|
||||
from .ui import topbar_file_new_draw_handler
|
||||
|
||||
|
||||
def register():
|
||||
anim_setup_ops.register()
|
||||
editorial_ops.register()
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler)
|
||||
ops.register()
|
||||
ui.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()
|
||||
ui.unregister()
|
||||
|
@ -1,34 +0,0 @@
|
||||
import bpy
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
from ... import prefs
|
||||
from ... import cache
|
||||
|
||||
|
||||
def animation_workspace_vse_area_add(context: bpy.types.Context):
|
||||
"""Split smallest 3D View in current workspace"""
|
||||
for workspace in [
|
||||
workspace for workspace in bpy.data.workspaces if workspace.name == "Animation"
|
||||
]:
|
||||
context.window.workspace = workspace
|
||||
context.view_layer.update()
|
||||
areas = workspace.screens[0].areas
|
||||
view_3d_areas = sorted(
|
||||
[area for area in areas if area.ui_type == "VIEW_3D"],
|
||||
key=lambda x: x.width,
|
||||
reverse=False,
|
||||
)
|
||||
small_view_3d = view_3d_areas[0]
|
||||
with context.temp_override(window=context.window, area=small_view_3d):
|
||||
bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.5)
|
||||
small_view_3d.ui_type = "SEQUENCE_EDITOR"
|
||||
small_view_3d.spaces[0].view_type = "PREVIEW"
|
||||
|
||||
|
||||
def animation_workspace_delete_others():
|
||||
"""Delete any workspace that is not an animation workspace"""
|
||||
for ws in bpy.data.workspaces:
|
||||
if ws.name != "Animation":
|
||||
with bpy.context.temp_override(workspace=ws):
|
||||
bpy.ops.workspace.delete()
|
@ -1,35 +0,0 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from .core import animation_workspace_delete_others, animation_workspace_vse_area_add
|
||||
class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator):
|
||||
bl_idname = "anim_setup.setup_workspaces"
|
||||
bl_label = "Setup Workspace"
|
||||
bl_description = "Sets up the workspaces for the animation task"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
animation_workspace_delete_others(self, context)
|
||||
self.report({"INFO"}, "Deleted non Animation workspaces")
|
||||
return {"FINISHED"}
|
||||
|
||||
class ANIM_SETUP_OT_animation_workspace_vse_area_add(bpy.types.Operator):
|
||||
bl_idname = "anim_setup.animation_workspace_vse_area_add"
|
||||
bl_label = "Split Viewport"
|
||||
bl_description = "Split smallest 3D View in current workspace"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
animation_workspace_vse_area_add(self, context)
|
||||
return {"FINISHED"}
|
||||
|
||||
classes = [
|
||||
ANIM_SETUP_OT_setup_workspaces,
|
||||
ANIM_SETUP_OT_animation_workspace_vse_area_add
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,50 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
|
||||
class Asset:
|
||||
"""
|
||||
Container to hold data where the asset can be located in the production repository.
|
||||
|
||||
path: absolute path to the blend file containing this asset.
|
||||
|
||||
"""
|
||||
|
||||
asset_type = ""
|
||||
code = ""
|
||||
name = ""
|
||||
path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend"
|
||||
collection = "{asset.code}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class AssetRef:
|
||||
"""
|
||||
Reference to an asset from an external system.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "", code: str = ""):
|
||||
self.name = name
|
||||
self.code = code
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
@ -1,100 +0,0 @@
|
||||
from ..project import Production
|
||||
from ..task_type import TaskType
|
||||
from ..asset import Asset, AssetRef
|
||||
from .build_step import BuildStep, BuildContext
|
||||
from .init_asset import InitAssetStep
|
||||
from .init_shot import InitShotStep
|
||||
from .set_render_settings import SetRenderSettingsStep
|
||||
from .new_scene import NewSceneStep
|
||||
from .invoke_hook import InvokeHookStep
|
||||
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShotBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
production: Production,
|
||||
task_type: TaskType,
|
||||
shot_name: str,
|
||||
):
|
||||
self._steps: typing.List[BuildStep] = []
|
||||
|
||||
shot = production.get_shot(context, shot_name)
|
||||
assert shot
|
||||
render_settings = production.get_render_settings(context, shot)
|
||||
self.build_context = BuildContext(
|
||||
context=context,
|
||||
production=production,
|
||||
shot=shot,
|
||||
render_settings=render_settings,
|
||||
task_type=task_type,
|
||||
)
|
||||
|
||||
def __find_asset(self, asset_ref: AssetRef) -> typing.Optional[Asset]:
|
||||
for asset_class in self.build_context.production.assets:
|
||||
asset = typing.cast(Asset, asset_class())
|
||||
logger.debug(f"{asset_ref.name}, {asset.name}")
|
||||
if asset_ref.name == asset.name:
|
||||
return asset
|
||||
return None
|
||||
|
||||
def create_build_steps(self) -> None:
|
||||
self._steps.append(InitShotStep())
|
||||
self._steps.append(NewSceneStep())
|
||||
self._steps.append(SetRenderSettingsStep())
|
||||
|
||||
production = self.build_context.production
|
||||
task_type = self.build_context.task_type
|
||||
|
||||
# Add global hooks.
|
||||
for hook in production.hooks.filter():
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
# Add task specific hooks.
|
||||
for hook in production.hooks.filter(match_task_type=task_type.name):
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
context = self.build_context.context
|
||||
shot = self.build_context.shot
|
||||
|
||||
# Collect assets that should be loaded.
|
||||
asset_refs = production.get_assets_for_shot(context, shot)
|
||||
assets = []
|
||||
for asset_ref in asset_refs:
|
||||
asset = self.__find_asset(asset_ref)
|
||||
if asset is None:
|
||||
logger.warning(f"cannot determine repository data for {asset_ref}")
|
||||
continue
|
||||
assets.append(asset)
|
||||
|
||||
# Sort the assets on asset_type and asset.code).
|
||||
assets.sort(key=lambda asset: (asset.asset_type, asset.code))
|
||||
|
||||
# Build asset specific build steps.
|
||||
for asset in assets:
|
||||
self._steps.append(InitAssetStep(asset))
|
||||
# Add asset specific hooks.
|
||||
for hook in production.hooks.filter(
|
||||
match_task_type=task_type.name, match_asset_type=asset.asset_type
|
||||
):
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
def build(self) -> None:
|
||||
num_steps = len(self._steps)
|
||||
step_number = 1
|
||||
build_context = self.build_context
|
||||
window_manager = build_context.context.window_manager
|
||||
window_manager.progress_begin(min=0, max=num_steps)
|
||||
for step in self._steps:
|
||||
logger.info(f"Building step [{step_number}/{num_steps}]: {step} ")
|
||||
step.execute(build_context=build_context)
|
||||
window_manager.progress_update(value=step_number)
|
||||
step_number += 1
|
||||
window_manager.progress_end()
|
@ -1,38 +0,0 @@
|
||||
import bpy
|
||||
import typing
|
||||
|
||||
from ..project import Production
|
||||
from ..shot import Shot
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..asset import Asset
|
||||
|
||||
|
||||
class BuildContext:
|
||||
def __init__(self, context: bpy.types.Context, production: Production, shot: Shot, render_settings: RenderSettings, task_type: TaskType):
|
||||
self.context = context
|
||||
self.production = production
|
||||
self.shot = shot
|
||||
self.task_type = task_type
|
||||
self.render_settings = render_settings
|
||||
self.asset: typing.Optional[Asset] = None
|
||||
self.scene: typing.Optional[bpy.types.Scene] = None
|
||||
|
||||
def as_dict(self) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'context': self.context,
|
||||
'scene': self.scene,
|
||||
'production': self.production,
|
||||
'shot': self.shot,
|
||||
'task_type': self.task_type,
|
||||
'render_settings': self.render_settings,
|
||||
'asset': self.asset,
|
||||
}
|
||||
|
||||
|
||||
class BuildStep:
|
||||
def __str__(self) -> str:
|
||||
return "unnamed build step"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
raise NotImplementedError()
|
@ -1,25 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitAssetStep(BuildStep):
|
||||
def __init__(self, asset: Asset):
|
||||
self.__asset = asset
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"init asset \"{self.__asset.name}\""
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
build_context.asset = self.__asset
|
||||
self.__asset.path = self.__asset.path.format_map(build_context.as_dict())
|
||||
self.__asset.collection = self.__asset.collection.format_map(
|
||||
build_context.as_dict()
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitShotStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return "init shot"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
shot = build_context.shot
|
||||
shot.file_path = shot.file_path_format.format_map(build_context.as_dict())
|
@ -1,21 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..hooks import HookFunction
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import types
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InvokeHookStep(BuildStep):
|
||||
def __init__(self, hook: HookFunction):
|
||||
self._hook = hook
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"invoke hook [{self._hook.__name__}]"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
params = build_context.as_dict()
|
||||
self._hook(**params) # type: ignore
|
@ -1,30 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..render_settings import RenderSettings
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewSceneStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return f"new scene"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
production = build_context.production
|
||||
scene_name = production.scene_name_format.format_map(
|
||||
build_context.as_dict())
|
||||
logger.debug(f"create scene with name {scene_name}")
|
||||
scene = bpy.data.scenes.new(name=scene_name)
|
||||
|
||||
bpy.context.window.scene = scene
|
||||
build_context.scene = scene
|
||||
|
||||
self.__remove_other_scenes(build_context)
|
||||
|
||||
def __remove_other_scenes(self, build_context: BuildContext) -> None:
|
||||
for scene in bpy.data.scenes:
|
||||
if scene != build_context.scene:
|
||||
logger.debug(f"remove scene {scene.name}")
|
||||
bpy.data.scenes.remove(scene)
|
@ -1,30 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
import pathlib
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def save_shot_builder_file(file_path: str):
|
||||
"""Save Shot File within Folder of matching name.
|
||||
Set Shot File to relative Paths."""
|
||||
dir_path = pathlib.Path(file_path)
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_mainfile(filepath=file_path, relative_remap=True)
|
||||
|
||||
|
||||
class SaveFileStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return "save file"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
shot = build_context.shot
|
||||
file_path = pathlib.Path(shot.file_path)
|
||||
save_shot_builder_file(file_path)
|
||||
logger.info(f"save file {shot.file_path}")
|
@ -1,27 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetRenderSettingsStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return f"set render settings"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
scene = typing.cast(bpy.types.Scene, build_context.scene)
|
||||
render_settings = build_context.render_settings
|
||||
logger.debug(
|
||||
f"set render resolution to {render_settings.width}x{render_settings.height}")
|
||||
scene.render.resolution_x = render_settings.width
|
||||
scene.render.resolution_y = render_settings.height
|
||||
scene.render.resolution_percentage = 100
|
||||
|
||||
shot = build_context.shot
|
||||
scene.frame_start = shot.frame_start
|
||||
scene.frame_current = shot.frame_start
|
||||
scene.frame_end = shot.frame_start + shot.frames -1
|
||||
logger.debug(f"set frame range to ({scene.frame_start}-{scene.frame_end})")
|
@ -1,19 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
@ -1,108 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
"""
|
||||
This module contains the Connector class. It is an abstract base class for concrete connectors.
|
||||
"""
|
||||
|
||||
from ..shot import Shot, ShotRef
|
||||
from..asset import Asset, AssetRef
|
||||
from..task_type import TaskType
|
||||
from..render_settings import RenderSettings
|
||||
from typing import *
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from..project import Production
|
||||
from..properties import ShotBuilderPreferences
|
||||
|
||||
|
||||
class Connector:
|
||||
"""
|
||||
A Connector is used to retrieve data from a source. This source can be an external system.
|
||||
|
||||
Connectors can be configured for productions in its `shot-builder/config.py` file.
|
||||
|
||||
# Members
|
||||
|
||||
_production: reference to the production that we want to read data for.
|
||||
_preference: reference to the add-on preference to read settings for.
|
||||
Connectors can add settings to the add-on preferences.
|
||||
|
||||
# Class Members
|
||||
|
||||
PRODUCTION_KEYS: Connectors can register production configuration keys that will be loaded from the production config file.
|
||||
When keys are added the content will be read and stored in the production.
|
||||
|
||||
# Usage
|
||||
|
||||
Concrete connectors only overrides methods that they support. All non-overridden methods will raise an
|
||||
NotImplementerError.
|
||||
|
||||
|
||||
Example of using predefined connectors in a production config file:
|
||||
```shot-builder/config.py
|
||||
from ..connectors.default import DefaultConnector
|
||||
from ..connectors.kitsu import KitsuConnector
|
||||
|
||||
PRODUCTION_NAME = DefaultConnector
|
||||
TASK_TYPES = KitsuConnector
|
||||
KITSU_PROJECT_ID = "...."
|
||||
```
|
||||
"""
|
||||
PRODUCTION_KEYS: Set[str] = set()
|
||||
|
||||
def __init__(self, production: 'Production', preferences: 'ShotBuilderPreferences'):
|
||||
self._production = production
|
||||
self._preferences = preferences
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""
|
||||
Retrieve the production name using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of production name")
|
||||
|
||||
def get_task_types(self) -> List[TaskType]:
|
||||
"""
|
||||
Retrieve the task types using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of task types")
|
||||
|
||||
def get_shots(self) -> List[ShotRef]:
|
||||
"""
|
||||
Retrieve the shots using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of shots")
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]:
|
||||
"""
|
||||
Retrieve the sequences using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of assets for a shot")
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of render settings for a shot")
|
@ -1,49 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
from ..shot import Shot, ShotRef
|
||||
from ..asset import Asset, AssetRef
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..connectors.connector import Connector
|
||||
from typing import *
|
||||
|
||||
|
||||
class DefaultConnector(Connector):
|
||||
"""
|
||||
Default connector is a connector that returns the defaults for the shot builder add-on.
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "unnamed production"
|
||||
|
||||
def get_shots(self) -> List[ShotRef]:
|
||||
return []
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]:
|
||||
return []
|
||||
|
||||
def get_task_types(self) -> List[TaskType]:
|
||||
return [TaskType("anim"), TaskType("lighting"), TaskType("comp"), TaskType("fx")]
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
return RenderSettings(width=1920, height=1080, frames_per_second=24.0)
|
@ -1,248 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import bpy
|
||||
from .. import vars
|
||||
from ..shot import Shot, ShotRef
|
||||
from ..asset import Asset, AssetRef
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..connectors.connector import Connector
|
||||
import requests
|
||||
from ... import cache
|
||||
import gazu
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KitsuException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KitsuPreferences(bpy.types.PropertyGroup):
|
||||
backend: bpy.props.StringProperty( # type: ignore
|
||||
name="Server URL",
|
||||
description="Kitsu server address",
|
||||
default="https://kitsu.blender.cloud/api",
|
||||
)
|
||||
|
||||
username: bpy.props.StringProperty( # type: ignore
|
||||
name="Username",
|
||||
description="Username to connect to Kitsu",
|
||||
)
|
||||
|
||||
password: bpy.props.StringProperty( # type: ignore
|
||||
name="Password",
|
||||
description="Password to connect to Kitsu",
|
||||
subtype='PASSWORD',
|
||||
)
|
||||
|
||||
def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.label(text="Kitsu")
|
||||
layout.prop(self, "backend")
|
||||
layout.prop(self, "username")
|
||||
layout.prop(self, "password")
|
||||
|
||||
def _validate(self):
|
||||
if not (self.backend and self.username and self.password):
|
||||
raise KitsuException(
|
||||
"Kitsu connector has not been configured in the add-on preferences"
|
||||
)
|
||||
|
||||
|
||||
class KitsuDataContainer:
|
||||
def __init__(self, data: typing.Dict[str, typing.Optional[str]]):
|
||||
self._data = data
|
||||
|
||||
def get_parent_id(self) -> typing.Optional[str]:
|
||||
return self._data['parent_id']
|
||||
|
||||
def get_id(self) -> str:
|
||||
return str(self._data['id'])
|
||||
|
||||
def get_name(self) -> str:
|
||||
return str(self._data['name'])
|
||||
|
||||
def get_code(self) -> typing.Optional[str]:
|
||||
return self._data['code']
|
||||
|
||||
def get_description(self) -> str:
|
||||
result = self._data['description']
|
||||
if result is None:
|
||||
return ""
|
||||
return result
|
||||
|
||||
|
||||
class KitsuProject(KitsuDataContainer):
|
||||
def get_resolution(self) -> typing.Tuple[int, int]:
|
||||
"""
|
||||
Get the resolution and decode it to (width, height)
|
||||
"""
|
||||
res_str = str(self._data['resolution'])
|
||||
splitted = res_str.split("x")
|
||||
return (int(splitted[0]), int(splitted[1]))
|
||||
|
||||
|
||||
class KitsuSequenceRef(ShotRef):
|
||||
def __init__(self, kitsu_id: str, name: str, code: str):
|
||||
super().__init__(name=name, code=code)
|
||||
self.kitsu_id = kitsu_id
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
shot.sequence_code = self.name
|
||||
|
||||
|
||||
class KitsuShotRef(ShotRef):
|
||||
def __init__(
|
||||
self,
|
||||
kitsu_id: str,
|
||||
name: str,
|
||||
code: str,
|
||||
frame_start: int,
|
||||
frames: int,
|
||||
frame_end: int,
|
||||
frames_per_second: float,
|
||||
sequence: KitsuSequenceRef,
|
||||
):
|
||||
super().__init__(name=name, code=code)
|
||||
self.kitsu_id = kitsu_id
|
||||
self.frame_start = frame_start
|
||||
self.frames = frames
|
||||
self.frame_end = frame_end
|
||||
self.frames_per_second = frames_per_second
|
||||
self.sequence = sequence
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
shot.name = self.name
|
||||
shot.code = self.code
|
||||
shot.kitsu_id = self.kitsu_id
|
||||
shot.frame_start = self.frame_start
|
||||
shot.frames = self.frames
|
||||
shot.frame_end = self.frame_end
|
||||
shot.frames_per_second = self.frames_per_second
|
||||
self.sequence.sync_data(shot)
|
||||
|
||||
|
||||
class KitsuConnector(Connector):
|
||||
# PRODUCTION_KEYS = {'KITSU_PROJECT_ID'}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __get_production_data(self) -> KitsuProject:
|
||||
production = cache.project_active_get()
|
||||
project = KitsuProject(typing.cast(typing.Dict[str, typing.Any], production))
|
||||
return project
|
||||
|
||||
def get_name(self) -> str:
|
||||
production = self.__get_production_data()
|
||||
return production.get_name()
|
||||
|
||||
def get_task_types(self) -> typing.List[TaskType]:
|
||||
project = cache.project_active_get()
|
||||
task_types = project.task_types
|
||||
import pprint
|
||||
|
||||
pprint.pprint(task_types)
|
||||
return []
|
||||
|
||||
def get_shots(self) -> typing.List[ShotRef]:
|
||||
project = cache.project_active_get()
|
||||
kitsu_sequences = gazu.shot.all_sequences_for_project(project.id)
|
||||
|
||||
sequence_lookup = {
|
||||
sequence_data['id']: KitsuSequenceRef(
|
||||
kitsu_id=sequence_data['id'],
|
||||
name=sequence_data['name'],
|
||||
code=sequence_data['code'],
|
||||
)
|
||||
for sequence_data in kitsu_sequences
|
||||
}
|
||||
|
||||
kitsu_shots = gazu.shot.all_shots_for_project(project.id)
|
||||
shots: typing.List[ShotRef] = []
|
||||
|
||||
for shot_data in kitsu_shots:
|
||||
# Initialize default values
|
||||
frame_start = vars.DEFAULT_FRAME_START
|
||||
frame_end = 0
|
||||
|
||||
# shot_data['data'] can be None
|
||||
if shot_data['data']:
|
||||
# If 3d_start key not found use default start frame.
|
||||
frame_start = int(
|
||||
shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START)
|
||||
)
|
||||
frame_end = (
|
||||
int(shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START))
|
||||
+ shot_data['nb_frames']
|
||||
- 1
|
||||
)
|
||||
|
||||
# If 3d_start and 3d_out available use that to calculate frames.
|
||||
# If not try shot_data['nb_frames'] or 0 -> invalid.
|
||||
frames = int(
|
||||
(frame_end - frame_start + 1)
|
||||
if frame_end
|
||||
else shot_data['nb_frames'] or 0
|
||||
)
|
||||
if frames < 0:
|
||||
logger.error(
|
||||
"%s duration is negative: %i. Check frame range information on Kitsu",
|
||||
shot_data['name'],
|
||||
frames,
|
||||
)
|
||||
frames = 0
|
||||
|
||||
shots.append(
|
||||
KitsuShotRef(
|
||||
kitsu_id=shot_data['id'],
|
||||
name=shot_data['name'],
|
||||
code=shot_data['code'],
|
||||
frame_start=frame_start,
|
||||
frames=frames,
|
||||
frame_end=frame_end,
|
||||
frames_per_second=24.0,
|
||||
sequence=sequence_lookup[shot_data['parent_id']],
|
||||
)
|
||||
)
|
||||
|
||||
return shots
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]:
|
||||
kitsu_assets = gazu.asset.all_assets_for_shot(shot.kitsu_id)
|
||||
|
||||
return [
|
||||
AssetRef(name=asset_data['name'], code=asset_data['code'])
|
||||
for asset_data in kitsu_assets
|
||||
]
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
project = cache.project_active_get()
|
||||
return RenderSettings(
|
||||
width=int(project.resolution.split('x')[0]),
|
||||
height=int(project.resolution.split('x')[1]),
|
||||
frames_per_second=project.fps,
|
||||
)
|
@ -1,246 +0,0 @@
|
||||
# Project Description (DRAFT)
|
||||
|
||||
Shot Builder is an Add-on that helps studios to work with task specific
|
||||
Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are
|
||||
|
||||
* Build blend files for a specific task and shot.
|
||||
* Sync data back from work files to places like kitsu, or `edit.blend`.
|
||||
|
||||
## Design Principles
|
||||
|
||||
The main design principles are:
|
||||
|
||||
* The core-tool can be installed as an add-on, but the (production specific)
|
||||
configuration should be part of the production repository.
|
||||
* The configuration files are a collection of python files. The API between
|
||||
the configuration files and the add-on should be easy to use as pipeline
|
||||
TDs working on the production should be able to work with it.
|
||||
* TDs/artists should be able to handle issues during building without looking
|
||||
at how the add-on is structured.
|
||||
* The tool contains connectors that can be configured to read/write data
|
||||
from the system/file that is the main location of the data. For example
|
||||
The start and end time of a shot could be stored in an external production tracking application.
|
||||
|
||||
## Connectors
|
||||
|
||||
Connectors are components that can be used to read or write to files or
|
||||
systems. The connectors will add flexibility to the add-on so it could be used
|
||||
in multiple productions or studios.
|
||||
|
||||
In the configuration files the TD can setup the connectors that are used for
|
||||
the production. Possible connectors would be:
|
||||
|
||||
* Connector for text based config files (json/yaml).
|
||||
* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html).
|
||||
* Connector for blend files.
|
||||
|
||||
## Layering & Hooks
|
||||
|
||||
The configuration of the tool is layered. When building a work file for a sequence
|
||||
there are multiple ways to change the configuration.
|
||||
|
||||
* Configuration for the production.
|
||||
* Configuration for the asset that is needed.
|
||||
* Configuration for the asset type of the loaded asset.
|
||||
* Configuration for the sequence.
|
||||
* Configuration for the shot.
|
||||
* Configuration for the task type.
|
||||
|
||||
For any combination of these configurations hooks can be defined.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A')
|
||||
def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides when Spring is loaded in 02_020A.
|
||||
"""
|
||||
|
||||
@shot_tools.hook(match_task_type='anim')
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task.
|
||||
"""
|
||||
```
|
||||
|
||||
### Data
|
||||
|
||||
All hooks must have Python’s `**kwargs` parameter. The `kwargs` contains
|
||||
the context at the moment the hook is invoked. The context can contain the
|
||||
following items.
|
||||
|
||||
* `production`: `shot_tools.Production`: Include the name of the production
|
||||
and the location on the filesystem.
|
||||
* `task`: `shot_tools.Task`: The task (combination of task_type and shot)
|
||||
* `task_type`: `shot_tools.TaskType`: Is part of the `task`.
|
||||
* `sequence`: `shot_tools.Sequence`: Is part of `shot`.
|
||||
* `shot`: `shot_tools.Shot` Is part of `task`.
|
||||
* `asset`: `shot_tools.Asset`: Only available during asset loading phase.
|
||||
* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase.
|
||||
|
||||
### Execution Order
|
||||
|
||||
The add-on will internally create a list containing the hooks that needs to be
|
||||
executed for the command in a sensible order. It will then execute them in that
|
||||
order.
|
||||
|
||||
By default the next order will be used:
|
||||
|
||||
* Production wide hooks
|
||||
* Asset Type hooks
|
||||
* Asset hooks
|
||||
* Sequence hooks
|
||||
* Shot hooks
|
||||
* Task type hooks
|
||||
|
||||
A hook with a single ‘match’ rule will be run in the corresponding phase. A hook with
|
||||
multiple ‘match’ rules will be run in the last matching phase. For example, a hook with
|
||||
‘asset’ and ‘task type’ match rules will be run in the ‘task type’ phase.
|
||||
|
||||
#### Events
|
||||
|
||||
Order of execution can be customized by adding the optional `run_before`
|
||||
or `run_after` parameters.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_task_type='anim',
|
||||
requires={shot_tools.events.AssetsLoaded, hook_task_other_anim},
|
||||
is_required_by={shot_tools.events.ShotOverrides})
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task run after all assets have been loaded.
|
||||
"""
|
||||
```
|
||||
|
||||
Events could be:
|
||||
|
||||
* `shot_tools.events.BuildStart`
|
||||
* `shot_tools.events.ProductionSettingsLoaded`
|
||||
* `shot_tools.events.AssetsLoaded`
|
||||
* `shot_tools.events.AssetTypeOverrides`
|
||||
* `shot_tools.events.SequenceOverrides`
|
||||
* `shot_tools.events.ShotOverrides`
|
||||
* `shot_tools.events.TaskTypeOverrides`
|
||||
* `shot_tools.events.BuildFinished`
|
||||
* `shot_tools.events.HookStart`
|
||||
* `shot_tools.events.HookEnd`
|
||||
|
||||
During usage we should see which one of these or other events are needed.
|
||||
|
||||
`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded`
|
||||
and `shot_tools.events.HookStart` can only be used in the `run_after`
|
||||
parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished`
|
||||
can only be used in the `run_before` parameter.
|
||||
|
||||
|
||||
## API
|
||||
|
||||
The shot builder has an API between the add-on and the configuration files. This
|
||||
API contains convenience functions and classes to hide complexity and makes
|
||||
sure that the configuration files are easy to maintain.
|
||||
|
||||
```
|
||||
register_task_type(task_type="anim")
|
||||
register_task_type(task_type="lighting")
|
||||
```
|
||||
|
||||
```
|
||||
# shot_tool/characters.py
|
||||
class Asset(shot_tool.some_module.Asset):
|
||||
asset_file = "/{asset_type}/{name}/{name}.blend"
|
||||
collection = “{class_name}”
|
||||
name = “{class_name}”
|
||||
|
||||
class Character(Asset):
|
||||
asset_type = ‘char’
|
||||
|
||||
|
||||
class Ellie(Character):
|
||||
collection = “{class_name}-{variant_name}”
|
||||
variants = {‘default’, ‘short_hair’}
|
||||
|
||||
class Victoria(Character): pass
|
||||
class Rex(Character): pass
|
||||
|
||||
# shot_tool/shots.py
|
||||
class Shot_01_020_A(shot_tool.some_module.Shot):
|
||||
shot_id = ‘01_020_A’
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
sets.LogOverChasm,
|
||||
}
|
||||
|
||||
class AllHumansShot(shot_tool.some_module.Shot):
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
characters.Victoria,
|
||||
}
|
||||
|
||||
class Shot_01_035_A(AllHumansShot):
|
||||
assets = {
|
||||
sets.Camp,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This API is structured/implemented in a way that it keeps track of what
|
||||
is being done. This will be used when an error occurs so a descriptive
|
||||
error message can be generated that would help the TD to solve the issue more
|
||||
quickly. The goal would be that the error messages are descriptive enough to
|
||||
direct the TD into the direction where the actual cause is. And when possible
|
||||
propose several solutions to fix it.
|
||||
|
||||
## Setting up the tool
|
||||
|
||||
The artist/TD can configure their current local project directory in the add-on preferences.
|
||||
This can then be used for new blend files. The project associated with an opened (so existing)
|
||||
blend file can be found automatically by iterating over parent directories until a Shot Builder
|
||||
configuration file is found. Project-specific settings are not configured/stored in the add-on,
|
||||
but in this configuration file.
|
||||
|
||||
The add-on will look in the root of the production repository to locate the
|
||||
main configuration file `/project_root_directory/pro/shot-builder/config.py`. This file contains general
|
||||
settings about the production, including:
|
||||
|
||||
* The name of the production for reporting back to the user when needed.
|
||||
* Naming standards to test against when reporting deviations.
|
||||
* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production.
|
||||
* Configuration of the needed connectors.
|
||||
|
||||
### Directory Layout
|
||||
``` bash
|
||||
└── project-name/ # Project Root Directory
|
||||
└── pro/
|
||||
├── assets/
|
||||
├── shot-builder/
|
||||
│ ├── assets.py
|
||||
│ ├── config.py
|
||||
│ ├── hooks.py
|
||||
│ └── shots.py
|
||||
└── shots/
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Any artist can open a shot file via the `File` menu. A modal panel appears
|
||||
where the user can select the task type and sequence/shot. When the file
|
||||
already exists, it will be opened. When the file doesn't exist, the file
|
||||
will be built.
|
||||
|
||||
In the future other use cases will also be accessible, such as:
|
||||
|
||||
* Syncing data back from a work file to the source of the data.
|
||||
* Report of errors/differences between the shot file and the configuration.
|
||||
|
||||
## Open Issues
|
||||
|
||||
### Security
|
||||
|
||||
* Security keys needed by connectors need to be stored somewhere. The easy
|
||||
place is to place inside the production repository, but that isn't secure
|
||||
Anyone with access to the repository could misuse the keys to access the
|
||||
connector. Other solution might be to use the OS key store or retrieve the
|
||||
keys from an online service authenticated by the blender cloud add-on.
|
||||
|
||||
We could use `keyring` to access OS key stores.
|
@ -1,4 +0,0 @@
|
||||
# Example configuration files
|
||||
|
||||
This folder contains an example shot builder configuration. It shows the part
|
||||
that a TD would do to incorporate the shot builder in a production.
|
@ -1,30 +0,0 @@
|
||||
from blender_kitsu.shot_builder.asset import Asset
|
||||
|
||||
|
||||
class ProductionAsset(Asset):
|
||||
path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend" # Path to most assets
|
||||
color_tag = "NONE"
|
||||
|
||||
|
||||
# Categories
|
||||
class Character(ProductionAsset):
|
||||
asset_type = "chars"
|
||||
collection = "CH-{asset.code}" # Prefix for characters
|
||||
|
||||
|
||||
class Prop(ProductionAsset):
|
||||
asset_type = "props"
|
||||
collection = "PR-{asset.code}" # Prefix for props
|
||||
|
||||
|
||||
# Assets
|
||||
class MyCharacter(Character):
|
||||
name = "My Character" # Name on Kitsu Server
|
||||
code = "mycharacter" # Name of Collection without prefix (e.g. CH-mycharacter)
|
||||
path = "{production.path}/assets/{asset.asset_type}/mycharacter/publish/mycharacter.v001.blend" # This asset has a custom path
|
||||
color_tag = "COLOR_01"
|
||||
|
||||
|
||||
class MyProp(Prop):
|
||||
name = "MyProp"
|
||||
code = "myprop"
|
@ -1,13 +0,0 @@
|
||||
from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector
|
||||
|
||||
PRODUCTION_NAME = KitsuConnector
|
||||
SHOTS = KitsuConnector
|
||||
ASSETS = KitsuConnector
|
||||
RENDER_SETTINGS = KitsuConnector
|
||||
|
||||
# Formatting rules
|
||||
# ----------------
|
||||
|
||||
# The name of the scene in blender where the shot is build in.
|
||||
SCENE_NAME_FORMAT = "{shot.name}.{task_type}"
|
||||
SHOT_NAME_FORMAT = "{shot.name}"
|
@ -1,170 +0,0 @@
|
||||
import bpy
|
||||
from blender_kitsu.shot_builder.hooks import hook, Wildcard
|
||||
from blender_kitsu.shot_builder.asset import Asset
|
||||
from blender_kitsu.shot_builder.shot import Shot
|
||||
from blender_kitsu.shot_builder.project import Production
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------- Global Hook ----------
|
||||
|
||||
|
||||
CAMERA_NAME = 'CAM-camera'
|
||||
|
||||
|
||||
@hook()
|
||||
def set_cycles_render_engine(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
By default we set Cycles as the renderer.
|
||||
"""
|
||||
scene.render.engine = 'CYCLES'
|
||||
|
||||
|
||||
# ---------- Overrides for animation files ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def task_type_anim_set_workbench(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
Override of the render engine to Workbench when building animation files.
|
||||
"""
|
||||
scene.render.engine = 'BLENDER_WORKBENCH'
|
||||
|
||||
|
||||
# ---------- Create output collection for animation files ----------
|
||||
|
||||
|
||||
def _add_camera_rig(
|
||||
scene: bpy.types.Scene,
|
||||
production: Production,
|
||||
shot: Shot,
|
||||
):
|
||||
"""
|
||||
Function to load the camera rig. The rig will be added to the output collection
|
||||
of the shot and the camera will be set as active camera.
|
||||
"""
|
||||
# Load camera rig.
|
||||
path = f"{production.path}/assets/cam/camera_rig.blend"
|
||||
|
||||
if not Path(path).exists():
|
||||
camera_data = bpy.data.cameras.new(name=CAMERA_NAME)
|
||||
camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data)
|
||||
shot.output_collection.objects.link(camera_object)
|
||||
return
|
||||
|
||||
collection_name = "CA-camera_rig"
|
||||
bpy.ops.wm.link(
|
||||
filepath=path,
|
||||
directory=path + "/Collection",
|
||||
filename=collection_name,
|
||||
)
|
||||
# Keep the active object name as this would also be the name of the collection after enabling library override.
|
||||
active_object_name = bpy.context.active_object.name
|
||||
|
||||
# Make library override.
|
||||
bpy.ops.object.make_override_library()
|
||||
|
||||
# Add camera collection to the output collection
|
||||
asset_collection = bpy.data.collections[active_object_name]
|
||||
shot.output_collection.children.link(asset_collection)
|
||||
|
||||
# Set the camera of the camera rig as active scene camera.
|
||||
camera = bpy.data.objects[CAMERA_NAME]
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def task_type_anim_output_collection(
|
||||
scene: bpy.types.Scene, production: Production, shot: Shot, task_type: str, **kwargs
|
||||
):
|
||||
"""
|
||||
Animations are stored in an output collection. This collection will be linked
|
||||
by the lighting file.
|
||||
|
||||
Also loads the camera rig.
|
||||
"""
|
||||
output_collection = bpy.data.collections.new(
|
||||
name=shot.get_output_collection_name(shot=shot, task_type=task_type)
|
||||
)
|
||||
shot.output_collection = output_collection
|
||||
output_collection.use_fake_user = True
|
||||
scene.collection.children.link(output_collection)
|
||||
|
||||
_add_camera_rig(scene, production, shot)
|
||||
|
||||
|
||||
@hook(match_task_type='lighting')
|
||||
def link_anim_output_collection(
|
||||
scene: bpy.types.Scene, production: Production, shot: Shot, **kwargs
|
||||
):
|
||||
"""
|
||||
Link in the animation output collection from the animation file.
|
||||
"""
|
||||
anim_collection = bpy.data.collections.new(name="animation")
|
||||
scene.collection.children.link(anim_collection)
|
||||
anim_file_path = shot.get_anim_file_path(production, shot)
|
||||
anim_output_collection_name = shot.get_output_collection_name(
|
||||
shot=shot, task_type="anim"
|
||||
)
|
||||
result = bpy.ops.wm.link(
|
||||
filepath=anim_file_path,
|
||||
directory=anim_file_path + "/Collection",
|
||||
filename=anim_output_collection_name,
|
||||
)
|
||||
assert result == {'FINISHED'}
|
||||
|
||||
# Move the anim output collection from scene collection to the animation collection.
|
||||
anim_output_collection = bpy.data.objects[anim_output_collection_name]
|
||||
anim_collection.objects.link(anim_output_collection)
|
||||
scene.collection.objects.unlink(anim_output_collection)
|
||||
|
||||
# Use animation camera as active scene camera.
|
||||
camera = bpy.data.objects['CAM-camera']
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
# ---------- Asset loading and linking ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim', match_asset_type=['chars', 'props'])
|
||||
def link_char_prop_for_anim(scene: bpy.types.Scene, shot: Shot, asset: Asset, **kwargs):
|
||||
"""
|
||||
Loading a character or prop for an animation file.
|
||||
"""
|
||||
collection_names = []
|
||||
if asset.code == 'notepad_pencil':
|
||||
collection_names.append("PR-pencil")
|
||||
collection_names.append("PR-notepad")
|
||||
else:
|
||||
collection_names.append(asset.collection)
|
||||
|
||||
for collection_name in collection_names:
|
||||
logger.info("link asset")
|
||||
bpy.ops.wm.link(
|
||||
filepath=str(asset.path),
|
||||
directory=str(asset.path) + "/Collection",
|
||||
filename=collection_name,
|
||||
)
|
||||
# Keep the active object name as this would also be the name of the collection after enabling library override.
|
||||
active_object_name = bpy.context.active_object.name
|
||||
|
||||
# Make library override.
|
||||
bpy.ops.object.make_override_library()
|
||||
|
||||
# Add overridden collection to the output collection.
|
||||
asset_collection = bpy.data.collections[active_object_name]
|
||||
shot.output_collection.children.link(asset_collection)
|
||||
|
||||
|
||||
@hook(match_task_type=Wildcard, match_asset_type='sets')
|
||||
def link_set(asset: Asset, **kwargs):
|
||||
"""
|
||||
Load the set of the shot.
|
||||
"""
|
||||
bpy.ops.wm.link(
|
||||
filepath=str(asset.path),
|
||||
directory=str(asset.path) + "/Collection",
|
||||
filename=asset.collection,
|
||||
)
|
@ -1,6 +0,0 @@
|
||||
# Example configuration files
|
||||
|
||||
This folder contains an example shot builder configuration. It shows the part
|
||||
that a TD would do to incorporate the shot builder in a production.
|
||||
|
||||
|
@ -1,41 +0,0 @@
|
||||
from blender_kitsu.shot_builder.shot import Shot
|
||||
from blender_kitsu.shot_builder.project import Production
|
||||
|
||||
|
||||
class ProductionShot(Shot):
|
||||
def get_anim_file_path(self, production: Production, shot: Shot) -> str:
|
||||
"""Get the animation file path for this given shot."""
|
||||
return self.file_path_format.format_map(
|
||||
{'production': production, 'shot': shot, 'task_type': "anim"}
|
||||
)
|
||||
|
||||
def get_lighting_file_path(self, production: Production, shot: Shot) -> str:
|
||||
"""Get the lighting file path for this given shot."""
|
||||
return self.file_path_format.format_map(
|
||||
{'production': production, 'shot': shot, 'task_type': "lighting"}
|
||||
)
|
||||
|
||||
def get_output_collection_name(self, shot: Shot, task_type: str) -> str:
|
||||
"""Get the collection name where the output is stored."""
|
||||
return f"{shot.name}.{task_type}.output"
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if this shot contains all data, so it could be selected
|
||||
for shot building.
|
||||
"""
|
||||
if not super().is_valid():
|
||||
return False
|
||||
return True
|
||||
|
||||
# Assuming path to file is in `project_name/svn/pro/shot/sequence_name/shot_name`
|
||||
# Render Ouput path should be `project_name/shared/shot_frames/sequence_name/shot_name/`
|
||||
|
||||
def get_render_output_dir(self) -> str:
|
||||
return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.lighting"
|
||||
|
||||
def get_comp_output_dir(self) -> str:
|
||||
return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.comp"
|
||||
|
||||
|
||||
class GenericShot(ProductionShot):
|
||||
is_generic = True
|
@ -1,30 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
from . import ops
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ops.unregister()
|
@ -1,82 +0,0 @@
|
||||
import bpy
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
from ... import prefs
|
||||
from ... 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)
|
||||
strip_channel = 1
|
||||
latest_file = editorial_export_check_latest(context)
|
||||
if not latest_file:
|
||||
return None
|
||||
# Check if Kitsu server returned empty shot
|
||||
if shot.get("id") == '':
|
||||
return None
|
||||
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(
|
||||
latest_file.name,
|
||||
strip_filepath,
|
||||
strip_channel + 1,
|
||||
strip_frame_start,
|
||||
fit_method="FIT",
|
||||
)
|
||||
sound_strip = seq_editor.sequences.new_sound(
|
||||
latest_file.name,
|
||||
strip_filepath,
|
||||
strip_channel,
|
||||
strip_frame_start,
|
||||
)
|
||||
new_strips = [movie_strip, sound_strip]
|
||||
|
||||
# Update shift frame range prop.
|
||||
frame_in = shot["data"].get("frame_in")
|
||||
frame_3d_start = shot["data"].get("3d_start")
|
||||
frame_3d_offset = frame_3d_start - 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 + edit_export_offset
|
||||
)
|
||||
return new_strips
|
||||
|
||||
|
||||
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
|
@ -1,42 +0,0 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from .core import editorial_export_get_latest
|
||||
from ... import cache
|
||||
import gazu
|
||||
|
||||
|
||||
class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator):
|
||||
bl_idname = "asset_setup.load_latest_editorial"
|
||||
bl_label = "Load Editorial Export"
|
||||
bl_description = (
|
||||
"Loads latest edit from shot_preview_folder "
|
||||
"Shifts edit so current shot starts at 3d_start metadata shot key from Kitsu"
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
cache_shot = cache.shot_active_get()
|
||||
shot = gazu.shot.get_shot(cache_shot.id) # TODO INEFFICENT TO LOAD SHOT TWICE
|
||||
strips = editorial_export_get_latest(context, shot)
|
||||
if strips is None:
|
||||
self.report(
|
||||
{"ERROR"}, f"No valid editorial export in editorial export path."
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.report({"INFO"}, f"Loaded latest edit: {strips[0].name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = [
|
||||
ANIM_SETUP_OT_load_latest_editorial,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,8 +1,14 @@
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import *
|
||||
|
||||
import typing
|
||||
import types
|
||||
from collections.abc import Iterable
|
||||
|
||||
import importlib
|
||||
from .. import prefs
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -44,6 +50,8 @@ HookFunction = typing.Callable[[typing.Any], None]
|
||||
def _match_hook_parameter(
|
||||
hook_criteria: MatchCriteriaType, match_query: typing.Optional[str]
|
||||
) -> bool:
|
||||
if hook_criteria == None:
|
||||
return True
|
||||
if hook_criteria == DoNotMatch:
|
||||
return match_query is None
|
||||
if hook_criteria == Wildcard:
|
||||
@ -60,10 +68,6 @@ class Hooks:
|
||||
def __init__(self):
|
||||
self._hooks: typing.List[HookFunction] = []
|
||||
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def matches(
|
||||
self,
|
||||
hook: HookFunction,
|
||||
@ -73,7 +77,6 @@ class Hooks:
|
||||
) -> bool:
|
||||
assert not kwargs
|
||||
rules = typing.cast(MatchingRulesType, getattr(hook, '_shot_builder_rules'))
|
||||
|
||||
return all(
|
||||
(
|
||||
_match_hook_parameter(rules['match_task_type'], match_task_type),
|
||||
@ -86,32 +89,56 @@ class Hooks:
|
||||
if self.matches(hook=hook, **kwargs):
|
||||
yield hook
|
||||
|
||||
def execute_hooks(
|
||||
self, match_task_type: str = None, match_asset_type: str = None, *args, **kwargs
|
||||
) -> None:
|
||||
for hook in self._hooks:
|
||||
if self.matches(
|
||||
hook, match_task_type=match_task_type, match_asset_type=match_asset_type
|
||||
):
|
||||
hook(*args, **kwargs)
|
||||
|
||||
def _register_hook(func: types.FunctionType) -> None:
|
||||
from .project import get_active_production
|
||||
def load_hooks(self, context):
|
||||
root_dir = prefs.project_root_dir_get(context)
|
||||
shot_builder_config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder")
|
||||
if not shot_builder_config_dir.exists():
|
||||
raise Exception("Shot Builder Hooks directory does not exist")
|
||||
paths = [
|
||||
shot_builder_config_dir.resolve().__str__() # TODO Make variable path
|
||||
] # TODO Set path to where hooks are stored
|
||||
with SystemPathInclude(paths) as _include:
|
||||
try:
|
||||
import hooks as production_hooks
|
||||
|
||||
production = get_active_production()
|
||||
production.hooks.register(func)
|
||||
importlib.reload(production_hooks)
|
||||
self.register_hooks(production_hooks)
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("Production has no `hooks.py` configuration file")
|
||||
|
||||
return False
|
||||
|
||||
def register_hooks(module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
_register_hook(module_item)
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def register_hooks(self, module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
self.register(module_item)
|
||||
|
||||
|
||||
def hook(
|
||||
match_task_type: MatchCriteriaType = DoNotMatch,
|
||||
match_asset_type: MatchCriteriaType = DoNotMatch,
|
||||
match_task_type: MatchCriteriaType = None,
|
||||
match_asset_type: MatchCriteriaType = None,
|
||||
) -> typing.Callable[[types.FunctionType], types.FunctionType]:
|
||||
"""
|
||||
Decorator to add custom logic when building a shot.
|
||||
@ -128,3 +155,41 @@ def hook(
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SystemPathInclude:
|
||||
"""
|
||||
Resource class to temporary include system paths to `sys.paths`.
|
||||
|
||||
Usage:
|
||||
```
|
||||
paths = [pathlib.Path("/home/guest/my_python_scripts")]
|
||||
with SystemPathInclude(paths) as t:
|
||||
import my_module
|
||||
reload(my_module)
|
||||
```
|
||||
|
||||
It is possible to nest multiple SystemPathIncludes.
|
||||
"""
|
||||
|
||||
def __init__(self, paths_to_add: List[pathlib.Path]):
|
||||
# TODO: Check if all paths exist and are absolute.
|
||||
self.__paths = paths_to_add
|
||||
self.__original_sys_path: List[str] = []
|
||||
|
||||
def __enter__(self):
|
||||
self.__original_sys_path = sys.path
|
||||
new_sys_path = []
|
||||
for path_to_add in self.__paths:
|
||||
# Do not add paths that are already in the sys path.
|
||||
# Report this to the logger as this might indicate wrong usage.
|
||||
path_to_add_str = str(path_to_add)
|
||||
if path_to_add_str in self.__original_sys_path:
|
||||
logger.warn(f"{path_to_add_str} already added to `sys.path`")
|
||||
continue
|
||||
new_sys_path.append(path_to_add_str)
|
||||
new_sys_path.extend(self.__original_sys_path)
|
||||
sys.path = new_sys_path
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
sys.path = self.__original_sys_path
|
||||
|
@ -1,298 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import pathlib
|
||||
from typing import *
|
||||
import bpy
|
||||
import gazu
|
||||
from .shot import ShotRef
|
||||
from .project import (
|
||||
ensure_loaded_production,
|
||||
get_active_production,
|
||||
)
|
||||
from .builder import ShotBuilder
|
||||
from .task_type import TaskType
|
||||
from .. import prefs, cache
|
||||
from .anim_setup.core import (
|
||||
animation_workspace_delete_others,
|
||||
animation_workspace_vse_area_add,
|
||||
)
|
||||
from .editorial.core import editorial_export_get_latest
|
||||
from .builder.save_file import save_shot_builder_file
|
||||
|
||||
|
||||
_production_task_type_items: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_task_type_items(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_task_type_items
|
||||
return _production_task_type_items
|
||||
|
||||
|
||||
_production_seq_id_items: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_seq_id_items(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_seq_id_items
|
||||
return _production_seq_id_items
|
||||
|
||||
|
||||
_production_shots: List[ShotRef] = []
|
||||
|
||||
|
||||
def production_shots(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_shots
|
||||
return _production_shots
|
||||
|
||||
|
||||
_production_shot_id_items_for_seq: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_shot_id_items_for_seq(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_shot_id_items_for_seq
|
||||
global _production_shot_id_items
|
||||
|
||||
if not self.seq_id or not _production_shots:
|
||||
return []
|
||||
|
||||
shots_for_seq: List[Tuple(str, str, str)] = [
|
||||
(s.name, s.name, "")
|
||||
for s in _production_shots
|
||||
if s.sequence.name == self.seq_id
|
||||
]
|
||||
|
||||
_production_shot_id_items_for_seq.clear()
|
||||
_production_shot_id_items_for_seq.extend(shots_for_seq)
|
||||
|
||||
return _production_shot_id_items_for_seq
|
||||
|
||||
|
||||
def reset_shot_id_enum(self: Any, context: bpy.types.Context) -> None:
|
||||
production_shot_id_items_for_seq(self, context)
|
||||
global _production_shot_id_items_for_seq
|
||||
if _production_shot_id_items_for_seq:
|
||||
self.shot_id = _production_shot_id_items_for_seq[0][0]
|
||||
|
||||
|
||||
class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator):
|
||||
"""Build a new shot file"""
|
||||
|
||||
bl_idname = "shotbuilder.new_shot_file"
|
||||
bl_label = "New Production Shot File"
|
||||
|
||||
_timer = None
|
||||
_built_shot = False
|
||||
_add_vse_area = False
|
||||
_file_path = ''
|
||||
|
||||
production_root: bpy.props.StringProperty( # type: ignore
|
||||
name="Production Root", description="Root of the production", subtype='DIR_PATH'
|
||||
)
|
||||
|
||||
production_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Production",
|
||||
description="Name of the production to create a shot file for",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
seq_id: bpy.props.EnumProperty( # type: ignore
|
||||
name="Sequence ID",
|
||||
description="Sequence ID of the shot to build",
|
||||
items=production_seq_id_items,
|
||||
update=reset_shot_id_enum,
|
||||
)
|
||||
|
||||
shot_id: bpy.props.EnumProperty( # type: ignore
|
||||
name="Shot ID",
|
||||
description="Shot ID of the shot to build",
|
||||
items=production_shot_id_items_for_seq,
|
||||
)
|
||||
|
||||
task_type: bpy.props.EnumProperty( # type: ignore
|
||||
name="Task",
|
||||
description="Task to create the shot file for",
|
||||
items=production_task_type_items,
|
||||
)
|
||||
auto_save: bpy.props.BoolProperty(
|
||||
name="Save after building.",
|
||||
description="Automatically save build file after 'Shot Builder' is complete.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == 'TIMER' and not self._add_vse_area:
|
||||
# Show Storyboard/Animatic from VSE
|
||||
"""Running as Modal Event because functions within execute() function like
|
||||
animation_workspace_delete_others() changed UI context that needs to be refreshed.
|
||||
https://docs.blender.org/api/current/info_gotcha.html#no-updates-after-changing-ui-context
|
||||
"""
|
||||
# TODO this is a hack, should be inherient to above builder
|
||||
# TODO fix during refactor
|
||||
if self.task_type == 'anim':
|
||||
animation_workspace_vse_area_add(context)
|
||||
self._add_vse_area = True
|
||||
|
||||
if self._built_shot and self._add_vse_area:
|
||||
if self.auto_save:
|
||||
file_path = pathlib.Path()
|
||||
try:
|
||||
save_shot_builder_file(self._file_path)
|
||||
self.report(
|
||||
{"INFO"}, f"Saved Shot{self.shot_id} at {self._file_path}"
|
||||
)
|
||||
return {'FINISHED'}
|
||||
except FileExistsError:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Cannot create a file/folder when that file/folder already exists {file_path}",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
self.report({"INFO"}, f"Built Shot {self.shot_id}, file is not saved!")
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
project = cache.project_active_get()
|
||||
|
||||
if addon_prefs.session.is_auth() is False:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if project.id == "":
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the project root directory. \nCheck project root directiory is configured in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.production_root = addon_prefs.project_root_dir
|
||||
self.production_name = project.name
|
||||
|
||||
if not ensure_loaded_production(context):
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Shot builder configuration files not found in current project directory. \nCheck addon preferences to ensure project root contains shot_builder config.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
production = get_active_production()
|
||||
|
||||
global _production_task_type_items
|
||||
_production_task_type_items = production.get_task_type_items(context=context)
|
||||
|
||||
global _production_seq_id_items
|
||||
_production_seq_id_items = production.get_seq_items(context=context)
|
||||
|
||||
global _production_shots
|
||||
_production_shots = production.get_shots(context=context)
|
||||
|
||||
return cast(
|
||||
Set[str], context.window_manager.invoke_props_dialog(self, width=400)
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(0.1, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
if not self.production_root:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Shot builder can only be started from the File menu. Shortcuts like CTRL-N don't work",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self._built_shot:
|
||||
return {'RUNNING_MODAL'}
|
||||
ensure_loaded_production(context)
|
||||
production = get_active_production()
|
||||
shot_builder = ShotBuilder(
|
||||
context=context,
|
||||
production=production,
|
||||
shot_name=self.shot_id,
|
||||
task_type=TaskType(self.task_type),
|
||||
)
|
||||
shot_builder.create_build_steps()
|
||||
shot_builder.build()
|
||||
|
||||
active_project = cache.project_active_get()
|
||||
|
||||
# Build Kitsu Context
|
||||
sequence = gazu.shot.get_sequence_by_name(active_project.id, self.seq_id)
|
||||
shot = gazu.shot.get_shot_by_name(sequence, self.shot_id)
|
||||
|
||||
# TODO this is a hack, should be inherient to above builder
|
||||
# TODO fix during refactor
|
||||
if self.task_type == 'anim':
|
||||
# Load EDIT
|
||||
editorial_export_get_latest(context, shot)
|
||||
# Load Anim Workspace
|
||||
animation_workspace_delete_others()
|
||||
|
||||
# Initilize armatures
|
||||
for obj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]:
|
||||
base_name = obj.name.split(addon_prefs.shot_builder_armature_prefix)[-1]
|
||||
new_action = bpy.data.actions.new(
|
||||
f"{addon_prefs.shot_builder_action_prefix}{base_name}.{self.shot_id}.v001"
|
||||
)
|
||||
new_action.use_fake_user = True
|
||||
obj.animation_data.action = new_action
|
||||
|
||||
# Set Shot Frame Range
|
||||
context.scene.frame_start = int(shot["data"].get("3d_start"))
|
||||
context.scene.frame_end = (
|
||||
int(shot["data"].get("3d_start")) + shot.get('nb_frames') - 1
|
||||
)
|
||||
|
||||
# Run User Script
|
||||
exec(addon_prefs.user_exec_code)
|
||||
|
||||
self._file_path = shot_builder.build_context.shot.file_path
|
||||
self._built_shot = True
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
layout.prop(self, "seq_id")
|
||||
layout.prop(self, "shot_id")
|
||||
layout.prop(self, "task_type")
|
||||
layout.prop(self, "auto_save")
|
@ -1,460 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import importlib
|
||||
from collections import defaultdict
|
||||
|
||||
import bpy
|
||||
|
||||
from .task_type import *
|
||||
from .shot import Shot, ShotRef
|
||||
from .render_settings import RenderSettings
|
||||
from .asset import Asset, AssetRef
|
||||
from .sys_utils import *
|
||||
from .hooks import Hooks, register_hooks
|
||||
from .connectors.default import DefaultConnector
|
||||
from .connectors.connector import Connector
|
||||
import os
|
||||
|
||||
from .. import prefs
|
||||
from pathlib import Path
|
||||
|
||||
from typing import *
|
||||
import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Production:
|
||||
"""
|
||||
Class containing data and methods for a production.
|
||||
|
||||
# Data members #
|
||||
path: contains the path to the root of the production.
|
||||
task_types: contains a list of `TaskType`s or a Connector to retrieve that are defined for this
|
||||
production. By default the task_types are prefilled with anim and light.
|
||||
name: human readable name of the production.
|
||||
|
||||
"""
|
||||
|
||||
__ATTRNAMES_SUPPORTING_CONNECTOR = ['task_types', 'shots', 'name']
|
||||
|
||||
def __init__(self, production_path: pathlib.Path):
|
||||
self.path = production_path
|
||||
self.task_types: List[TaskType] = []
|
||||
self.task_types_connector = DefaultConnector
|
||||
self.shots_connector = DefaultConnector
|
||||
self.assets: List[type] = []
|
||||
self.shots: List[Shot] = []
|
||||
self.name = ""
|
||||
self.name_connector = DefaultConnector
|
||||
self.render_settings_connector = DefaultConnector
|
||||
self.config: Dict[str, Any] = {}
|
||||
self.__shot_lookup: Dict[str, Shot] = {}
|
||||
self.hooks: Hooks = Hooks()
|
||||
self.shot_data_synced = False
|
||||
|
||||
self.scene_name_format = "{shot.sequence_code}_{shot.code}.{task_type}"
|
||||
self.shot_name_format = "{shot.sequence_code}_{shot.code}"
|
||||
self.file_name_format = (
|
||||
"{production.path}shots/{shot.code}/{shot.code}.{task_type}.blend"
|
||||
)
|
||||
|
||||
def __create_connector(
|
||||
self, connector_cls: Type[Connector], context: bpy.types.Context
|
||||
) -> Connector:
|
||||
# TODO: Cache connector
|
||||
preferences = context.preferences.addons["blender_kitsu"].preferences
|
||||
return connector_cls(production=self, preferences=preferences)
|
||||
|
||||
def __format_shot_name(self, shot: Shot) -> str:
|
||||
return self.shot_name_format.format(shot=shot)
|
||||
|
||||
def get_task_type_items(
|
||||
self, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of task types items to be used in an item function of a
|
||||
`bpy.props.EnumProperty`
|
||||
"""
|
||||
if not self.task_types:
|
||||
connector = self.__create_connector(
|
||||
self.task_types_connector, context=context
|
||||
)
|
||||
self.task_types = connector.get_task_types()
|
||||
return [
|
||||
(task_type.name, task_type.name, task_type.name)
|
||||
for task_type in self.task_types
|
||||
]
|
||||
|
||||
def get_assets_for_shot(
|
||||
self, context: bpy.types.Context, shot: Shot
|
||||
) -> List[AssetRef]:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
|
||||
return connector.get_assets_for_shot(shot)
|
||||
|
||||
def get_shots(self, context: bpy.types.Context) -> List[ShotRef]:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
return connector.get_shots()
|
||||
|
||||
def get_shot(self, context: bpy.types.Context, shot_name: str) -> Optional[Shot]:
|
||||
self._ensure_shot_data(context)
|
||||
|
||||
for shot in self.shots:
|
||||
if shot.name == shot_name:
|
||||
return shot
|
||||
return None
|
||||
|
||||
def _ensure_shot_data(self, context: bpy.types.Context) -> None:
|
||||
if self.shot_data_synced:
|
||||
return
|
||||
# Find a generic shot definition. This class will be used as template
|
||||
# when no specific shot definition could be found.
|
||||
generic_shot_class = None
|
||||
for shot in self.shots:
|
||||
if shot.is_generic:
|
||||
generic_shot_class = shot.__class__
|
||||
break
|
||||
|
||||
shot_refs = self.get_shots(context)
|
||||
for shot_ref in shot_refs:
|
||||
logger.debug(f"Finding shot definition for {shot_ref.name}")
|
||||
for shot in self.shots:
|
||||
if shot.name == shot_ref.name:
|
||||
logger.debug(f"Shot definition found for {shot_ref.name}")
|
||||
shot_ref.sync_data(shot)
|
||||
break
|
||||
else:
|
||||
logger.info(f"No shot definition found for {shot_ref.name}")
|
||||
if generic_shot_class:
|
||||
logger.info(f"Using generic shot class")
|
||||
shot = generic_shot_class()
|
||||
shot_ref.sync_data(shot)
|
||||
shot.is_generic = False
|
||||
self.shots.append(shot)
|
||||
|
||||
self.shot_data_synced = True
|
||||
|
||||
def get_render_settings(
|
||||
self, context: bpy.types.Context, shot: Shot
|
||||
) -> RenderSettings:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
return connector.get_render_settings(shot)
|
||||
|
||||
def get_shot_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of shot items to be used in an item function of a
|
||||
`bpy.props.EnumProperty` to select a shot.
|
||||
"""
|
||||
result = []
|
||||
self._ensure_shot_data(context)
|
||||
sequences: Dict[str, List[Shot]] = defaultdict(list)
|
||||
for shot in self.shots:
|
||||
if not shot.is_valid():
|
||||
continue
|
||||
sequences[shot.sequence_code].append(shot)
|
||||
|
||||
sorted_sequences = sorted(sequences.keys())
|
||||
for sequence in sorted_sequences:
|
||||
result.append(("", sequence, sequence))
|
||||
for shot in sorted(sequences[sequence], key=lambda x: x.name):
|
||||
result.append((shot.name, self.__format_shot_name(shot), shot.name))
|
||||
|
||||
return result
|
||||
|
||||
def get_seq_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of seq items to be used in an item function of a
|
||||
`bpy.props.EnumProperty` to select a shot.
|
||||
"""
|
||||
shots = self.get_shots(context)
|
||||
sequences = list(set([s.sequence for s in shots]))
|
||||
sequences.sort(key=lambda seq: seq.name)
|
||||
|
||||
return [(seq.name, seq.name, "") for seq in sequences]
|
||||
|
||||
def get_name(self, context: bpy.types.Context) -> str:
|
||||
"""
|
||||
Get the name of the production
|
||||
"""
|
||||
if not self.name:
|
||||
connector = self.__create_connector(self.name_connector, context=context)
|
||||
self.name = connector.get_name()
|
||||
return self.name
|
||||
|
||||
# TODO: Use visitor pattern.
|
||||
def __load_name(self, main_config_mod: types.ModuleType) -> None:
|
||||
name = getattr(main_config_mod, "PRODUCTION_NAME", None)
|
||||
if name is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if isinstance(name, str):
|
||||
self.name = name
|
||||
return
|
||||
|
||||
if issubclass(name, Connector):
|
||||
self.name = ""
|
||||
self.name_connector = name
|
||||
return
|
||||
|
||||
logger.warn(
|
||||
"Skip loading of production name. Incorrect configuration detected."
|
||||
)
|
||||
|
||||
def __load_task_types(self, main_config_mod: types.ModuleType) -> None:
|
||||
task_types = getattr(main_config_mod, "TASK_TYPES", None)
|
||||
if task_types is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if isinstance(task_types, list):
|
||||
self.task_types = [TaskType(task_type) for task_type in task_types]
|
||||
return
|
||||
|
||||
if issubclass(task_types, Connector):
|
||||
self.task_types = task_types
|
||||
|
||||
logger.warn("Skip loading of task_types. Incorrect configuration detected.")
|
||||
|
||||
def __load_shots_connector(self, main_config_mod: types.ModuleType) -> None:
|
||||
shots = getattr(main_config_mod, "SHOTS", None)
|
||||
if shots is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if issubclass(shots, Connector):
|
||||
self.shots_connector = shots
|
||||
return
|
||||
|
||||
logger.warn("Skip loading of shots. Incorrect configuration detected.")
|
||||
|
||||
def __load_connector_keys(self, main_config_mod: types.ModuleType) -> None:
|
||||
connectors = set()
|
||||
for attrname in Production.__ATTRNAMES_SUPPORTING_CONNECTOR:
|
||||
connector = getattr(self, f"{attrname}_connector")
|
||||
connectors.add(connector)
|
||||
|
||||
connector_keys = set()
|
||||
for connector in connectors:
|
||||
for key in connector.PRODUCTION_KEYS:
|
||||
connector_keys.add(key)
|
||||
|
||||
for connector_key in connector_keys:
|
||||
if hasattr(main_config_mod, connector_key):
|
||||
self.config[connector_key] = getattr(main_config_mod, connector_key)
|
||||
|
||||
def __load_render_settings(self, main_config_mod: types.ModuleType) -> None:
|
||||
render_settings = getattr(main_config_mod, "RENDER_SETTINGS", None)
|
||||
if render_settings is None:
|
||||
return
|
||||
|
||||
if issubclass(render_settings, Connector):
|
||||
self.render_settings_connector = render_settings
|
||||
return
|
||||
|
||||
logger.warn("Skip loading of render settings. Incorrect configuration detected")
|
||||
|
||||
def __load_formatting_strings(self, main_config_mod: types.ModuleType) -> None:
|
||||
self.shot_name_format = getattr(
|
||||
main_config_mod, "SHOT_NAME_FORMAT", self.scene_name_format
|
||||
)
|
||||
self.scene_name_format = getattr(
|
||||
main_config_mod, "SCENE_NAME_FORMAT", self.scene_name_format
|
||||
)
|
||||
self.file_name_format = getattr(
|
||||
main_config_mod, "FILE_NAME_FORMAT", self.file_name_format
|
||||
)
|
||||
|
||||
def _load_config(self, main_config_mod: types.ModuleType) -> None:
|
||||
self.__load_name(main_config_mod)
|
||||
self.__load_task_types(main_config_mod)
|
||||
self.__load_shots_connector(main_config_mod)
|
||||
self.__load_connector_keys(main_config_mod)
|
||||
self.__load_render_settings(main_config_mod)
|
||||
self.__load_formatting_strings(main_config_mod)
|
||||
|
||||
def _load_asset_definitions(self, asset_mod: types.ModuleType) -> None:
|
||||
"""
|
||||
Load all assets from the given module.
|
||||
"""
|
||||
self.assets = []
|
||||
for module_item_str in dir(asset_mod):
|
||||
module_item = getattr(asset_mod, module_item_str)
|
||||
if module_item.__class__ != type:
|
||||
continue
|
||||
if not issubclass(module_item, Asset):
|
||||
continue
|
||||
if not hasattr(module_item, "name"):
|
||||
continue
|
||||
logger.info(f"loading asset config {module_item}")
|
||||
self.assets.append(module_item)
|
||||
# TODO: only add assets that are leaves
|
||||
|
||||
def _load_shot_definitions(self, shot_mod: types.ModuleType) -> None:
|
||||
"""
|
||||
Load all assets from the given module.
|
||||
"""
|
||||
self.shots = []
|
||||
for module_item_str in dir(shot_mod):
|
||||
module_item = getattr(shot_mod, module_item_str)
|
||||
if module_item.__class__ != type:
|
||||
continue
|
||||
if not issubclass(module_item, Shot):
|
||||
continue
|
||||
if not hasattr(module_item, "name"):
|
||||
continue
|
||||
logger.info(f"loading shot config {module_item}")
|
||||
self.shots.append(module_item())
|
||||
|
||||
|
||||
_PRODUCTION: Optional[Production] = None
|
||||
|
||||
|
||||
def is_valid_production_root(path: pathlib.Path) -> bool:
|
||||
"""
|
||||
Test if the given project path is configured correctly.
|
||||
|
||||
A valid project path contains a subfolder with the name `shot-builder`
|
||||
holding configuration files.
|
||||
"""
|
||||
if not path.is_absolute():
|
||||
return False
|
||||
if not path.exists():
|
||||
return False
|
||||
if not path.is_dir():
|
||||
return False
|
||||
config_file_path = get_production_config_file_path(path)
|
||||
return config_file_path.exists()
|
||||
|
||||
|
||||
def get_production_config_dir_path(path: pathlib.Path) -> pathlib.Path:
|
||||
"""
|
||||
Get the production configuration dir path.
|
||||
"""
|
||||
return path / "shot-builder"
|
||||
|
||||
|
||||
def get_production_config_file_path(path: pathlib.Path) -> pathlib.Path:
|
||||
"""
|
||||
Get the production configuration file path.
|
||||
"""
|
||||
return get_production_config_dir_path(path) / "config.py"
|
||||
|
||||
|
||||
def _find_production_root(path: pathlib.Path) -> Optional[pathlib.Path]:
|
||||
"""
|
||||
Given a path try to find the production root
|
||||
"""
|
||||
if is_valid_production_root(path):
|
||||
return path
|
||||
try:
|
||||
parent_path = path.parents[0]
|
||||
return _find_production_root(parent_path)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
# TODO: return type is optional
|
||||
def get_production_root(context: bpy.types.Context) -> Optional[pathlib.Path]:
|
||||
"""
|
||||
Determine the project root based on the current file.
|
||||
When current file isn't part of a project the project root
|
||||
configured in the add-on will be used.
|
||||
"""
|
||||
current_file = pathlib.Path(bpy.data.filepath)
|
||||
production_root = _find_production_root(current_file)
|
||||
if production_root:
|
||||
return production_root
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
production_root = Path(addon_prefs.project_root_dir)
|
||||
if is_valid_production_root(production_root):
|
||||
return production_root
|
||||
return None
|
||||
|
||||
|
||||
def ensure_loaded_production(context: bpy.types.Context) -> bool:
|
||||
"""
|
||||
Ensure that the production of the current context is loaded.
|
||||
|
||||
Returns if the production of for the given context is loaded.
|
||||
"""
|
||||
global _PRODUCTION
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
base_path = Path(addon_prefs.project_root_dir)
|
||||
production_root = os.path.join(
|
||||
base_path, "pro"
|
||||
) # TODO Fix during refactor should use base_path
|
||||
if is_valid_production_root(Path(production_root)):
|
||||
logger.debug(f"loading new production configuration from '{production_root}'.")
|
||||
__load_production_configuration(context, Path(production_root))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def __load_production_configuration(
|
||||
context: bpy.types.Context, production_path: pathlib.Path
|
||||
) -> bool:
|
||||
global _PRODUCTION
|
||||
_PRODUCTION = Production(production_path)
|
||||
paths = [production_path / "shot-builder"]
|
||||
with SystemPathInclude(paths) as _include:
|
||||
try:
|
||||
import config as production_config
|
||||
|
||||
importlib.reload(production_config)
|
||||
_PRODUCTION._load_config(production_config)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `config.py` configuration file")
|
||||
|
||||
try:
|
||||
import shots as production_shots
|
||||
|
||||
importlib.reload(production_shots)
|
||||
_PRODUCTION._load_shot_definitions(production_shots)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `shots.py` configuration file")
|
||||
|
||||
try:
|
||||
import assets as production_assets
|
||||
|
||||
importlib.reload(production_assets)
|
||||
_PRODUCTION._load_asset_definitions(production_assets)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `assets.py` configuration file")
|
||||
|
||||
try:
|
||||
import hooks as production_hooks
|
||||
|
||||
importlib.reload(production_hooks)
|
||||
register_hooks(production_hooks)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `hooks.py` configuration file")
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_active_production() -> Production:
|
||||
global _PRODUCTION
|
||||
assert _PRODUCTION
|
||||
return _PRODUCTION
|
@ -1,29 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
from ..shot_builder.asset import Asset
|
||||
from typing import *
|
||||
|
||||
|
||||
class RenderSettings:
|
||||
def __init__(self, width: int, height: int, frames_per_second: float):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.frames_per_second = frames_per_second
|
@ -1,64 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
class Shot:
|
||||
is_generic = False
|
||||
kitsu_id = ""
|
||||
sequence_code = ""
|
||||
name = ""
|
||||
code = ""
|
||||
frame_start = 0
|
||||
frames = 0
|
||||
# Frame_end will be stored for debugging only.
|
||||
frame_end = 0
|
||||
frames_per_second = 24.0
|
||||
file_path_format = "{production.path}/shots/{shot.sequence_code}/{shot.name}/{shot.name}.{task_type}.blend"
|
||||
file_path = ""
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
Check if this shot contains all data so it could be selected
|
||||
for shot building.
|
||||
|
||||
When not valid it won't be shown in the shot selection field.
|
||||
"""
|
||||
if not self.name:
|
||||
return False
|
||||
|
||||
if self.frames <= 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ShotRef:
|
||||
"""
|
||||
Reference to an asset from an external system.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "", code: str = ""):
|
||||
self.name = name
|
||||
self.code = code
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
pass
|
@ -1,64 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
import logging
|
||||
from typing import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemPathInclude:
|
||||
"""
|
||||
Resource class to temporary include system paths to `sys.paths`.
|
||||
|
||||
Usage:
|
||||
```
|
||||
paths = [pathlib.Path("/home/guest/my_python_scripts")]
|
||||
with SystemPathInclude(paths) as t:
|
||||
import my_module
|
||||
reload(my_module)
|
||||
```
|
||||
|
||||
It is possible to nest multiple SystemPathIncludes.
|
||||
"""
|
||||
|
||||
def __init__(self, paths_to_add: List[pathlib.Path]):
|
||||
# TODO: Check if all paths exist and are absolute.
|
||||
self.__paths = paths_to_add
|
||||
self.__original_sys_path: List[str] = []
|
||||
|
||||
def __enter__(self):
|
||||
self.__original_sys_path = sys.path
|
||||
new_sys_path = []
|
||||
for path_to_add in self.__paths:
|
||||
# Do not add paths that are already in the sys path.
|
||||
# Report this to the logger as this might indicate wrong usage.
|
||||
path_to_add_str = str(path_to_add)
|
||||
if path_to_add_str in self.__original_sys_path:
|
||||
logger.warn(f"{path_to_add_str} already added to `sys.path`")
|
||||
continue
|
||||
new_sys_path.append(path_to_add_str)
|
||||
new_sys_path.extend(self.__original_sys_path)
|
||||
sys.path = new_sys_path
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
sys.path = self.__original_sys_path
|
@ -1,27 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
|
||||
class TaskType:
|
||||
def __init__(self, task_name: str):
|
||||
self.name = task_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
@ -1,27 +1,31 @@
|
||||
# ##### 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 #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import bpy
|
||||
from typing import *
|
||||
from .operators import *
|
||||
from typing import Any
|
||||
|
||||
|
||||
def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
op = layout.operator(SHOTBUILDER_OT_NewShotFile.bl_idname, text="Shot File")
|
||||
op = layout.operator("kitsu.build_new_shot", text="Shot File")
|
||||
|
||||
|
||||
class KITSU_PT_new_shot_panel(bpy.types.Panel): # TODO Remove (for testing only)
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_label = "New Shot"
|
||||
bl_category = "New Shot"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("kitsu.build_new_shot")
|
||||
self.layout.operator("kitsu.save_shot_builder_hooks")
|
||||
|
||||
|
||||
classes = (KITSU_PT_new_shot_panel,)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
@ -1 +0,0 @@
|
||||
DEFAULT_FRAME_START: int = 101
|
@ -1,11 +0,0 @@
|
||||
from . import ops, ui
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
ui.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ops.unregister()
|
||||
ui.unregister()
|
@ -1,195 +0,0 @@
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import *
|
||||
|
||||
import typing
|
||||
import types
|
||||
from collections.abc import Iterable
|
||||
import importlib
|
||||
from .. import prefs
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Wildcard:
|
||||
pass
|
||||
|
||||
|
||||
class DoNotMatch:
|
||||
pass
|
||||
|
||||
|
||||
MatchCriteriaType = typing.Union[
|
||||
str, typing.List[str], typing.Type[Wildcard], typing.Type[DoNotMatch]
|
||||
]
|
||||
"""
|
||||
The MatchCriteriaType is a type definition for the parameters of the `hook` decorator.
|
||||
|
||||
The matching parameters can use multiple types to detect how the matching criteria
|
||||
would work.
|
||||
|
||||
* `str`: would perform an exact string match.
|
||||
* `typing.Iterator[str]`: would perform an exact string match with any of the given strings.
|
||||
* `typing.Type[Wildcard]`: would match any type for this parameter. This would be used so a hook
|
||||
is called for any value.
|
||||
* `typing.Type[DoNotMatch]`: would ignore this hook when matching the hook parameter. This is the default
|
||||
value for the matching criteria and would normally not be set directly in a
|
||||
production configuration.
|
||||
"""
|
||||
|
||||
MatchingRulesType = typing.Dict[str, MatchCriteriaType]
|
||||
"""
|
||||
Hooks are stored as `_shot_builder_rules' attribute on the function.
|
||||
The MatchingRulesType is the type definition of the `_shot_builder_rules` attribute.
|
||||
"""
|
||||
|
||||
HookFunction = typing.Callable[[typing.Any], None]
|
||||
|
||||
|
||||
def _match_hook_parameter(
|
||||
hook_criteria: MatchCriteriaType, match_query: typing.Optional[str]
|
||||
) -> bool:
|
||||
if hook_criteria == None:
|
||||
return True
|
||||
if hook_criteria == DoNotMatch:
|
||||
return match_query is None
|
||||
if hook_criteria == Wildcard:
|
||||
return True
|
||||
if isinstance(hook_criteria, str):
|
||||
return match_query == hook_criteria
|
||||
if isinstance(hook_criteria, list):
|
||||
return match_query in hook_criteria
|
||||
logger.error(f"Incorrect matching criteria {hook_criteria}, {match_query}")
|
||||
return False
|
||||
|
||||
|
||||
class Hooks:
|
||||
def __init__(self):
|
||||
self._hooks: typing.List[HookFunction] = []
|
||||
|
||||
def matches(
|
||||
self,
|
||||
hook: HookFunction,
|
||||
match_task_type: typing.Optional[str] = None,
|
||||
match_asset_type: typing.Optional[str] = None,
|
||||
**kwargs: typing.Optional[str],
|
||||
) -> bool:
|
||||
assert not kwargs
|
||||
rules = typing.cast(MatchingRulesType, getattr(hook, '_shot_builder_rules'))
|
||||
return all(
|
||||
(
|
||||
_match_hook_parameter(rules['match_task_type'], match_task_type),
|
||||
_match_hook_parameter(rules['match_asset_type'], match_asset_type),
|
||||
)
|
||||
)
|
||||
|
||||
def filter(self, **kwargs: typing.Optional[str]) -> typing.Iterator[HookFunction]:
|
||||
for hook in self._hooks:
|
||||
if self.matches(hook=hook, **kwargs):
|
||||
yield hook
|
||||
|
||||
def execute_hooks(
|
||||
self, match_task_type: str = None, match_asset_type: str = None, *args, **kwargs
|
||||
) -> None:
|
||||
for hook in self._hooks:
|
||||
if self.matches(
|
||||
hook, match_task_type=match_task_type, match_asset_type=match_asset_type
|
||||
):
|
||||
hook(*args, **kwargs)
|
||||
|
||||
def load_hooks(self, context):
|
||||
root_dir = prefs.project_root_dir_get(context)
|
||||
shot_builder_config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder")
|
||||
if not shot_builder_config_dir.exists():
|
||||
raise Exception("Shot Builder Hooks directory does not exist")
|
||||
paths = [
|
||||
shot_builder_config_dir.resolve().__str__() # TODO Make variable path
|
||||
] # TODO Set path to where hooks are stored
|
||||
with SystemPathInclude(paths) as _include:
|
||||
try:
|
||||
import hooks as production_hooks
|
||||
|
||||
importlib.reload(production_hooks)
|
||||
self.register_hooks(production_hooks)
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("Production has no `hooks.py` configuration file")
|
||||
|
||||
return False
|
||||
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def register_hooks(self, module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
self.register(module_item)
|
||||
|
||||
|
||||
def hook(
|
||||
match_task_type: MatchCriteriaType = None,
|
||||
match_asset_type: MatchCriteriaType = None,
|
||||
) -> typing.Callable[[types.FunctionType], types.FunctionType]:
|
||||
"""
|
||||
Decorator to add custom logic when building a shot.
|
||||
|
||||
Hooks are used to extend the configuration that would be not part of the core logic of the shot builder tool.
|
||||
"""
|
||||
rules = {
|
||||
'match_task_type': match_task_type,
|
||||
'match_asset_type': match_asset_type,
|
||||
}
|
||||
|
||||
def wrapper(func: types.FunctionType) -> types.FunctionType:
|
||||
setattr(func, '_shot_builder_rules', rules)
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SystemPathInclude:
|
||||
"""
|
||||
Resource class to temporary include system paths to `sys.paths`.
|
||||
|
||||
Usage:
|
||||
```
|
||||
paths = [pathlib.Path("/home/guest/my_python_scripts")]
|
||||
with SystemPathInclude(paths) as t:
|
||||
import my_module
|
||||
reload(my_module)
|
||||
```
|
||||
|
||||
It is possible to nest multiple SystemPathIncludes.
|
||||
"""
|
||||
|
||||
def __init__(self, paths_to_add: List[pathlib.Path]):
|
||||
# TODO: Check if all paths exist and are absolute.
|
||||
self.__paths = paths_to_add
|
||||
self.__original_sys_path: List[str] = []
|
||||
|
||||
def __enter__(self):
|
||||
self.__original_sys_path = sys.path
|
||||
new_sys_path = []
|
||||
for path_to_add in self.__paths:
|
||||
# Do not add paths that are already in the sys path.
|
||||
# Report this to the logger as this might indicate wrong usage.
|
||||
path_to_add_str = str(path_to_add)
|
||||
if path_to_add_str in self.__original_sys_path:
|
||||
logger.warn(f"{path_to_add_str} already added to `sys.path`")
|
||||
continue
|
||||
new_sys_path.append(path_to_add_str)
|
||||
new_sys_path.extend(self.__original_sys_path)
|
||||
sys.path = new_sys_path
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
sys.path = self.__original_sys_path
|
@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class KITSU_PT_new_shot_panel(bpy.types.Panel): # TODO Remove (for testing only)
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_label = "New Shot"
|
||||
bl_category = "New Shot"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("kitsu.build_new_shot")
|
||||
|
||||
|
||||
classes = (KITSU_PT_new_shot_panel,)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
Loading…
Reference in New Issue
Block a user