Move Anim_Setup module into Blender_Kitsu #5

Merged
Nick Alberelli merged 27 commits from :feature/merge_anim_setup_into_blender_kitsu into master 2023-04-05 17:38:41 +02:00
33 changed files with 697 additions and 753 deletions
Showing only changes of commit b2aeefcc50 - Show all commits

View File

@ -20,6 +20,7 @@
import bpy
from blender_kitsu import (
shot_builder,
lookdev,
bkglobals,
types,
@ -92,6 +93,7 @@ def register():
# tasks.register()
playblast.register()
anim.register()
shot_builder.register()
LoggerLevelManager.configure_levels()
logger.info("Registered blender-kitsu")
@ -109,6 +111,7 @@ def unregister():
prefs.unregister()
lookdev.unregister()
playblast.unregister()
shot_builder.unregister()
LoggerLevelManager.restore_levels()

View File

@ -285,6 +285,15 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
type=KITSU_media_update_search_paths
)
production_path: bpy.props.StringProperty( # type: ignore
name="Production Root",
description="The location to load configuration files from when "
"they couldn't be found in any parent folder of the current "
"file. Folder must contain a sub-folder named `shot-builder` "
"that holds the configuration files",
subtype='DIR_PATH',
)
session: Session = Session()
tasks: bpy.props.CollectionProperty(type=KITSU_task)

View File

@ -1,58 +1,56 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
from shot_builder.ui import *
from shot_builder.connectors.kitsu import *
from shot_builder.operators import *
from shot_builder.properties import *
import bpy
# import logging
# logging.basicConfig(level=logging.DEBUG)
bl_info = {
'name': 'Shot Builder',
"author": "Jeroen Bakker",
'version': (0, 1),
'blender': (2, 90, 0),
'location': 'Addon Preferences panel and file new menu',
'description': 'Shot builder production tool.',
'category': 'Studio',
}
classes = (
KitsuPreferences,
ShotBuilderPreferences,
SHOTBUILDER_OT_NewShotFile,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler)
def unregister():
bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler)
for cls in classes:
bpy.utils.unregister_class(cls)
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
from blender_kitsu.shot_builder.ui import *
from blender_kitsu.shot_builder.connectors.kitsu import *
from blender_kitsu.shot_builder.operators import *
import bpy
# import logging
# logging.basicConfig(level=logging.DEBUG)
# bl_info = {
# 'name': 'Shot Builder',
# "author": "Jeroen Bakker",
# 'version': (0, 1),
# 'blender': (2, 90, 0),
# 'location': 'Addon Preferences panel and file new menu',
# 'description': 'Shot builder production tool.',
# 'category': 'Studio',
# }
classes = (
KitsuPreferences,
SHOTBUILDER_OT_NewShotFile,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler)
def unregister():
bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler)
for cls in classes:
bpy.utils.unregister_class(cls)

View File

@ -1,13 +1,13 @@
from shot_builder.project import Production
from shot_builder.task_type import TaskType
from shot_builder.asset import Asset, AssetRef
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.builder.init_asset import InitAssetStep
from shot_builder.builder.init_shot import InitShotStep
from shot_builder.builder.set_render_settings import SetRenderSettingsStep
from shot_builder.builder.new_scene import NewSceneStep
from shot_builder.builder.invoke_hook import InvokeHookStep
from shot_builder.builder.save_file import SaveFileStep
from blender_kitsu.shot_builder.project import Production
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.asset import Asset, AssetRef
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.builder.init_asset import InitAssetStep
from blender_kitsu.shot_builder.builder.init_shot import InitShotStep
from blender_kitsu.shot_builder.builder.set_render_settings import SetRenderSettingsStep
from blender_kitsu.shot_builder.builder.new_scene import NewSceneStep
from blender_kitsu.shot_builder.builder.invoke_hook import InvokeHookStep
from blender_kitsu.shot_builder.builder.save_file import SaveFileStep
import bpy

View File

@ -1,11 +1,11 @@
import bpy
import typing
from shot_builder.project import Production
from shot_builder.shot import Shot
from shot_builder.task_type import TaskType
from shot_builder.render_settings import RenderSettings
from shot_builder.asset import Asset
from blender_kitsu.shot_builder.project import Production
from blender_kitsu.shot_builder.shot import Shot
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.asset import Asset
class BuildContext:

View File

@ -1,7 +1,7 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.asset import *
from shot_builder.project import *
from shot_builder.shot import *
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.asset import *
from blender_kitsu.shot_builder.project import *
from blender_kitsu.shot_builder.shot import *
import bpy

View File

@ -1,7 +1,7 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.asset import *
from shot_builder.project import *
from shot_builder.shot import *
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.asset import *
from blender_kitsu.shot_builder.project import *
from blender_kitsu.shot_builder.shot import *
import bpy

View File

@ -1,5 +1,5 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.hooks import HookFunction
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.hooks import HookFunction
import bpy
import typing

View File

@ -1,5 +1,5 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.render_settings import RenderSettings
import bpy
import logging

View File

@ -1,7 +1,7 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from shot_builder.asset import *
from shot_builder.project import *
from shot_builder.shot import *
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.asset import *
from blender_kitsu.shot_builder.project import *
from blender_kitsu.shot_builder.shot import *
import pathlib
import bpy

View File

@ -1,4 +1,4 @@
from shot_builder.builder.build_step import BuildStep, BuildContext
from blender_kitsu.shot_builder.builder.build_step import BuildStep, BuildContext
import bpy
import typing

View File

@ -21,16 +21,16 @@
This module contains the Connector class. It is an abstract base class for concrete connectors.
"""
from shot_builder.shot import Shot, ShotRef
from shot_builder.asset import Asset, AssetRef
from shot_builder.task_type import TaskType
from shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.shot import Shot, ShotRef
from blender_kitsu.shot_builder.asset import Asset, AssetRef
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.render_settings import RenderSettings
from typing import *
if TYPE_CHECKING:
from shot_builder.project import Production
from shot_builder.properties import ShotBuilderPreferences
from blender_kitsu.shot_builder.project import Production
from blender_kitsu.shot_builder.properties import ShotBuilderPreferences
class Connector:
@ -58,8 +58,8 @@ class Connector:
Example of using predefined connectors in a production config file:
```shot-builder/config.py
from shot_builder.connectors.default import DefaultConnector
from shot_builder.connectors.kitsu import KitsuConnector
from blender_kitsu.shot_builder.connectors.default import DefaultConnector
from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector
PRODUCTION_NAME = DefaultConnector
TASK_TYPES = KitsuConnector

View File

@ -17,11 +17,11 @@
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
from shot_builder.shot import Shot, ShotRef
from shot_builder.asset import Asset, AssetRef
from shot_builder.task_type import TaskType
from shot_builder.render_settings import RenderSettings
from shot_builder.connectors.connector import Connector
from blender_kitsu.shot_builder.shot import Shot, ShotRef
from blender_kitsu.shot_builder.asset import Asset, AssetRef
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.connectors.connector import Connector
from typing import *

View File

@ -18,13 +18,16 @@
# <pep8 compliant>
import bpy
from shot_builder import vars
from shot_builder.shot import Shot, ShotRef
from shot_builder.asset import Asset, AssetRef
from shot_builder.task_type import TaskType
from shot_builder.render_settings import RenderSettings
from shot_builder.connectors.connector import Connector
from blender_kitsu.shot_builder import vars
from blender_kitsu.shot_builder.shot import Shot, ShotRef
from blender_kitsu.shot_builder.asset import Asset, AssetRef
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.connectors.connector import Connector
import requests
from blender_kitsu import cache
from blender_kitsu.gazu.asset import all_assets_for_shot
from blender_kitsu.gazu.shot import all_shots_for_project, all_sequences_for_project
import typing
import logging
@ -131,47 +134,9 @@ class KitsuConnector(Connector):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__jwt_access_token = ""
self.__validate()
self.__authorize()
def __validate(self) -> None:
self._preferences.kitsu._validate()
if not self._production.config.get('KITSU_PROJECT_ID'):
raise KitsuException(
"KITSU_PROJECT_ID is not configured in config.py")
def __authorize(self) -> None:
kitsu_pref = self._preferences.kitsu
backend = kitsu_pref.backend
username = kitsu_pref.username
password = kitsu_pref.password
logger.info(f"authorize {username} against {backend}")
response = requests.post(
url=f"{backend}/auth/login", data={'email': username, 'password': password})
if response.status_code != 200:
self.__jwt_access_token = ""
raise KitsuException(
f"unable to authorize (status code={response.status_code})")
json_response = response.json()
self.__jwt_access_token = json_response['access_token']
def __api_get(self, api: str) -> typing.Any:
kitsu_pref = self._preferences.kitsu
backend = kitsu_pref.backend
response = requests.get(url=f"{backend}{api}", headers={
"Authorization": f"Bearer {self.__jwt_access_token}"
})
if response.status_code != 200:
raise KitsuException(
f"unable to call kitsu (api={api}, status code={response.status_code})")
return response.json()
def __get_production_data(self) -> KitsuProject:
project_id = self._production.config['KITSU_PROJECT_ID']
production = self.__api_get(f"data/projects/{project_id}")
production = cache.project_active_get()
project = KitsuProject(typing.cast(
typing.Dict[str, typing.Any], production))
return project
@ -181,22 +146,23 @@ class KitsuConnector(Connector):
return production.get_name()
def get_task_types(self) -> typing.List[TaskType]:
task_types = self.__api_get(f"data/task_types/")
project = cache.project_active_get()
task_types = project.task_types
import pprint
pprint.pprint(task_types)
return []
def get_shots(self) -> typing.List[ShotRef]:
project_id = self._production.config['KITSU_PROJECT_ID']
kitsu_sequences = self.__api_get(f"data/projects/{project_id}/sequences")
project = cache.project_active_get()
kitsu_sequences = all_sequences_for_project(project.id)
sequence_lookup = {sequence_data['id']: KitsuSequenceRef(
kitsu_id=sequence_data['id'],
name=sequence_data['name'],
code=sequence_data['code'],
) for sequence_data in kitsu_sequences}
kitsu_shots = self.__api_get(f"data/projects/{project_id}/shots")
kitsu_shots = all_shots_for_project(project.id)
shots: typing.List[ShotRef] = []
for shot_data in kitsu_shots:
@ -232,8 +198,8 @@ class KitsuConnector(Connector):
return shots
def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]:
kitsu_assets = self.__api_get(
f"data/shots/{shot.kitsu_id}/assets")
kitsu_assets = all_assets_for_shot(shot.kitsu_id)
return [AssetRef(name=asset_data['name'], code=asset_data['code'])
for asset_data in kitsu_assets]
@ -241,7 +207,5 @@ class KitsuConnector(Connector):
"""
Retrieve the render settings for the given shot.
"""
kitsu_project = self.__get_production_data()
resolution = kitsu_project.get_resolution()
frames_per_second = shot.frames_per_second
return RenderSettings(width=resolution[0], height=resolution[1], frames_per_second=frames_per_second)
project = cache.project_active_get()
return RenderSettings(width=int(project.resolution.split('x')[0]), height=int(project.resolution.split('x')[1]), frames_per_second=project.fps)

View File

@ -1,233 +1,233 @@
# Project Description (DRAFT)
Shot Builder is an Add-on that helps studios to work with task specific
Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are
* Build blend files for a specific task and shot.
* Sync data back from work files to places like kitsu, or `edit.blend`.
## Design Principles
The main design principles are:
* The core-tool can be installed as an add-on, but the (production specific)
configuration should be part of the production repository.
* The configuration files are a collection of python files. The API between
the configuration files and the add-on should be easy to use as pipeline
TDs working on the production should be able to work with it.
* TDs/artists should be able to handle issues during building without looking
at how the add-on is structured.
* The tool contains connectors that can be configured to read/write data
from the system/file that is the main location of the data. For example
The start and end time of a shot could be stored in an external production tracking application.
## Connectors
Connectors are components that can be used to read or write to files or
systems. The connectors will add flexibility to the add-on so it could be used
in multiple productions or studios.
In the configuration files the TD can setup the connectors that are used for
the production. Possible connectors would be:
* Connector for text based config files (json/yaml).
* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html).
* Connector for blend files.
## Layering & Hooks
The configuration of the tool is layered. When building a work file for a sequence
there are multiple ways to change the configuration.
* Configuration for the production.
* Configuration for the asset that is needed.
* Configuration for the asset type of the loaded asset.
* Configuration for the sequence.
* Configuration for the shot.
* Configuration for the task type.
For any combination of these configurations hooks can be defined.
```
@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A')
def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides when Spring is loaded in 02_020A.
"""
@shot_tools.hook(match_task_type='anim')
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides for any animation task.
"""
```
### Data
All hooks must have Pythons `**kwargs` parameter. The `kwargs` contains
the context at the moment the hook is invoked. The context can contain the
following items.
* `production`: `shot_tools.Production`: Include the name of the production
and the location on the filesystem.
* `task`: `shot_tools.Task`: The task (combination of task_type and shot)
* `task_type`: `shot_tools.TaskType`: Is part of the `task`.
* `sequence`: `shot_tools.Sequence`: Is part of `shot`.
* `shot`: `shot_tools.Shot` Is part of `task`.
* `asset`: `shot_tools.Asset`: Only available during asset loading phase.
* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase.
### Execution Order
The add-on will internally create a list containing the hooks that needs to be
executed for the command in a sensible order. It will then execute them in that
order.
By default the next order will be used:
* Production wide hooks
* Asset Type hooks
* Asset hooks
* Sequence hooks
* Shot hooks
* Task type hooks
A hook with a single match rule will be run in the corresponding phase. A hook with
multiple match rules will be run in the last matching phase. For example, a hook with
asset and task type match rules will be run in the task type phase.
#### Events
Order of execution can be customized by adding the optional `run_before`
or `run_after` parameters.
```
@shot_tools.hook(match_task_type='anim',
requires={shot_tools.events.AssetsLoaded, hook_task_other_anim},
is_required_by={shot_tools.events.ShotOverrides})
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides for any animation task run after all assets have been loaded.
"""
```
Events could be:
* `shot_tools.events.BuildStart`
* `shot_tools.events.ProductionSettingsLoaded`
* `shot_tools.events.AssetsLoaded`
* `shot_tools.events.AssetTypeOverrides`
* `shot_tools.events.SequenceOverrides`
* `shot_tools.events.ShotOverrides`
* `shot_tools.events.TaskTypeOverrides`
* `shot_tools.events.BuildFinished`
* `shot_tools.events.HookStart`
* `shot_tools.events.HookEnd`
During usage we should see which one of these or other events are needed.
`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded`
and `shot_tools.events.HookStart` can only be used in the `run_after`
parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished`
can only be used in the `run_before` parameter.
## API
The shot builder has an API between the add-on and the configuration files. This
API contains convenience functions and classes to hide complexity and makes
sure that the configuration files are easy to maintain.
```
register_task_type(task_type="anim")
register_task_type(task_type="lighting")
```
```
# shot_tool/characters.py
class Asset(shot_tool.some_module.Asset):
asset_file = "/{asset_type}/{name}/{name}.blend"
collection = “{class_name}”
name = “{class_name}”
class Character(Asset):
asset_type = char
class Ellie(Character):
collection = “{class_name}-{variant_name}”
variants = {default, short_hair}
class Victoria(Character): pass
class Rex(Character): pass
# shot_tool/shots.py
class Shot_01_020_A(shot_tool.some_module.Shot):
shot_id = 01_020_A
assets = {
characters.Ellie(variant=”short_hair”),
characters.Rex,
sets.LogOverChasm,
}
class AllHumansShot(shot_tool.some_module.Shot):
assets = {
characters.Ellie(variant=”short_hair”),
characters.Rex,
characters.Victoria,
}
class Shot_01_035_A(AllHumansShot):
assets = {
sets.Camp,
}
```
This API is structured/implemented in a way that it keeps track of what
is being done. This will be used when an error occurs so a descriptive
error message can be generated that would help the TD to solve the issue more
quickly. The goal would be that the error messages are descriptive enough to
direct the TD into the direction where the actual cause is. And when possible
propose several solutions to fix it.
## Setting up the tool
The artist/TD can configure their current local project directory in the add-on preferences.
This can then be used for new blend files. The project associated with an opened (so existing)
blend file can be found automatically by iterating over parent directories until a Shot Builder
configuration file is found. Project-specific settings are not configured/stored in the add-on,
but in this configuration file.
The add-on will look in the root of the production repository to locate the
main configuration file `shot-builder/config.py`. This file contains general
settings about the production, including:
* The name of the production for reporting back to the user when needed.
* Naming standards to test against when reporting deviations.
* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production.
* Configuration of the needed connectors.
## Usage
Any artist can open a shot file via the `File` menu. A modal panel appears
where the user can select the task type and sequence/shot. When the file
already exists, it will be opened. When the file doesn't exist, the file
will be built.
In the future other use cases will also be accessible, such as:
* Syncing data back from a work file to the source of the data.
* Report of errors/differences between the shot file and the configuration.
## Open Issues
### Security
* Security keys needed by connectors need to be stored somewhere. The easy
place is to place inside the production repository, but that isn't secure
Anyone with access to the repository could misuse the keys to access the
connector. Other solution might be to use the OS key store or retrieve the
keys from an online service authenticated by the blender cloud add-on.
We could use `keyring` to access OS key stores.
# Project Description (DRAFT)
Shot Builder is an Add-on that helps studios to work with task specific
Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are
* Build blend files for a specific task and shot.
* Sync data back from work files to places like kitsu, or `edit.blend`.
## Design Principles
The main design principles are:
* The core-tool can be installed as an add-on, but the (production specific)
configuration should be part of the production repository.
* The configuration files are a collection of python files. The API between
the configuration files and the add-on should be easy to use as pipeline
TDs working on the production should be able to work with it.
* TDs/artists should be able to handle issues during building without looking
at how the add-on is structured.
* The tool contains connectors that can be configured to read/write data
from the system/file that is the main location of the data. For example
The start and end time of a shot could be stored in an external production tracking application.
## Connectors
Connectors are components that can be used to read or write to files or
systems. The connectors will add flexibility to the add-on so it could be used
in multiple productions or studios.
In the configuration files the TD can setup the connectors that are used for
the production. Possible connectors would be:
* Connector for text based config files (json/yaml).
* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html).
* Connector for blend files.
## Layering & Hooks
The configuration of the tool is layered. When building a work file for a sequence
there are multiple ways to change the configuration.
* Configuration for the production.
* Configuration for the asset that is needed.
* Configuration for the asset type of the loaded asset.
* Configuration for the sequence.
* Configuration for the shot.
* Configuration for the task type.
For any combination of these configurations hooks can be defined.
```
@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A')
def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides when Spring is loaded in 02_020A.
"""
@shot_tools.hook(match_task_type='anim')
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides for any animation task.
"""
```
### Data
All hooks must have Pythons `**kwargs` parameter. The `kwargs` contains
the context at the moment the hook is invoked. The context can contain the
following items.
* `production`: `shot_tools.Production`: Include the name of the production
and the location on the filesystem.
* `task`: `shot_tools.Task`: The task (combination of task_type and shot)
* `task_type`: `shot_tools.TaskType`: Is part of the `task`.
* `sequence`: `shot_tools.Sequence`: Is part of `shot`.
* `shot`: `shot_tools.Shot` Is part of `task`.
* `asset`: `shot_tools.Asset`: Only available during asset loading phase.
* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase.
### Execution Order
The add-on will internally create a list containing the hooks that needs to be
executed for the command in a sensible order. It will then execute them in that
order.
By default the next order will be used:
* Production wide hooks
* Asset Type hooks
* Asset hooks
* Sequence hooks
* Shot hooks
* Task type hooks
A hook with a single match rule will be run in the corresponding phase. A hook with
multiple match rules will be run in the last matching phase. For example, a hook with
asset and task type match rules will be run in the task type phase.
#### Events
Order of execution can be customized by adding the optional `run_before`
or `run_after` parameters.
```
@shot_tools.hook(match_task_type='anim',
requires={shot_tools.events.AssetsLoaded, hook_task_other_anim},
is_required_by={shot_tools.events.ShotOverrides})
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
"""
Specific overrides for any animation task run after all assets have been loaded.
"""
```
Events could be:
* `shot_tools.events.BuildStart`
* `shot_tools.events.ProductionSettingsLoaded`
* `shot_tools.events.AssetsLoaded`
* `shot_tools.events.AssetTypeOverrides`
* `shot_tools.events.SequenceOverrides`
* `shot_tools.events.ShotOverrides`
* `shot_tools.events.TaskTypeOverrides`
* `shot_tools.events.BuildFinished`
* `shot_tools.events.HookStart`
* `shot_tools.events.HookEnd`
During usage we should see which one of these or other events are needed.
`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded`
and `shot_tools.events.HookStart` can only be used in the `run_after`
parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished`
can only be used in the `run_before` parameter.
## API
The shot builder has an API between the add-on and the configuration files. This
API contains convenience functions and classes to hide complexity and makes
sure that the configuration files are easy to maintain.
```
register_task_type(task_type="anim")
register_task_type(task_type="lighting")
```
```
# shot_tool/characters.py
class Asset(shot_tool.some_module.Asset):
asset_file = "/{asset_type}/{name}/{name}.blend"
collection = “{class_name}”
name = “{class_name}”
class Character(Asset):
asset_type = char
class Ellie(Character):
collection = “{class_name}-{variant_name}”
variants = {default, short_hair}
class Victoria(Character): pass
class Rex(Character): pass
# shot_tool/shots.py
class Shot_01_020_A(shot_tool.some_module.Shot):
shot_id = 01_020_A
assets = {
characters.Ellie(variant=”short_hair”),
characters.Rex,
sets.LogOverChasm,
}
class AllHumansShot(shot_tool.some_module.Shot):
assets = {
characters.Ellie(variant=”short_hair”),
characters.Rex,
characters.Victoria,
}
class Shot_01_035_A(AllHumansShot):
assets = {
sets.Camp,
}
```
This API is structured/implemented in a way that it keeps track of what
is being done. This will be used when an error occurs so a descriptive
error message can be generated that would help the TD to solve the issue more
quickly. The goal would be that the error messages are descriptive enough to
direct the TD into the direction where the actual cause is. And when possible
propose several solutions to fix it.
## Setting up the tool
The artist/TD can configure their current local project directory in the add-on preferences.
This can then be used for new blend files. The project associated with an opened (so existing)
blend file can be found automatically by iterating over parent directories until a Shot Builder
configuration file is found. Project-specific settings are not configured/stored in the add-on,
but in this configuration file.
The add-on will look in the root of the production repository to locate the
main configuration file `shot-builder/config.py`. This file contains general
settings about the production, including:
* The name of the production for reporting back to the user when needed.
* Naming standards to test against when reporting deviations.
* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production.
* Configuration of the needed connectors.
## Usage
Any artist can open a shot file via the `File` menu. A modal panel appears
where the user can select the task type and sequence/shot. When the file
already exists, it will be opened. When the file doesn't exist, the file
will be built.
In the future other use cases will also be accessible, such as:
* Syncing data back from a work file to the source of the data.
* Report of errors/differences between the shot file and the configuration.
## Open Issues
### Security
* Security keys needed by connectors need to be stored somewhere. The easy
place is to place inside the production repository, but that isn't secure
Anyone with access to the repository could misuse the keys to access the
connector. Other solution might be to use the OS key store or retrieve the
keys from an online service authenticated by the blender cloud add-on.
We could use `keyring` to access OS key stores.

View File

@ -1,4 +1,4 @@
# Example configuration files
This folder contains an example shot builder configuration. It shows the part
that a TD would do to incorporate the shot builder in a production.
# Example configuration files
This folder contains an example shot builder configuration. It shows the part
that a TD would do to incorporate the shot builder in a production.

View File

@ -1,83 +1,83 @@
from shot_builder.asset import Asset
class SpriteFrightAsset(Asset):
path = "{production.path}/lib/{asset.asset_type}/{asset.code}/{asset.code}.blend"
class Character(SpriteFrightAsset):
asset_type = "char"
collection = "CH-{asset.code}"
class Ellie(Character):
name = "Ellie"
code = "ellie"
class Victoria(Character):
name = "Victoria"
code = "victoria"
class Phil(Character):
name = "Phil"
code = "phil"
class Rex(Character):
name = "Rex"
code = "rex"
class Jay(Character):
name = "Jay"
code = "jay"
# TODO: Bird character has no asset file yet.
# class Bird(Character):
# name = "Bird"
# code = "bird"
class Prop(SpriteFrightAsset):
asset_type = "props"
collection = "PR-{asset.code}"
class Boombox(Prop):
name = "Boombox"
code = "boombox"
class BBQGrill(Prop):
name = "BBQ Grill"
code = "bbq_grill"
# NOTE: NotepadAndPencil is a combined asset. In Kitsu it is defined as a single asset. In the production
# reportitory it is stored as 2 collections in a single file. See `hooks.link_char_prop_for_anim`
# where this is handled.
class NotepadAndPencil(Prop):
name = "Notepad and pencil"
code = "notepad_pencil"
class Binoculars(Prop):
name = "Binoculars (Ellie)"
code = "binoculars"
class Backpack(Prop):
name = "Backpack (Phil)"
code = "backpack"
class Set(SpriteFrightAsset):
asset_type = "sets"
collection = "SE-{asset.code}"
class MushroomGrove(Set):
name = "Mushroom grove"
code = "mushroom_grove"
from blender_kitsu.shot_builder.asset import Asset
class SpriteFrightAsset(Asset):
path = "{production.path}/lib/{asset.asset_type}/{asset.code}/{asset.code}.blend"
class Character(SpriteFrightAsset):
asset_type = "char"
collection = "CH-{asset.code}"
class Ellie(Character):
name = "Ellie"
code = "ellie"
class Victoria(Character):
name = "Victoria"
code = "victoria"
class Phil(Character):
name = "Phil"
code = "phil"
class Rex(Character):
name = "Rex"
code = "rex"
class Jay(Character):
name = "Jay"
code = "jay"
# TODO: Bird character has no asset file yet.
# class Bird(Character):
# name = "Bird"
# code = "bird"
class Prop(SpriteFrightAsset):
asset_type = "props"
collection = "PR-{asset.code}"
class Boombox(Prop):
name = "Boombox"
code = "boombox"
class BBQGrill(Prop):
name = "BBQ Grill"
code = "bbq_grill"
# NOTE: NotepadAndPencil is a combined asset. In Kitsu it is defined as a single asset. In the production
# reportitory it is stored as 2 collections in a single file. See `hooks.link_char_prop_for_anim`
# where this is handled.
class NotepadAndPencil(Prop):
name = "Notepad and pencil"
code = "notepad_pencil"
class Binoculars(Prop):
name = "Binoculars (Ellie)"
code = "binoculars"
class Backpack(Prop):
name = "Backpack (Phil)"
code = "backpack"
class Set(SpriteFrightAsset):
asset_type = "sets"
collection = "SE-{asset.code}"
class MushroomGrove(Set):
name = "Mushroom grove"
code = "mushroom_grove"

View File

@ -1,14 +1,14 @@
from shot_builder.connectors.kitsu import KitsuConnector
PRODUCTION_NAME = KitsuConnector
SHOTS = KitsuConnector
ASSETS = KitsuConnector
RENDER_SETTINGS = KitsuConnector
KITSU_PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec"
# Formatting rules
# ----------------
# The name of the scene in blender where the shot is build in.
# SCENE_NAME_FORMAT = "{shot.sequence_code}_{shot.code}.{task_type}"
from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector
PRODUCTION_NAME = KitsuConnector
SHOTS = KitsuConnector
ASSETS = KitsuConnector
RENDER_SETTINGS = KitsuConnector
KITSU_PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec"
# Formatting rules
# ----------------
# The name of the scene in blender where the shot is build in.
# SCENE_NAME_FORMAT = "{shot.sequence_code}_{shot.code}.{task_type}"

View File

@ -1,151 +1,151 @@
import bpy
from shot_builder.hooks import hook, Wildcard
from shot_builder.asset import Asset
from shot_builder.shot import Shot
from shot_builder.project import Production
import logging
logger = logging.getLogger(__name__)
# ---------- Global Hook ----------
@hook()
def set_cycles_render_engine(scene: bpy.types.Scene, **kwargs):
"""
By default we set Cycles as the renderer.
"""
scene.render.engine = 'CYCLES'
# ---------- Overrides for animation files ----------
@hook(match_task_type='anim')
def task_type_anim_set_workbench(scene: bpy.types.Scene, **kwargs):
"""
Override of the render engine to Workbench when building animation files.
"""
scene.render.engine = 'BLENDER_WORKBENCH'
# ---------- Create output collection for animation files ----------
def _add_camera_rig(
scene: bpy.types.Scene,
production: Production,
shot: Shot,
):
"""
Function to load the camera rig. The rig will be added to the output collection
of the shot and the camera will be set as active camera.
"""
# Load camera rig.
path = f"{production.path}/lib/cam/camera_rig.blend"
collection_name = "CA-camera_rig"
bpy.ops.wm.link(
filepath=path,
directory=path + "/Collection",
filename=collection_name,
)
# Keep the active object name as this would also be the name of the collection after enabling library override.
active_object_name = bpy.context.active_object.name
# Make library override.
bpy.ops.object.make_override_library()
# Add camera collection to the output collection
asset_collection = bpy.data.collections[active_object_name]
shot.output_collection.children.link(asset_collection)
# Set the camera of the camera rig as active scene camera.
camera = bpy.data.objects['CAM-camera']
scene.camera = camera
@hook(match_task_type='anim')
def task_type_anim_output_collection(scene: bpy.types.Scene, production: Production, shot: Shot, task_type: str, **kwargs):
"""
Animations are stored in an output collection. This collection will be linked
by the lighting file.
Also loads the camera rig.
"""
output_collection = bpy.data.collections.new(
name=shot.get_output_collection_name(shot=shot, task_type=task_type))
shot.output_collection = output_collection
output_collection.use_fake_user = True
_add_camera_rig(scene, production, shot)
@hook(match_task_type='lighting')
def link_anim_output_collection(scene: bpy.types.Scene, production: Production, shot: Shot, **kwargs):
"""
Link in the animation output collection from the animation file.
"""
anim_collection = bpy.data.collections.new(name="animation")
scene.collection.children.link(anim_collection)
anim_file_path = shot.get_anim_file_path(production, shot)
anim_output_collection_name = shot.get_output_collection_name(
shot=shot, task_type="anim")
result = bpy.ops.wm.link(
filepath=anim_file_path,
directory=anim_file_path + "/Collection",
filename=anim_output_collection_name,
)
assert (result == {'FINISHED'})
# Move the anim output collection from scene collection to the animation collection.
anim_output_collection = bpy.data.objects[anim_output_collection_name]
anim_collection.objects.link(anim_output_collection)
scene.collection.objects.unlink(anim_output_collection)
# Use animation camera as active scene camera.
camera = bpy.data.objects['CAM-camera']
scene.camera = camera
# ---------- Asset loading and linking ----------
@hook(match_task_type='anim', match_asset_type=['char', 'props'])
def link_char_prop_for_anim(scene: bpy.types.Scene, shot: Shot, asset: Asset, **kwargs):
"""
Loading a character or prop for an animation file.
"""
collection_names = []
if asset.code == 'notepad_pencil':
collection_names.append("PR-pencil")
collection_names.append("PR-notepad")
else:
collection_names.append(asset.collection)
for collection_name in collection_names:
logger.info("link asset")
bpy.ops.wm.link(
filepath=str(asset.path),
directory=str(asset.path) + "/Collection",
filename=collection_name,
)
# Keep the active object name as this would also be the name of the collection after enabling library override.
active_object_name = bpy.context.active_object.name
# Make library override.
bpy.ops.object.make_override_library()
# Add overridden collection to the output collection.
asset_collection = bpy.data.collections[active_object_name]
shot.output_collection.children.link(asset_collection)
@hook(match_task_type=Wildcard, match_asset_type='sets')
def link_set(asset: Asset, **kwargs):
"""
Load the set of the shot.
"""
bpy.ops.wm.link(
filepath=str(asset.path),
directory=str(asset.path) + "/Collection",
filename=asset.collection,
)
import bpy
from blender_kitsu.shot_builder.hooks import hook, Wildcard
from blender_kitsu.shot_builder.asset import Asset
from blender_kitsu.shot_builder.shot import Shot
from blender_kitsu.shot_builder.project import Production
import logging
logger = logging.getLogger(__name__)
# ---------- Global Hook ----------
@hook()
def set_cycles_render_engine(scene: bpy.types.Scene, **kwargs):
"""
By default we set Cycles as the renderer.
"""
scene.render.engine = 'CYCLES'
# ---------- Overrides for animation files ----------
@hook(match_task_type='anim')
def task_type_anim_set_workbench(scene: bpy.types.Scene, **kwargs):
"""
Override of the render engine to Workbench when building animation files.
"""
scene.render.engine = 'BLENDER_WORKBENCH'
# ---------- Create output collection for animation files ----------
def _add_camera_rig(
scene: bpy.types.Scene,
production: Production,
shot: Shot,
):
"""
Function to load the camera rig. The rig will be added to the output collection
of the shot and the camera will be set as active camera.
"""
# Load camera rig.
path = f"{production.path}/lib/cam/camera_rig.blend"
collection_name = "CA-camera_rig"
bpy.ops.wm.link(
filepath=path,
directory=path + "/Collection",
filename=collection_name,
)
# Keep the active object name as this would also be the name of the collection after enabling library override.
active_object_name = bpy.context.active_object.name
# Make library override.
bpy.ops.object.make_override_library()
# Add camera collection to the output collection
asset_collection = bpy.data.collections[active_object_name]
shot.output_collection.children.link(asset_collection)
# Set the camera of the camera rig as active scene camera.
camera = bpy.data.objects['CAM-camera']
scene.camera = camera
@hook(match_task_type='anim')
def task_type_anim_output_collection(scene: bpy.types.Scene, production: Production, shot: Shot, task_type: str, **kwargs):
"""
Animations are stored in an output collection. This collection will be linked
by the lighting file.
Also loads the camera rig.
"""
output_collection = bpy.data.collections.new(
name=shot.get_output_collection_name(shot=shot, task_type=task_type))
shot.output_collection = output_collection
output_collection.use_fake_user = True
_add_camera_rig(scene, production, shot)
@hook(match_task_type='lighting')
def link_anim_output_collection(scene: bpy.types.Scene, production: Production, shot: Shot, **kwargs):
"""
Link in the animation output collection from the animation file.
"""
anim_collection = bpy.data.collections.new(name="animation")
scene.collection.children.link(anim_collection)
anim_file_path = shot.get_anim_file_path(production, shot)
anim_output_collection_name = shot.get_output_collection_name(
shot=shot, task_type="anim")
result = bpy.ops.wm.link(
filepath=anim_file_path,
directory=anim_file_path + "/Collection",
filename=anim_output_collection_name,
)
assert (result == {'FINISHED'})
# Move the anim output collection from scene collection to the animation collection.
anim_output_collection = bpy.data.objects[anim_output_collection_name]
anim_collection.objects.link(anim_output_collection)
scene.collection.objects.unlink(anim_output_collection)
# Use animation camera as active scene camera.
camera = bpy.data.objects['CAM-camera']
scene.camera = camera
# ---------- Asset loading and linking ----------
@hook(match_task_type='anim', match_asset_type=['char', 'props'])
def link_char_prop_for_anim(scene: bpy.types.Scene, shot: Shot, asset: Asset, **kwargs):
"""
Loading a character or prop for an animation file.
"""
collection_names = []
if asset.code == 'notepad_pencil':
collection_names.append("PR-pencil")
collection_names.append("PR-notepad")
else:
collection_names.append(asset.collection)
for collection_name in collection_names:
logger.info("link asset")
bpy.ops.wm.link(
filepath=str(asset.path),
directory=str(asset.path) + "/Collection",
filename=collection_name,
)
# Keep the active object name as this would also be the name of the collection after enabling library override.
active_object_name = bpy.context.active_object.name
# Make library override.
bpy.ops.object.make_override_library()
# Add overridden collection to the output collection.
asset_collection = bpy.data.collections[active_object_name]
shot.output_collection.children.link(asset_collection)
@hook(match_task_type=Wildcard, match_asset_type='sets')
def link_set(asset: Asset, **kwargs):
"""
Load the set of the shot.
"""
bpy.ops.wm.link(
filepath=str(asset.path),
directory=str(asset.path) + "/Collection",
filename=asset.collection,
)

View File

@ -0,0 +1,6 @@
# Example configuration files
This folder contains an example shot builder configuration. It shows the part
that a TD would do to incorporate the shot builder in a production.

View File

@ -1,29 +1,29 @@
from shot_builder.shot import Shot
from shot_builder.project import Production
class SpriteFrightShot(Shot):
def get_anim_file_path(self, production: Production, shot: Shot) -> str:
"""
Get the animation file path for this given shot.
"""
return self.file_path_format.format_map({
'production': production,
'shot': shot,
'task_type': "anim"
})
def get_output_collection_name(self, shot: Shot, task_type: str) -> str:
"""
Get the collection name where the output is stored.
"""
return f"{shot.sequence_code}_{shot.code}.{task_type}.output"
class Sequence_0002(SpriteFrightShot):
sequence_code = "0002"
class Shot_0001_0001_A(Sequence_0002):
name = "001"
code = "0001"
from blender_kitsu.shot_builder.shot import Shot
from blender_kitsu.shot_builder.project import Production
class SpriteFrightShot(Shot):
def get_anim_file_path(self, production: Production, shot: Shot) -> str:
"""
Get the animation file path for this given shot.
"""
return self.file_path_format.format_map({
'production': production,
'shot': shot,
'task_type': "anim"
})
def get_output_collection_name(self, shot: Shot, task_type: str) -> str:
"""
Get the collection name where the output is stored.
"""
return f"{shot.sequence_code}_{shot.code}.{task_type}.output"
class Sequence_0002(SpriteFrightShot):
sequence_code = "0002"
class Shot_0001_0001_A(Sequence_0002):
name = "001"
code = "0001"

View File

@ -78,7 +78,7 @@ class Hooks:
def _register_hook(func: types.FunctionType) -> None:
from shot_builder.project import get_active_production
from blender_kitsu.shot_builder.project import get_active_production
production = get_active_production()
production.hooks.register(func)

View File

@ -19,10 +19,11 @@
# <pep8 compliant>
from typing import *
import bpy
from shot_builder.shot import ShotRef
from shot_builder.project import *
from shot_builder.builder import ShotBuilder
from shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.shot import ShotRef
from blender_kitsu.shot_builder.project import ensure_loaded_production, get_active_production
from blender_kitsu.shot_builder.builder import ShotBuilder
from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu import prefs, cache
_production_task_type_items: List[Tuple[str, str, str]] = []
@ -103,18 +104,34 @@ class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator):
)
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(bpy.context)
project = cache.project_active_get()
production_root = get_production_root(context)
if production_root is None:
if addon_prefs.session.is_auth() is False:
self.report(
{'WARNING'}, "Operator is cancelled due to inability to determine the production path. Make sure the a default path in configured in the preferences.")
{'ERROR'}, "Must be logged into Kitsu to continue. Check login status in 'Blender Kitsu' addon preferences.")
return {'CANCELLED'}
if project.id == "":
self.report(
{'ERROR'}, "Operator is not able to determine the Kitsu production's name. Check project is selected in 'Blender Kitsu' addon preferences.")
return {'CANCELLED'}
if not addon_prefs.is_project_root_valid:
self.report(
{'ERROR'}, "Operator is not able to determine the project root directory. Check project root directiory is configured in 'Blender Kitsu' addon preferences.")
return {'CANCELLED'}
self.production_root = addon_prefs.project_root_dir
self.production_name = project.name
ensure_loaded_production(context)
production = get_active_production()
self.production_root = str(production.path)
self.production_name = production.get_name(context=context)
self.production_root = addon_prefs.project_root_dir
self.production_name = project.name
global _production_task_type_items
_production_task_type_items = production.get_task_type_items(

View File

@ -23,15 +23,18 @@ from collections import defaultdict
import bpy
from shot_builder.task_type import *
from shot_builder.shot import Shot, ShotRef
from shot_builder.render_settings import RenderSettings
from shot_builder.asset import Asset, AssetRef
from shot_builder.sys_utils import *
from shot_builder.hooks import Hooks, register_hooks
from blender_kitsu.shot_builder.task_type import *
from blender_kitsu.shot_builder.shot import Shot, ShotRef
from blender_kitsu.shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.asset import Asset, AssetRef
from blender_kitsu.shot_builder.sys_utils import *
from blender_kitsu.shot_builder.hooks import Hooks, register_hooks
from shot_builder.connectors.default import DefaultConnector
from shot_builder.connectors.connector import Connector
from blender_kitsu.shot_builder.connectors.default import DefaultConnector
from blender_kitsu.shot_builder.connectors.connector import Connector
from blender_kitsu import prefs
from pathlib import Path
from typing import *
import types
@ -77,7 +80,7 @@ class Production:
connector_cls: Type[Connector],
context: bpy.types.Context) -> Connector:
# TODO: Cache connector
preferences = context.preferences.addons[__package__].preferences
preferences = context.preferences.addons["blender_kitsu"].preferences
return connector_cls(production=self, preferences=preferences)
def __format_shot_name(self, shot: Shot) -> str:
@ -382,8 +385,9 @@ def get_production_root(context: bpy.types.Context) -> Optional[pathlib.Path]:
production_root = _find_production_root(current_file)
if production_root:
return production_root
production_root = pathlib.Path(
context.preferences.addons[__package__].preferences.production_path)
addon_prefs = prefs.addon_prefs_get(bpy.context)
production_root = Path(addon_prefs.project_root_dir)
if is_valid_production_root(production_root):
return production_root
return None

View File

@ -18,7 +18,7 @@
# <pep8 compliant>
from shot_builder.asset import Asset
from blender_kitsu.shot_builder.asset import Asset
from typing import *

View File

@ -19,7 +19,7 @@
# <pep8 compliant>
import bpy
from typing import *
from shot_builder.operators import *
from blender_kitsu.shot_builder.operators import *
def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:

View File

@ -1,57 +0,0 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import bpy
import pathlib
from shot_builder.project import is_valid_production_root
from shot_builder.connectors.kitsu import KitsuPreferences
class ShotBuilderPreferences(bpy.types.AddonPreferences):
bl_idname = __package__
production_path: bpy.props.StringProperty( # type: ignore
name="Production Root",
description="The location to load configuration files from when "
"they couldn't be found in any parent folder of the current "
"file. Folder must contain a sub-folder named `shot-builder` "
"that holds the configuration files",
subtype='DIR_PATH',
)
kitsu: bpy.props.PointerProperty( # type: ignore
name="Kitsu Preferences",
type=KitsuPreferences
)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
is_valid = is_valid_production_root(pathlib.Path(self.production_path))
layout.prop(self, "production_path",
icon='NONE' if is_valid else 'ERROR')
if not is_valid:
layout.label(text="Folder must contain a sub-folder named "
"`shot-builder` that holds the configuration "
"files.",
icon="ERROR")
sublayout = layout.box()
self.kitsu.draw(sublayout, context)