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
11 changed files with 0 additions and 2064 deletions
Showing only changes of commit 6a26d54e84 - Show all commits

112
anim_setup/.gitignore vendored
View File

@ -1,112 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
.venv*
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# IDE settings
.vscode/
# utility bat files:
*jump_in_venv.bat
#local tests
tests/local*

View File

@ -1,24 +0,0 @@
# anim-setup
anim-setup is a Blender Add-on that automates the setup of animation scenes for the Sprite-Fright project.
## Installation
Download or clone this repository.
In the root project folder you will find the 'anim_setup' folder. Place this folder in your Blender addons directory or create a sym link to it.
After install you need to configure the addon in the addon preferences.
## Features
The addon relies on the correct naming of asset and camera actions in the corresponding previs file of the shot.
Check the <a href="https://www.notion.so/Animation-Setup-Checklist-ba4d044ec2354b8baae2b3472b757569"> Animation Setup Checklist</a>.
Operators of the addon:
- Setup Workspace for animation
- Load latest edit from edit export directory
- Import camera action from the previs file
- Import actions for found assets from previs file
- Shift animation of camera and asset actions to start at layout cut in
- Create missing actions for found assets in scene
## Development
In the project root you will find a `pyproject.toml` and `peotry.lock` file.
With `poetry` you can easily generate a virtual env for the project which should get you setup quickly.
Basic Usage: https://python-poetry.org/docs/basic-usage/

View File

@ -1,57 +0,0 @@
import bpy
from . import asglobals
from . import prefs
from . import kitsu
from . import props
from . import opsdata
from . import ops
from . import ui
from .log import LoggerFactory
logger = LoggerFactory.getLogger(__name__)
bl_info = {
"name": "Anim Setup",
"author": "Paul Golter",
"description": "Blender addon to setup animation scenes for the spritefright project",
"blender": (3, 0, 0),
"version": (0, 1, 0),
"location": "View3D",
"warning": "",
"doc_url": "",
"tracker_url": "",
"category": "Generic",
}
_need_reload = "ops" in locals()
if _need_reload:
import importlib
asglobals = importlib.reload(asglobals)
prefs = importlib.reload(prefs)
kitsu = importlib.reload(kitsu)
props = importlib.reload(props)
opsdata = importlib.reload(opsdata)
ops = importlib.reload(ops)
ui = importlib.reload(ui)
def register():
prefs.register()
props.register()
ops.register()
ui.register()
logger.info("Registered anim-setup")
def unregister():
ui.unregister()
ops.unregister()
props.unregister()
prefs.unregister()
if __name__ == "__main__":
register()

View File

@ -1,17 +0,0 @@
PROJECT_NAME = "SpriteFright"
PROJECT_ID = "fc77c0b9-bb76-41c3-b843-c9b156f9b3ec"
ACTION_ASSETS = [
"CH-ellie",
"CH-jay",
"CH-phil",
"CH-rex",
"CH-elder_sprite",
"CH-victoria",
"CH-bird",
"PR-bbq_grill",
"PR-boombox",
"PR-tree_chasm",
"PR-log_bridge_trunk"
]
MULTI_ASSETS = ["CH-sprite"]
HIDE_COLLS = ["mushrooms_center", "treetop_leaves"]

View File

@ -1,315 +0,0 @@
from __future__ import annotations
import requests
from dataclasses import asdict, dataclass, field
from typing import Any, Dict, List, Optional, Union
from .log import LoggerFactory
logger = LoggerFactory.getLogger()
class KitsuException(Exception):
pass
class KitsuConnector:
def __init__(self, preferences: "AS_AddonPreferences"):
self._preferences = preferences
self.__access_token = ""
self.__validate()
self.__authorize()
def __validate(self) -> None:
self._preferences.kitsu._validate()
def __authorize(self) -> None:
kitsu_pref = self._preferences.kitsu
backend = kitsu_pref.backend
email = kitsu_pref.email
password = kitsu_pref.password
logger.info(f"authorize {email} against {backend}")
response = requests.post(
url=f"{backend}/auth/login", data={"email": email, "password": password}
)
if response.status_code != 200:
self.__access_token = ""
raise KitsuException(
f"unable to authorize (status code={response.status_code})"
)
json_response = response.json()
self.__access_token = json_response["access_token"]
def api_get(self, api: str) -> Any:
kitsu_pref = self._preferences.kitsu
backend = kitsu_pref.backend
response = requests.get(
url=f"{backend}{api}",
headers={"Authorization": f"Bearer {self.__access_token}"},
)
if response.status_code != 200:
raise KitsuException(
f"unable to call kitsu (api={api}, status code={response.status_code})"
)
return response.json()
@classmethod
def fetch_first(
cls, json_response: Dict[str, Any], filter: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
if not isinstance(json_response, list):
raise ValueError(
f"Failed to fetch one, excpected list object: {json_response}"
)
for item in json_response:
matches = 0
for f in filter:
if f in item and item[f] == filter[f]:
matches += 1
if matches == len(filter):
return item
logger.error("Filter had no match %s on json response.", str(filter))
return None
@classmethod
def fetch_all(
cls, json_response: Dict[str, Any], filter: Dict[str, Any]
) -> List[Dict[str, Any]]:
if not isinstance(json_response, list):
raise ValueError(
f"Failed to fetch all, excpected list object: {json_response}"
)
valid_items: List[Dict[str, Any]] = []
for item in json_response:
matches = 0
for f in filter:
if f in item and item[f] == filter[f]:
matches += 1
if matches == len(filter):
valid_items.append(item)
return valid_items
class ProjectList(KitsuConnector):
"""
Class to get object oriented representation of backend productions data structure.
"""
def __init__(self):
self._projects: List[Project] = []
self._init_projects()
@property
def names(self) -> List[str]:
return [p.name for p in self._projects]
@property
def projects(self) -> List[Project]:
return self._projects
def _init_projects(self) -> None:
api_url = "data/projects"
for project in self.api_get(api_url):
self._projects.append(Project(**project))
@dataclass
class Project(KitsuConnector):
"""
Class to get object oriented representation of backend project data structure.
Can shortcut some functions from gazu api because active project is given through class instance.
Has multiple constructor functions (by_name, by_id, init>by_dict)
"""
id: str = ""
created_at: str = ""
updated_at: str = ""
name: str = ""
code: Optional[str] = None
description: Optional[str] = None
shotgun_id: Optional[str] = None
data: None = None
has_avatar: bool = False
fps: Optional[str] = None
ratio: Optional[str] = None
resolution: Optional[str] = None
production_type: str = ""
start_date: Optional[str] = None
end_date: Optional[str] = None
man_days: Optional[str] = None
nb_episodes: int = 0
episode_span: int = 0
project_status_id: str = ""
type: str = ""
project_status_name: str = ""
file_tree: Dict[str, Any] = field(default_factory=dict)
team: List[Any] = field(default_factory=list)
asset_types: List[Any] = field(default_factory=list)
task_types: List[Any] = field(default_factory=list)
task_statuses: List[Any] = field(default_factory=list)
@classmethod
def by_id(cls, connector: KitsuConnector, project_id: str) -> Project:
api_url = f"data/projects/{project_id}"
project_dict = connector.api_get(api_url)
return cls(**project_dict)
# SEQUENCES
# ---------------
def get_sequence(self, connector: KitsuConnector, seq_id: str) -> Sequence:
return Sequence.by_id(connector, seq_id)
def get_sequence_by_name(
self, connector: KitsuConnector, seq_name: str
) -> Optional[Sequence]:
return Sequence.by_name(connector, self, seq_name)
def get_sequences_all(self, connector: KitsuConnector) -> List[Sequence]:
api_url = f"data/projects/{self.id}/sequences"
seq_dicts = connector.api_get(api_url)
sequences = [Sequence(**s) for s in seq_dicts]
return sorted(sequences, key=lambda x: x.name)
# SHOT
# ---------------
def get_shot(self, connector: KitsuConnector, shot_id: str) -> Shot:
return Shot.by_id(connector, shot_id)
def get_shots_all(self, connector: KitsuConnector) -> List[Shot]:
api_url = f"data/projects/{self.id}/shots"
shot_dicts = connector.api_get(api_url)
shots = [Shot(**s) for s in shot_dicts]
return sorted(shots, key=lambda x: x.name)
def get_shot_by_name(
self, connector: KitsuConnector, sequence: Sequence, name: str
) -> Optional[Shot]:
all_shots = self.get_shots_all(connector)
return Shot.by_name(connector, sequence, name)
def __bool__(self):
return bool(self.id)
@dataclass
class Sequence(KitsuConnector):
"""
Class to get object oriented representation of backend sequence data structure.
Has multiple constructor functions (by_name, by_id, init>by_dict)
"""
id: str = ""
created_at: str = ""
updated_at: str = ""
name: str = ""
code: Optional[str] = None
description: Optional[str] = None
shotgun_id: Optional[str] = None
canceled: bool = False
nb_frames: Optional[int] = None
project_id: str = ""
entity_type_id: str = ""
parent_id: str = ""
source_id: Optional[str] = None
preview_file_id: Optional[str] = None
data: Optional[Dict[str, Any]] = None
type: str = ""
project_name: str = ""
@classmethod
def by_id(cls, connector: KitsuConnector, seq_id: str) -> Sequence:
api_url = f"data/sequences/{seq_id}"
seq_dict = connector.api_get(seq_id)
return cls(**seq_dict)
@classmethod
def by_name(
cls, connector: KitsuConnector, project: Project, seq_name: str
) -> Optional[Sequence]:
api_url = f"data/projects/{project.id}/sequences"
seq_dicts = connector.api_get(api_url)
seq_dict = connector.fetch_first(seq_dicts, {"name": seq_name})
# Can be None if name not found.
if not seq_dict:
return None
return cls(**seq_dict)
def __bool__(self):
return bool(self.id)
@dataclass
class Shot(KitsuConnector):
"""
Class to get object oriented representation of backend shot data structure.
Has multiple constructor functions (by_name, by_id, init>by_dict
"""
id: str = ""
created_at: str = ""
updated_at: str = ""
name: str = ""
canceled: bool = False
code: Optional[str] = None
description: Optional[str] = None
entity_type_id: str = ""
episode_id: Optional[str] = None
episode_name: str = ""
fps: str = ""
frame_in: str = ""
frame_out: str = ""
nb_frames: int = 0
parent_id: str = ""
preview_file_id: Optional[str] = None
project_id: str = ""
project_name: str = ""
sequence_id: str = ""
sequence_name: str = ""
source_id: Optional[str] = None
shotgun_id: Optional[str] = None
type: str = ""
data: Dict[str, Any] = field(default_factory=dict)
tasks: List[Dict[str, Any]] = field(default_factory=list)
@classmethod
def by_id(cls, connector: KitsuConnector, shot_id: str) -> Shot:
api_url = f"data/shots/{shot_id}"
shot_dict = connector.api_get(shot_id)
return cls(**shot_dict)
@classmethod
def by_name(
cls, connector: KitsuConnector, sequence: Sequence, shot_name: str
) -> Optional[Shot]:
api_url = f"data/projects/{sequence.project_id}/shots"
shot_dicts = connector.api_get(api_url)
shot_dict = connector.fetch_first(
shot_dicts, {"parent_id": sequence.id, "name": shot_name}
)
# Can be None if name not found.
if not shot_dict:
return None
return cls(**shot_dict)
def __bool__(self):
return bool(self.id)

View File

@ -1,16 +0,0 @@
import logging
import sys
from typing import List, Tuple
class LoggerFactory:
"""
Utility class to streamline logger creation
"""
@staticmethod
def getLogger(name=__name__):
name = name
logger = logging.getLogger(name)
return logger

View File

@ -1,835 +0,0 @@
import re
from pathlib import Path
import types
from typing import Container, Dict, List, Set, Optional
import bpy
from .log import LoggerFactory
from .kitsu import KitsuConnector, Shot, Project, Sequence
from . import opsdata, prefs, asglobals
logger = LoggerFactory.getLogger()
def ui_redraw() -> None:
"""Forces blender to redraw the UI."""
for screen in bpy.data.screens:
for area in screen.areas:
area.tag_redraw()
class AS_OT_create_actions(bpy.types.Operator):
bl_idname = "as.create_action"
bl_label = "Create action"
bl_description = (
"Creates action for all found assets that have no assigned yet. "
"Names them following the blender-studio convention"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
act_coll = context.view_layer.active_layer_collection.collection
return bool(bpy.data.filepath and act_coll)
def execute(self, context: bpy.types.Context) -> Set[str]:
assigned: List[bpy.types.Action] = []
created: List[bpy.types.Action] = []
failed: List[bpy.types.Collection] = []
collections = opsdata.get_valid_collections(context)
exists: List[bpy.types.Collection] = []
if not collections:
self.report({"WARNING"}, "No valid collections available")
return {"CANCELLED"}
for coll in collections:
print("\n")
rig = opsdata.find_rig(coll)
if not rig:
logger.warning(f"{coll.name} contains no rig.")
failed.append(coll)
continue
# Create animation data if not existent.
if not rig.animation_data:
rig.animation_data_create()
logger.info("%s created animation data", rig.name)
# If action already exists check for fake user and then continue.
if rig.animation_data.action:
logger.info("%s already has an action assigned", rig.name)
if not rig.animation_data.action.use_fake_user:
rig.animation_data.action.use_fake_user = True
logger.info("%s assigned existing action fake user", rig.name)
exists.append(coll)
continue
# Create new action.
action_name_new = opsdata.gen_action_name(coll)
try:
action = bpy.data.actions[action_name_new]
except KeyError:
action = bpy.data.actions.new(action_name_new)
logger.info("Created action: %s", action.name)
created.append(action)
else:
logger.info("Action %s already exists. Will take that.", action.name)
# Assign action.
rig.animation_data.action = action
logger.info("%s assigned action %s", rig.name, action.name)
# Add fake user.
action.use_fake_user = True
assigned.append(action)
self.report(
{"INFO"},
"Actions: Created %s | Assigned %s | Exists %s | Failed %s"
% (len(created), len(assigned), len(exists), len(failed)),
)
return {"FINISHED"}
class AS_OT_setup_workspaces(bpy.types.Operator):
bl_idname = "as.setup_workspaces"
bl_label = "Setup Workspace"
bl_description = "Sets up the workspaces for the animation task"
def execute(self, context: bpy.types.Context) -> Set[str]:
# Remove non anim workspaces.
for ws in bpy.data.workspaces:
if ws.name != "Animation":
bpy.ops.workspace.delete({"workspace": ws})
self.report({"INFO"}, "Deleted non Animation workspaces")
return {"FINISHED"}
class AS_OT_load_latest_edit(bpy.types.Operator):
bl_idname = "as.load_latest_edit"
bl_label = "Load edit"
bl_description = (
"Loads latest edit from shot_preview_folder "
"Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu"
)
@classmethod
def can_load_edit(cls, context: bpy.types.Context) -> bool:
"""Check if shared dir and VSE area are available"""
addon_prefs = prefs.addon_prefs_get(context)
edit_export_path = Path(addon_prefs.edit_export_path)
# Needs to be run in sequence editor area
# TODO: temporarily create a VSE area if not available.
area_override = None
for area in bpy.context.screen.areas:
if area.type == "SEQUENCE_EDITOR":
area_override = area
return bool(area_override and edit_export_path)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return cls.can_load_edit(context)
@classmethod
def description(cls, context, properties):
if cls.can_load_edit(context):
return "Load latest edit from shared folder"
else:
return "Shared folder not set, or VSE area not available in this workspace"
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
edit_export_path = Path(addon_prefs.edit_export_path)
strip_channel = 1
latest_file = self._get_latest_edit(context)
if not latest_file:
self.report(
{"ERROR"}, f"Found no edit file in: {edit_export_path.as_posix()}"
)
strip_filepath = latest_file.as_posix()
strip_frame_start = 101
# Needs to be run in sequence editor area.
area_override = None
for area in bpy.context.screen.areas:
if area.type == "SEQUENCE_EDITOR":
area_override = area
if not area_override:
self.report({"ERROR"}, "No sequence editor are found")
return {"CANCELLED"}
override = bpy.context.copy()
override["area"] = area_override
bpy.ops.sequencer.movie_strip_add(
override,
filepath=strip_filepath,
relative_path=False,
frame_start=strip_frame_start,
channel=strip_channel,
fit_method="FIT",
)
# Get sequence name.
seqname = opsdata.get_sequence_from_file()
if not seqname:
self.report({"ERROR"}, "Failed to retrieve seqname from current file.")
return {"CANCELLED"}
# Get shotname.
shotname = opsdata.get_shot_name_from_file()
if not shotname:
self.report({"ERROR"}, "Failed to retrieve shotname from current file.")
return {"CANCELLED"}
# Setup connector and get data from kitsu.
connector = KitsuConnector(addon_prefs)
project = Project.by_id(connector, addon_prefs.kitsu.project_id)
sequence = project.get_sequence_by_name(connector, seqname)
if not sequence:
self.report({"ERROR"}, f"Failed to find {seqname} on kitsu.")
return {"CANCELLED"}
shot = project.get_shot_by_name(connector, sequence, shotname)
if not shot:
self.report({"ERROR"}, f"Failed to find shot {shotname} on kitsu.")
return {"CANCELLED"}
# Update shift frame range prop.
frame_in = shot.data["frame_in"]
frame_out = shot.data["frame_out"]
frame_3d_in = shot.data["3d_in"]
frame_3d_offset = frame_3d_in - 101
if not frame_in:
self.report(
{"ERROR"}, f"On kitsu 'frame_in' is not defined for shot {shotname}."
)
return {"CANCELLED"}
# Set sequence strip start kitsu data.
for strip in context.scene.sequence_editor.sequences_all:
strip.frame_start = -frame_in + (strip_frame_start * 2) + frame_3d_offset
self.report({"INFO"}, f"Loaded latest edit: {latest_file.name}")
return {"FINISHED"}
def _get_latest_edit(self, context: bpy.types.Context):
addon_prefs = prefs.addon_prefs_get(context)
edit_export_path = Path(addon_prefs.edit_export_path)
files_list = [
f
for f in edit_export_path.iterdir()
if f.is_file() and self._is_valid_edit_name(f.name)
]
files_list = sorted(files_list, reverse=True)
return files_list[0]
def _is_valid_edit_name(self, filename: str) -> bool:
pattern = r"sf-edit-v\d\d\d.mp4"
match = re.search(pattern, filename)
if match:
return True
return False
class AS_OT_import_camera(bpy.types.Operator):
bl_idname = "as.import_camera"
bl_label = "Import Camera"
bl_description = "Imports camera rig and makes library override"
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(addon_prefs.is_project_root_valid and bpy.data.filepath)
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
# Import camera rig and make override.
camera_rig_path = addon_prefs.camera_rig_path
if not camera_rig_path:
self.report({"ERROR"}, "Failed to import camera rig")
return {"CANCELLED"}
cam_lib_coll = opsdata.import_data_from_lib(
"collections",
"CA-camera_rig",
camera_rig_path,
)
opsdata.instance_coll_to_scene_and_override(context, cam_lib_coll)
cam_coll = bpy.data.collections[cam_lib_coll.name, None]
self.report({"INFO"}, f"Imported camera: {cam_coll.name}")
return {"FINISHED"}
class AS_OT_import_camera_action(bpy.types.Operator):
bl_idname = "as.import_camera_action"
bl_label = "Import Camera Action"
bl_description = (
"Imports camera action of previs file that matches current shot and assigns it"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(addon_prefs.is_project_root_valid and bpy.data.filepath)
def execute(self, context: bpy.types.Context) -> Set[str]:
try:
cam_coll = bpy.data.collections["CA-camera_rig", None]
except KeyError:
self.report({"ERROR"}, f"Camera collection CA-camera_rig is not imported")
return {"CANCELELD"}
# Import camera action from previz file.
# Get shotname and previs filepath.
shotname = opsdata.get_shot_name_from_file()
if not shotname:
self.report({"ERROR"}, "Failed to retrieve shotname from current file.")
return {"CANCELLED"}
previs_path = opsdata.get_previs_file(context)
if not previs_path:
self.report({"ERROR"}, "Failed to find previz file")
return {"CANCELLED"}
# Check if cam action name exists in previs library.
cam_action_name_new = opsdata.get_cam_action_name_from_lib(
shotname, previs_path
)
if not cam_action_name_new:
self.report(
{"ERROR"},
f"Camera action: {cam_action_name_new} not found in lib: {previs_path.name}",
)
return {"CANCELLED"}
# Import cam action data block.
cam_action = opsdata.import_data_from_lib(
"actions", cam_action_name_new, previs_path, link=False
)
# Find rig to assing action to.
rig = opsdata.find_rig(cam_coll)
if not rig:
self.report({"WARNING"}, f"{cam_coll.name} contains no rig.")
return {"CANCELLED"}
# Assign action.
rig.animation_data.action = cam_action
logger.info("%s assigned action %s", rig.name, cam_action.name)
# Add fake user.
cam_action.use_fake_user = True
# Ensure version suffix to action data bloc.
opsdata.ensure_name_version_suffix(cam_action)
self.report({"INFO"}, f"{rig.name} imported camera action: {cam_action.name}")
return {"FINISHED"}
class AS_OT_import_asset_actions(bpy.types.Operator):
"""Imports asset action of previs file that matches current shot and assigns it"""
bl_idname = "as.import_asset_actions"
bl_label = "Import Asset Actions"
bl_description = (
"For each found asset tries to find action in previs file. "
"Imports it to current file, renames it, adds fake user and assigns it"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(addon_prefs.is_project_root_valid and bpy.data.filepath)
def execute(self, context: bpy.types.Context) -> Set[str]:
succeeded = []
failed = []
actions_imported = []
renamed_actions = []
# Get shotname and previs filepath.
shotname = opsdata.get_shot_name_from_file()
if not shotname:
self.report({"ERROR"}, "Failed to retrieve shotname from current file.")
return {"CANCELLED"}
previs_path = opsdata.get_previs_file(context)
if not previs_path:
self.report({"ERROR"}, "Failed to find previz file")
return {"CANCELLED"}
# Check if cam action name exists in previs library.
action_candidates: Dict[str, List[str]] = {}
asset_colls = []
with bpy.data.libraries.load(
previs_path.as_posix(), relative=True, link=False
) as (
data_from,
data_to,
):
for asset in asglobals.ACTION_ASSETS:
# Check if asset is in current scene.
try:
coll = bpy.data.collections[asset]
except KeyError:
# can continue here if not in scene we
# cant load action anyway
continue
else:
logger.info("Found asset in scene: %s", coll.name)
asset_colls.append(coll)
# Find if actions exists for that asset in previs file.
asset_name = opsdata.find_asset_name(asset)
for action in data_from.actions:
if action.startswith(f"ANI-{asset_name}."):
# Create key if not existent yet.
if asset not in action_candidates:
action_candidates[asset] = []
# Append action to that asset.
action_candidates[asset].append(action)
# Load and assign actions for asset colls.
for coll in asset_colls:
# Find rig.
rig = opsdata.find_rig(coll)
if not rig:
logger.warning("%s contains no rig.", coll.name)
continue
# Check if action was found in previs file for that asset.
if not coll.name in action_candidates:
logger.warning("%s no action found in previs file", coll.name)
continue
else:
logger.info(
"%s found actions in previs file: %s",
asset,
str(action_candidates[coll.name]),
)
# Check if multiple actions are in the prvis file for that asset.
if len(action_candidates[coll.name]) > 1:
logger.warning(
"%s Multiple actions found in previs file: %s",
coll.name,
str(action_candidates[coll.name]),
)
continue
# Import action from previs file.
actions = action_candidates[coll.name]
action = opsdata.import_data_from_lib(
"actions", actions[0], previs_path, link=False
)
if not action:
continue
actions_imported.append(action)
# Create animation data if not existent.
if not rig.animation_data:
rig.animation_data_create()
logger.info("%s created animation data", rig.name)
# Assign action.
rig.animation_data.action = action
logger.info("%s assigned action %s", rig.name, action.name)
# Add fake user.
action.use_fake_user = True
# Rename actions.
action_name_new = opsdata.gen_action_name(coll)
try:
action_existing = bpy.data.actions[action_name_new]
except KeyError:
# Action does not exists can rename.
old_name = action.name
action.name = action_name_new
logger.info("Renamed action: %s to %s", old_name, action.name)
renamed_actions.append(action)
else:
# Action name already exists in this scene.
logger.info(
"Failed to rename action action %s to %s. Already exists",
action.name,
action_name_new,
)
continue
self.report(
{"INFO"},
f"Found Assets: {len(asset_colls)} | Imported Actions: {len(actions_imported)} | Renamed Actions: {len(renamed_actions)}",
)
return {"FINISHED"}
class AS_OT_import_multi_assets(bpy.types.Operator):
bl_idname = "as.import_multi_assets"
bl_label = "Import Multi Assets"
bl_description = (
"For each found multi asset tries to find action in previs file. "
"Imports it to current file, renames it, adds fake user and assigns it"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(addon_prefs.is_project_root_valid and bpy.data.filepath)
def execute(self, context: bpy.types.Context) -> Set[str]:
actions_imported = []
new_colls = []
# Get shotname and previs filepath.
shotname = opsdata.get_shot_name_from_file()
if not shotname:
self.report({"ERROR"}, "Failed to retrieve shotname from current file.")
return {"CANCELLED"}
previs_path = opsdata.get_previs_file(context)
if not previs_path:
self.report({"ERROR"}, "Failed to find previz file")
return {"CANCELLED"}
# Check if cam action name exists in previs library.
action_candidates: Dict[str, List[str]] = {}
asset_colls: List[bpy.types.Collection] = []
with bpy.data.libraries.load(
previs_path.as_posix(), relative=True, link=False
) as (
data_from,
data_to,
):
data_from_actions: List[str] = data_from.actions
data_from_actions.sort()
# Find all sprites actions.
for asset in asglobals.MULTI_ASSETS:
# Check if asset is in current scene.
try:
coll = bpy.data.collections[asset]
except KeyError:
# Can continue here if not in scene we
# cant load action anyway.
continue
else:
logger.info("Found asset in scene: %s", coll.name)
asset_colls.append(coll)
# Find if actions exists for that asset in previs file.
asset_name = opsdata.find_asset_name(asset)
for action in data_from_actions:
if action.startswith(f"ANI-{asset_name}"):
# Create key if not existent yet.
if asset not in action_candidates:
action_candidates[asset] = []
# Append action to that asset.
action_candidates[asset].append(action)
# Load and assign actions for asset colls.
color_tag: str = ""
for coll in asset_colls:
# Check if action was found in previs file for that asset.
if not coll.name in action_candidates:
logger.warning("%s no action found in previs file", coll.name)
continue
else:
logger.info(
"%s found actions in previs file: %s",
asset,
str(action_candidates[coll.name]),
)
# Create duplicate for each action.
for idx, action_candidate in enumerate(action_candidates[coll.name]):
# First index use existing collection that was already created by shot builder.
if idx == 0:
new_coll = bpy.data.collections[asset, None]
logger.info("First index will use existing coll: %s", new_coll.name)
color_tag = new_coll.color_tag # Take color from first collection.
else:
ref_coll = opsdata.get_ref_coll(coll)
new_coll = ref_coll.override_hierarchy_create(
context.scene, context.view_layer, reference=coll
)
new_coll.color_tag = color_tag
logger.info("Created new override collection: %s", new_coll.name)
new_colls.append(new_coll)
# Find rig of new coll.
rig = opsdata.find_rig(new_coll)
if not rig:
logger.warning("%s contains no rig.", coll.name)
continue
# Import action.
action = opsdata.import_data_from_lib(
"actions", action_candidate, previs_path, link=False
)
if not action:
continue
actions_imported.append(action)
# Create animation data if not existent.
if not rig.animation_data:
rig.animation_data_create()
logger.info("%s created animation data", rig.name)
# Assign action.
rig.animation_data.action = action
logger.info("%s assigned action %s", rig.name, action.name)
self.report(
{"INFO"},
f"Found Assets: {len(asset_colls)} | Imported Actions: {len(actions_imported)} | New collections: {len(new_colls)}",
)
return {"FINISHED"}
class AS_OT_shift_anim(bpy.types.Operator):
bl_idname = "as.shift_anim"
bl_label = "Shift Anim"
bl_description = (
"Shifts the animation of found assets by number of frames. "
"It also shifts the camera animation as well as its modifier values"
)
multi_assets: bpy.props.BoolProperty(name="Do Multi Assets")
def execute(self, context: bpy.types.Context) -> Set[str]:
# Define the frame offset by:
# Subtracting the layout cut in frame (to set the 0)
# Adding 101 (the animation start for a shot)
# For example, layout frame 520 becomes frames_offset -520 + 101 = -419.
frames_offset = -context.scene.anim_setup.layout_cut_in + 101
rigs: List[bpy.types.Armature] = []
if not self.multi_assets:
# Get cam coll.
try:
rig = bpy.data.objects["RIG-camera", None]
except KeyError:
logger.warning("Failed to find camera object 'RIG-camera'")
else:
rigs.append(rig)
# Find assets.
for asset in asglobals.ACTION_ASSETS:
# Check if asset is in current scene.
try:
coll = bpy.data.collections[asset]
except KeyError:
# Can continue here if not in scene we
# cant load action anyway.
continue
else:
logger.info("Found asset in scene: %s", coll.name)
# Find rig.
rig = opsdata.find_rig(coll)
if not rig:
logger.warning("%s contains no rig.", coll.name)
continue
rigs.append(rig)
else:
for asset in asglobals.MULTI_ASSETS:
for coll in bpy.data.collections:
if not opsdata.is_item_lib_override(coll):
continue
if not coll.name.startswith(asset):
continue
logger.info("Found asset in scene: %s", coll.name)
# Find rig.
rig = opsdata.find_rig(coll)
if not rig:
logger.warning("%s contains no rig.", coll.name)
continue
rigs.append(rig)
if not rigs:
self.report(
{"ERROR"}, "Failed to find any assets or cameras to shift animation."
)
return {"CANCELLED"}
for rig in rigs:
for fcurve in rig.animation_data.action.fcurves:
# Shift all keyframes.
for point in fcurve.keyframe_points:
# Print(f"{fcurve.data_path}|{fcurve.array_index}: {point.co.x}|{point.co.y}").
point.co.x += frames_offset
# Don't forget the keyframe's handles:.
point.handle_left.x += frames_offset
point.handle_right.x += frames_offset
# Shift all noise modififers values.
for m in fcurve.modifiers:
if not m.type == "NOISE":
continue
m.offset += frames_offset
if m.use_restricted_range:
frame_start = m.frame_start
frame_end = m.frame_end
m.frame_start = frame_start + (frames_offset)
m.frame_end = frame_end + (frames_offset)
logger.info(
"%s shifted %s modifier values by %i frames",
m.id_data.name,
m.type.lower(),
frames_offset,
)
logger.info(
"%s: %s shifted all keyframes by %i frames",
rig.name,
rig.animation_data.action.name,
frames_offset,
)
self.report(
{"INFO"}, f"Shifted animation of {len(rigs)} actions by {frames_offset}"
)
return {"FINISHED"}
class AS_OT_apply_additional_settings(bpy.types.Operator):
bl_idname = "as.apply_additional_settings"
bl_label = "Apply Additional Settings"
bl_description = (
"Apply some additional settings that are important " "for animation scenes"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe_area = cls._get_sqe_area(context)
return bool(sqe_area)
def execute(self, context: bpy.types.Context) -> Set[str]:
sqe_area = self._get_sqe_area(context)
sqe_area.spaces.active.use_proxies = False
sqe_area.spaces.active.proxy_render_size = "PROXY_100"
self.report({"INFO"}, "Set: use_proxies | proxy_render_size")
return {"FINISHED"}
@classmethod
def _get_sqe_area(cls, context: bpy.types.Context):
for window in context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == "SEQUENCE_EDITOR":
return area
return None
class AS_OT_exclude_colls(bpy.types.Operator):
"""Excludes Collections that are not needed for animation"""
bl_idname = "as.exclude_colls"
bl_label = "Exclude Collections"
bl_description = (
"Exclude some collections by name that are not needed in animation scenes"
)
def execute(self, context: bpy.types.Context) -> Set[str]:
view_layer_colls = opsdata.get_all_view_layer_colls(context)
excluded = []
for coll_name in asglobals.HIDE_COLLS:
# Find view layer collection, if same collection is linked in in 2 different colls in same scene, these
# are 2 different view layer colls, we need to grab all.
valid_view_layer_colls = [
vc for vc in view_layer_colls if vc.name == coll_name
]
if not valid_view_layer_colls:
logger.info("No view layer collections named: %s", coll_name)
continue
for view_layer_coll in valid_view_layer_colls:
view_layer_coll.exclude = True
logger.info("Excluded view layer collection: %s", view_layer_coll.name)
excluded.append(view_layer_coll)
self.report(
{"INFO"}, f"Excluded Collections: {list([v.name for v in excluded])}"
)
return {"FINISHED"}
# ---------REGISTER ----------.
classes = [
AS_OT_create_actions,
AS_OT_setup_workspaces,
AS_OT_load_latest_edit,
AS_OT_import_camera,
AS_OT_import_camera_action,
AS_OT_shift_anim,
AS_OT_apply_additional_settings,
AS_OT_import_asset_actions,
AS_OT_exclude_colls,
AS_OT_import_multi_assets,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,344 +0,0 @@
import re
from pathlib import Path
from typing import Optional, Dict, Union, Any, List, Generator
import bpy
from bpy.types import Key
from . import prefs
from .log import LoggerFactory
logger = LoggerFactory.getLogger()
def get_shot_name_from_file() -> Optional[str]:
if not bpy.data.filepath:
return None
# Default 110_0030_A.anim.blend.
return Path(bpy.data.filepath).name.split(".")[0]
def get_sequence_from_file() -> Optional[str]:
if not bpy.data.filepath:
return None
# ./spritefright/pro/shots/110_rextoria/110_0010_A/110_0010_A.anim.blend.
return Path(bpy.data.filepath).parents[1].name
def get_seqeunce_short_from_shot_name(shotname: str) -> str:
return shotname.split("_")[0]
def get_cam_action_name_from_shot(shotname: str) -> str:
# ANI-camera.070_0010_A.
return f"ANI-camera.{shotname}"
def get_cam_action_name_from_lib(shotname: str, libpath: Path) -> Optional[str]:
valid_actions = []
with bpy.data.libraries.load(libpath.as_posix(), relative=True) as (
data_from,
data_to,
):
for action in data_from.actions:
if action.startswith(get_cam_action_name_from_shot(shotname)):
valid_actions.append(action)
if not valid_actions:
return None
return sorted(valid_actions, reverse=True)[0]
def get_previs_file(context: bpy.types.Context) -> Optional[Path]:
addon_prefs = prefs.addon_prefs_get(context)
shotname = get_shot_name_from_file()
if not shotname:
return None
seqname = get_seqeunce_short_from_shot_name(shotname)
previs_path = Path(addon_prefs.previs_root_path)
# Catch custom cases when previs files are split up for specific shots.
if shotname == "020_0010_A":
return previs_path / "020_grove.020_0010_A.blend"
elif shotname == "020_0020_A" or shotname == "020_0050_A":
return previs_path / "020_grove.shove.blend"
elif shotname in ["020_0060_A", "020_0070_A"]:
return previs_path / "020_grove.crowdcamping_alt.blend"
elif shotname in ["020_0160_A", "020_0170_A", "020_0173_A", "020_0176_A"]:
return previs_path / "020_grove.weenie_alt.blend"
else:
for f in previs_path.iterdir():
if f.is_file() and f.suffix == ".blend" and f.name.startswith(seqname):
if len(f.name.split(".")) > 2:
continue
return f
return None
def traverse_collection_tree(
collection: bpy.types.Collection,
) -> Generator[bpy.types.Collection, None, None]:
yield collection
for child in collection.children:
yield from traverse_collection_tree(child)
def import_data_from_lib(
data_category: str,
data_name: str,
libpath: Path,
link: bool = True,
):
noun = "Appended"
if link:
noun = "Linked"
with bpy.data.libraries.load(libpath.as_posix(), relative=True, link=link) as (
data_from,
data_to,
):
if data_name not in eval(f"data_from.{data_category}"):
logger.error(
"Failed to import %s %s from %s. Doesn't exist in file.",
data_category,
data_name,
libpath.as_posix(),
)
return None
# Check if datablock with same name already exists in blend file.
try:
eval(f"bpy.data.{data_category}['{data_name}']")
except KeyError:
pass
else:
logger.info(
"%s already in bpy.data.%s of this blendfile.", data_name, data_category
)
return None
# Append data block.
eval(f"data_to.{data_category}.append('{data_name}')")
logger.info(
"%s: %s from library: %s",
noun,
data_name,
libpath.as_posix(),
)
if link:
return eval(
f"bpy.data.{data_category}['{data_name}', '{bpy.path.relpath(libpath.as_posix())}']"
)
return eval(f"bpy.data.{data_category}['{data_name}']")
def instance_coll_to_scene_and_override(
context: bpy.types.Context, source_collection: bpy.types.Collection
) -> bpy.types.Collection:
instance_obj = _create_collection_instance(source_collection)
_make_library_override(context, instance_obj)
return bpy.data.collections[source_collection.name, None]
def _create_collection_instance(
source_collection: bpy.types.Collection,
) -> bpy.types.Object:
# Name has no effect how the overwritten library collection in the end
# use empty to instance source collection.
instance_obj = bpy.data.objects.new(name="", object_data=None)
instance_obj.instance_collection = source_collection
instance_obj.instance_type = "COLLECTION"
parent_collection = bpy.context.view_layer.active_layer_collection
parent_collection.collection.objects.link(instance_obj)
logger.info(
"Instanced collection: %s as: %s",
source_collection.name,
instance_obj.name,
)
return instance_obj
def _make_library_override(
context: bpy.types.Context,
instance_obj: bpy.types.Object,
) -> None:
log_name = instance_obj.name
# Deselect all.
bpy.ops.object.select_all(action="DESELECT")
# Needs active object (coll instance).
context.view_layer.objects.active = instance_obj
instance_obj.select_set(True)
# Add library override.
bpy.ops.object.make_override_library()
logger.info(
"%s make library override.",
log_name,
)
def find_asset_name(name: str) -> str:
if name.endswith("_rig"):
name = name[:-4]
return name.split("-")[-1] # CH-rex -> 'rex'
def find_rig(coll: bpy.types.Collection) -> Optional[bpy.types.Armature]:
coll_suffix = find_asset_name(coll.name)
valid_rigs = []
for obj in coll.all_objects:
# Default rig name: 'RIG-rex' / 'RIG-Rex'.
if obj.type != "ARMATURE":
continue
if not obj.name.startswith("RIG"):
continue
valid_rigs.append(obj)
if not valid_rigs:
return None
elif len(valid_rigs) == 1:
logger.info("Found rig: %s", valid_rigs[0].name)
return valid_rigs[0]
else:
logger.error("%s found multiple rigs %s", coll.name, str(valid_rigs))
return None
def ensure_name_version_suffix(datablock: Any) -> Any:
version_pattern = r"v\d\d\d"
match = re.search(version_pattern, datablock.name)
if not match:
datablock.name = datablock.name + ".v001"
return datablock
def get_valid_collections(context: bpy.types.Context) -> List[bpy.types.Collection]:
valid_prefixes = ["CH-", "PR-"]
valid_colls: List[bpy.types.Collection] = []
for coll in context.scene.collection.children:
if coll.name[:3] not in valid_prefixes:
continue
valid_colls.append(coll)
return valid_colls
def is_multi_asset(asset_name: str) -> bool:
if asset_name.startswith("thorn"):
return True
multi_assets = ["sprite", "snail", "spider"]
if asset_name.lower() in multi_assets:
return True
return False
def gen_action_name(coll: bpy.types.Collection):
action_prefix = "ANI"
asset_name = find_asset_name(coll.name).lower()
asset_name = asset_name.replace(".", "_")
version = "v001"
shot_name = get_shot_name_from_file()
action_name_new = f"{action_prefix}-{asset_name}.{shot_name}.{version}"
if is_multi_asset(asset_name):
action_name_new = f"{action_prefix}-{asset_name}_A.{shot_name}.{version}"
return action_name_new
def set_layer_coll_exlcude(
layer_collections: List[bpy.types.LayerCollection], exclude: bool
) -> None:
noun = "Excluded" if exclude else "Included"
for lcoll in layer_collections:
if exclude:
if lcoll.exclude:
continue
lcoll.exclude = True
else:
if not lcoll.exclude:
continue
lcoll.exclude = False
logger.info("%s %s", noun, lcoll.name)
def get_all_view_layer_colls(
context: bpy.types.Context,
) -> List[bpy.types.LayerCollection]:
return list(traverse_collection_tree(context.view_layer.layer_collection))
def get_ref_coll(coll: bpy.types.Collection) -> bpy.types.Collection:
if not coll.override_library:
return coll
return coll.override_library.reference
def is_item_local(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Local collection of blend file.
if not item.override_library and not item.library:
return True
return False
def is_item_lib_override(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Collection from libfile and overwritten.
if item.override_library and not item.library:
return True
return False
def is_item_lib_source(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Source collection from libfile not overwritten.
if not item.override_library and item.library:
return True
return False

View File

@ -1,176 +0,0 @@
import os
from pathlib import Path
from typing import Union, Optional, Any, Dict, Set
import bpy
from .kitsu import KitsuException
from . import asglobals
class KitsuPreferences(bpy.types.PropertyGroup):
backend: bpy.props.StringProperty( # type: ignore
name="Server URL",
description="Kitsu server address",
default="https://kitsu.blender.cloud/api",
)
email: bpy.props.StringProperty( # type: ignore
name="Email",
description="Email to connect to Kitsu",
)
password: bpy.props.StringProperty( # type: ignore
name="Password",
description="Password to connect to Kitsu",
subtype="PASSWORD",
)
project_id: bpy.props.StringProperty( # type: ignore
name="Project ID",
description="Server Id that refers to the last active project",
default=asglobals.PROJECT_ID,
options={"HIDDEN", "SKIP_SAVE"},
)
def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
box = layout.box()
box.label(text="Kitsu")
box.prop(self, "backend")
box.prop(self, "email")
box.prop(self, "password")
box.prop(self, "project_id")
def _validate(self):
if not (self.backend and self.email and self.password and self.project_id):
raise KitsuException(
"Kitsu connector has not been configured in the add-on preferences"
)
class AS_AddonPreferences(bpy.types.AddonPreferences):
bl_idname = __package__
project_root: bpy.props.StringProperty( # type: ignore
name="Project Root",
default="",
options={"HIDDEN", "SKIP_SAVE"},
subtype="DIR_PATH",
)
edit_export_dir: bpy.props.StringProperty( # type: ignore
name="Edit Export Directory",
default="",
options={"HIDDEN", "SKIP_SAVE"},
subtype="DIR_PATH",
)
kitsu: bpy.props.PointerProperty( # type: ignore
name="Kitsu Preferences", type=KitsuPreferences
)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
box = layout.box()
box.row().prop(self, "project_root")
if not self.project_root:
row = box.row()
row.label(text="Please specify the project root directory.", icon="ERROR")
if not bpy.data.filepath and self.project_root.startswith("//"):
row = box.row()
row.label(
text="In order to use a relative path as root cache directory the current file needs to be saved.",
icon="ERROR",
)
box.row().prop(self, "edit_export_dir")
if not self.edit_export_dir:
row = box.row()
row.label(text="Please specify the edit edxport directory.", icon="ERROR")
if not bpy.data.filepath and self.edit_export_dir.startswith("//"):
row = box.row()
row.label(
text="In order to use a relative path as edit export directory the current file needs to be saved.",
icon="ERROR",
)
self.kitsu.draw(layout, context)
@property
def project_root_path(self) -> Optional[Path]:
if not self.is_project_root_valid:
return None
return Path(os.path.abspath(bpy.path.abspath(self.project_root)))
@property
def is_project_root_valid(self) -> bool:
# Check if file is saved.
if not self.project_root:
return False
if not bpy.data.filepath and self.project_root.startswith("//"):
return False
return True
@property
def is_editorial_valid(self) -> bool:
if not self.edit_export_dir:
return False
return Path(self.edit_export_dir).exists()
@property
def edit_export_path(self) -> Optional[Path]:
if not self.is_editorial_valid:
return None
return Path(self.edit_export_dir)
@property
def previs_root_path(self) -> Optional[Path]:
if not self.is_project_root_valid:
return None
previs_path = self.project_root_path / "previz"
if not previs_path.exists():
return None
return previs_path
@property
def camera_rig_path(self) -> Optional[Path]:
if not self.is_project_root_valid:
return None
camera_rig_path = self.project_root_path / "pro/lib/cam/camera_rig.blend"
if not camera_rig_path.exists():
return None
return camera_rig_path
def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences:
"""Shortcut to get cache_manager addon preferences"""
return context.preferences.addons["anim_setup"].preferences
# ---------REGISTER ----------.
classes = [KitsuPreferences, AS_AddonPreferences]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,34 +0,0 @@
from typing import List, Any, Generator, Optional
import bpy
class CM_property_group_scene(bpy.types.PropertyGroup):
layout_cut_in: bpy.props.IntProperty(
name="Layout Cut In",
description="Frame where the camera marker is set for the shot, in the layout file",
default=0,
step=1,
)
# ---------REGISTER ----------.
classes: List[Any] = [
CM_property_group_scene,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
# Scene Properties.
bpy.types.Scene.anim_setup = bpy.props.PointerProperty(type=CM_property_group_scene)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,134 +0,0 @@
import bpy
from . import opsdata
from .ops import (
AS_OT_create_actions,
AS_OT_setup_workspaces,
AS_OT_load_latest_edit,
AS_OT_import_camera,
AS_OT_import_camera_action,
AS_OT_shift_anim,
AS_OT_apply_additional_settings,
AS_OT_import_asset_actions,
AS_OT_exclude_colls,
AS_OT_import_multi_assets
)
class AS_PT_view3d_general(bpy.types.Panel):
"""
Animation Setup general operators.
"""
bl_category = "Anim Setup"
bl_label = "General"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_order = 10
def draw(self, context: bpy.types.Context) -> None:
valid_colls = opsdata.get_valid_collections(context)
layout = self.layout
col = layout.column(align=True)
# Workspace.
col.operator(AS_OT_setup_workspaces.bl_idname)
# Load edit.
col.operator(AS_OT_load_latest_edit.bl_idname)
class AS_PT_view3d_animation_and_actions(bpy.types.Panel):
"""
Animation Setup main operators and properties.
"""
bl_category = "Anim Setup"
bl_label = "Animation and Actions"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_order = 12
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
layout.label(text=f"Previs file: {opsdata.get_previs_file(context)}")
col = layout.column(align=True)
# Import camera action.
col.operator(AS_OT_import_camera_action.bl_idname)
# Import action.
col.operator(
AS_OT_import_asset_actions.bl_idname, text=f"Import Char Actions"
)
col.operator(
AS_OT_import_multi_assets.bl_idname, text=f"Import Multi Asset Actions"
)
col.separator()
col = layout.column()
# Shift animation.
col.prop(context.scene.anim_setup, "layout_cut_in")
col.separator()
split = col.split(factor=0.5, align=True)
split.operator(AS_OT_shift_anim.bl_idname, text="Shift Char/Cam")
split.operator(AS_OT_shift_anim.bl_idname, text="Shift Multi").multi_assets = True
col.separator()
# Create actions.
valid_collections_count = len(opsdata.get_valid_collections(context))
row = col.row(align=True)
row.operator(
AS_OT_create_actions.bl_idname, text=f"Create {valid_collections_count} actions"
)
class AS_PT_view3d_scene(bpy.types.Panel):
"""
Animation Setup scene operators.
"""
bl_category = "Anim Setup"
bl_label = "Scene"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_order = 13
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
# Exclude collections.
row = layout.row(align=True)
row.operator(
AS_OT_exclude_colls.bl_idname, text="Exclude Collections"
)
# ---------REGISTER ----------.
classes = [
AS_PT_view3d_general,
AS_PT_view3d_animation_and_actions,
AS_PT_view3d_scene,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)