2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import importlib
from ..context import ops, ui
# ---------REGISTER ----------.
def reload():
global ops
global ui
ops = importlib.reload(ops)
ui = importlib.reload(ui)
def register():
ops.register()
ui.register()
def unregister():
ui.unregister()
ops.unregister()
@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from .. import cache, bkglobals
# Category values are defined in enum props.py KITSU_property_group_scene under category
def is_edit_context():
return bpy.context.scene.kitsu.category == "EDIT"
def is_sequence_context():
return bpy.context.scene.kitsu.category == "SEQ"
def is_asset_context():
return bpy.context.scene.kitsu.category == "ASSET"
def is_shot_context():
return bpy.context.scene.kitsu.category == "SHOT"
def active_project_row(layout: bpy.types.UILayout) -> bpy.types.UILayout:
project_active = cache.project_active_get()
row = layout.row(align=True)
if not project_active:
row.enabled = False
return row
def active_episode_row(layout: bpy.types.UILayout) -> None:
episode_active = cache.episode_active_get()
project_active = cache.project_active_get()
row = active_project_row(layout)
if project_active.production_type == bkglobals.KITSU_TV_PROJECT and not episode_active:
row.enabled = False
return row
def draw_episode_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_project_row(layout)
row.prop(context.scene.kitsu, "episode_active_name")
def draw_sequence_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_episode_row(layout)
row.prop(context.scene.kitsu, "sequence_active_name")
def draw_asset_type_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_project_row(layout)
row.prop(context.scene.kitsu, "asset_type_active_name")
def draw_shot_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_episode_row(layout)
row.prop(context.scene.kitsu, "shot_active_name")
def draw_asset_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_project_row(layout)
row.prop(context.scene.kitsu, "asset_active_name")
def draw_edit_selector(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
row = active_project_row(layout)
row.prop(context.scene.kitsu, "edit_active_name")
def draw_task_type_selector(context: bpy.types.Context, layout: bpy.types.UILayout):
layout.prop(context.scene.kitsu, "task_type_active_name")
def get_versioned_file_basename(name: str) -> str:
"""Removes the version suffix from a file name if it exists and returns basename"""
return name.split(bkglobals.DELIMITER + "v")[0]
+343
View File
@@ -0,0 +1,343 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Dict, Optional, Set
from pathlib import Path
import bpy
from .. import bkglobals, cache, util, prefs
from ..logger import LoggerFactory
from ..types import TaskType, AssetType
from ..context import core as context_core
logger = LoggerFactory.getLogger()
class KITSU_OT_con_productions_load(bpy.types.Operator):
"""
Gets all productions that are available in server and let's user select. Invokes a search Popup (enum_prop) on click.
"""
bl_idname = "kitsu.con_productions_load"
bl_label = "Productions Load"
bl_property = "enum_prop"
bl_description = "Sets active project"
enum_prop: bpy.props.EnumProperty(items=cache.get_projects_enum_list) # type: ignore
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return prefs.session_auth(context)
def execute(self, context: bpy.types.Context) -> Set[str]:
# Store vars to check if project / seq / shot changed.
project_prev_id = cache.project_active_get().id
# Update kitsu metadata.
cache.project_active_set_by_id(context, self.enum_prop)
# Clear active shot when sequence changes.
if self.enum_prop != project_prev_id:
cache.sequence_active_reset(context)
cache.episode_active_reset(context)
cache.asset_type_active_reset(context)
cache.shot_active_reset(context)
cache.asset_active_reset(context)
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class KITSU_OT_con_detect_context(bpy.types.Operator):
bl_idname = "kitsu.con_detect_context"
bl_label = "Detect Context"
bl_description = "Auto detects context by looking at file path"
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(
prefs.session_auth(context) and cache.project_active_get() and bpy.data.filepath
)
def execute(self, context: bpy.types.Context) -> Set[str]:
# Update kitsu metadata.
filepath = Path(bpy.data.filepath)
active_project = cache.project_active_get()
kitsu_props = context.scene.kitsu
addon_prefs = context.preferences.addons['.'.join(__package__.split('.')[:-1])].preferences
is_edit_type = str(filepath.resolve()).startswith(str(prefs.project_root_dir_get(context) / addon_prefs.edit_dir_name))
if is_edit_type:
kitsu_props.category = "EDIT"
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
episode = active_project.get_episode_by_name(filepath.parents[0].name)
if episode:
kitsu_props.episode_active_name = episode.name
kitsu_props.edit_active_name = context_core.get_versioned_file_basename(filepath.stem)
kitsu_props.task_type_active_name = bkglobals.EDIT_TASK_TYPE
util.ui_redraw()
return {"FINISHED"}
# TODO REFACTOR THIS WHOLE THING, BAD HACK
# Path is different for tvshow
if (
active_project.production_type == bkglobals.KITSU_TV_PROJECT
and filepath.parents[3].name == addon_prefs.shot_dir_name
):
episode = active_project.get_episode_by_name(filepath.parents[2].name)
category = filepath.parents[3].name
else:
episode = None
category = filepath.parents[2].name
item_group = filepath.parents[1].name
item = filepath.parents[0].name
item_task_type = filepath.stem.split(bkglobals.DELIMITER)[-1]
# Sanity check that the folder struture is correct depending on the type.
is_shot_type = category == addon_prefs.shot_dir_name
is_seq_type = category == addon_prefs.seq_dir_name
is_asset_type = category == addon_prefs.asset_dir_name
if not is_shot_type and not is_seq_type and not is_asset_type:
self.report(
{"ERROR"},
(
f"Expected '{addon_prefs.shot_dir_name}' or '{addon_prefs.asset_dir_name}' 3 folders up. "
f"Got: '{filepath.parents[2].as_posix()}' instead. "
"Blend file might not be saved in project structure"
),
)
return {"CANCELLED"}
if is_shot_type or is_seq_type:
# TODO: check if frame range update gets triggered.
# Set category.
if is_shot_type:
kitsu_props.category = "SHOT"
task_mapping = bkglobals.SHOT_TASK_MAPPING
else:
kitsu_props.category = "SEQ"
task_mapping = bkglobals.SEQ_TASK_MAPPING
if episode:
kitsu_props.episode_active_name = episode.name
# Detect and load sequence.
sequence = active_project.get_sequence_by_name(item_group, episode)
if not sequence:
self.report({"ERROR"}, f"Failed to find sequence: '{item_group}' on server")
return {"CANCELLED"}
kitsu_props.sequence_active_name = sequence.name
if is_shot_type:
# Detect and load shot.
shot = active_project.get_shot_by_name(sequence, item)
if not shot:
self.report({"ERROR"}, f"Failed to find shot: '{item}' on server")
return {"CANCELLED"}
kitsu_props.shot_active_name = shot.name
# Detect and load shot task type.
kitsu_task_type_name = self._find_in_mapping(
item_task_type, task_mapping, "shot task type"
)
if not kitsu_task_type_name:
return {"CANCELLED"}
task_type = TaskType.by_name(kitsu_task_type_name)
if not task_type:
self.report(
{"ERROR"},
f"Failed to find task type: '{kitsu_task_type_name}' on server",
)
return {"CANCELLED"}
kitsu_props.task_type_active_name = task_type.name
elif is_asset_type:
# Set category.
kitsu_props.category = "ASSET"
# Detect and load asset type.
kitsu_asset_type_name = self._find_in_mapping(
item_group, bkglobals.ASSET_TYPE_MAPPING, "asset type"
)
if not kitsu_asset_type_name:
return {"CANCELLED"}
asset_type = AssetType.by_name(kitsu_asset_type_name)
if not asset_type:
self.report(
{"ERROR"},
f"Failed to find asset type: '{kitsu_asset_type_name}' on server",
)
return {"CANCELLED"}
kitsu_props.asset_type_active_name = asset_type.name
# Detect and load asset.
asset = active_project.get_asset_by_name(item)
if not asset:
self.report({"ERROR"}, f"Failed to find asset: '{item}' on server")
return {"CANCELLED"}
kitsu_props.asset_active_name = asset.name
# If split == 1 then filepath has no task type in name, skip asset task_type
if len(filepath.stem.split(bkglobals.DELIMITER)) > 1:
# Detect and load asset task_type.
kitsu_task_type_name = self._find_in_mapping(
item_task_type, bkglobals.ASSET_TASK_MAPPING, "task type"
)
if not kitsu_task_type_name:
return {"CANCELLED"}
task_type = TaskType.by_name(kitsu_task_type_name)
if not task_type:
self.report(
{"ERROR"},
f"Failed to find task type: '{kitsu_task_type_name}' on server",
)
return {"CANCELLED"}
kitsu_props.task_type_active_name = task_type.name
util.ui_redraw()
self.report({"INFO"}, f"Context Successfully Set!")
return {"FINISHED"}
def _find_in_mapping(
self, key: str, mapping: Dict[str, str], entity_type: str
) -> Optional[str]:
if not key in mapping:
self.report(
{"ERROR"},
f"Failed to find {entity_type}: '{key}' in {entity_type} remapping",
)
return None
return mapping[key]
class KITSU_OT_con_set_asset(bpy.types.Operator):
bl_idname = "kitsu.con_set_asset"
bl_label = "Set Kitsu Asset"
bl_description = (
"Mark the current file & target collection as an Asset on Kitsu Server "
"Assets marked with this method will be automatically loaded by the "
"Shot Builder, if the Asset is casted to the buider's target shot"
)
_published_file_path: Path = None
use_asset_pipeline_publish: bpy.props.BoolProperty( # type: ignore
name="Use Asset Pipeline Publish",
description=(
"Find the Publish of this file in the 'Publish' folder and use it's filepath for Kitsu Asset`"
"Selected Collection must be named exactly the same between current file and Publish"
),
default=False,
)
@classmethod
def poll(cls, context):
kitsu_props = context.scene.kitsu
if bpy.data.filepath == "":
cls.poll_message_set("Blend file must be saved")
return False
if not bpy.data.filepath.startswith(str(prefs.project_root_dir_get(context))):
cls.poll_message_set("Blend file must be saved in project structure")
return False
if not context_core.is_asset_context():
cls.poll_message_set("Kitsu Context panel must be set to 'Asset'")
return False
if kitsu_props.asset_type_active_name == "":
cls.poll_message_set("Asset Type must be set")
return False
if kitsu_props.asset_active_name == "":
cls.poll_message_set("Asset must be set")
return False
if not kitsu_props.asset_col:
cls.poll_message_set("Asset Collection must be set")
return False
return True
def is_asset_pipeline_enabled(self, context) -> bool:
for addon in context.preferences.addons:
if addon.module == "asset_pipeline":
return True
return False
def is_asset_pipeline_folder(self, context) -> bool:
current_folder = Path(bpy.data.filepath).parent
return current_folder.joinpath("task_layers.json").exists()
def get_asset_pipeline_publish(self, context) -> Path:
from asset_pipeline.merge.publish import find_latest_publish
return find_latest_publish(Path(bpy.data.filepath))
def invoke(self, context, event):
if self.is_asset_pipeline_enabled(context) and self.is_asset_pipeline_folder(context):
self._published_file_path = self.get_asset_pipeline_publish(context)
if self._published_file_path.exists():
self.use_asset_pipeline_publish = True
wm = context.window_manager
return wm.invoke_props_dialog(self)
return self.execute(context)
def draw(self, context):
layout = self.layout
relative_path = self._published_file_path.relative_to(Path(bpy.data.filepath).parent)
box = layout.box()
box.enabled = self.use_asset_pipeline_publish
box.label(text=f"//{str(relative_path)}")
layout.prop(self, "use_asset_pipeline_publish")
def execute(self, context):
project_root = prefs.project_root_dir_get(context)
if self.use_asset_pipeline_publish:
relative_path = self._published_file_path.relative_to(project_root)
else:
relative_path = Path(bpy.data.filepath).relative_to(project_root)
blender_asset = context.scene.kitsu.asset_col
kitsu_asset = cache.asset_active_get()
if not kitsu_asset:
self.report({"ERROR"}, "Failed to find active Kitsu Asset")
return {"CANCELLED"}
kitsu_asset.set_asset_path(str(relative_path), blender_asset.name)
self.report(
{"INFO"},
f"Kitsu Asset '{kitsu_asset.name}' set to Collection '{blender_asset.name}' at path '{relative_path}'",
)
return {"FINISHED"}
# ---------REGISTER ----------.
classes = [
KITSU_OT_con_productions_load,
KITSU_OT_con_detect_context,
KITSU_OT_con_set_asset,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+156
View File
@@ -0,0 +1,156 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from ..context import core as context_core
from .. import cache, prefs, ui, bkglobals
from ..context.ops import KITSU_OT_con_detect_context, KITSU_OT_con_set_asset
class KITSU_PT_vi3d_context(bpy.types.Panel):
"""
Panel in 3dview that enables browsing through backend data structure.
Thought of as a menu to setup a context by selecting active production
active sequence, shot etc.
"""
bl_category = "Kitsu"
bl_label = "Context"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
bl_order = 20
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return prefs.session_auth(context)
@classmethod
def poll_error(cls, context: bpy.types.Context) -> bool:
project_active = cache.project_active_get()
return bool(not project_active)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
project_active = cache.project_active_get()
episode_active = cache.episode_active_get()
# Catch errors
if self.poll_error(context):
box = ui.draw_error_box(layout)
if not project_active:
ui.draw_error_active_project_unset(box)
# Production
layout.row().label(text=f"Production: {project_active.name}")
layout.row(align=True)
row = layout.row(align=True)
row.label(text="Browser", icon="FILEBROWSER")
# Detect Context
row.operator(
KITSU_OT_con_detect_context.bl_idname,
icon="FILE_REFRESH",
text="",
emboss=False,
)
flow = layout.grid_flow(
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
)
col = flow.column()
# Entity context
col.prop(context.scene.kitsu, "category")
if not prefs.session_auth(context) or not project_active:
row.enabled = False
# Episode selector
if project_active.production_type == bkglobals.KITSU_TV_PROJECT:
context_core.draw_episode_selector(context, col)
# Sequence selector (if context is Sequence)
if context_core.is_sequence_context():
context_core.draw_sequence_selector(context, col)
# Shot selector
if context_core.is_shot_context():
context_core.draw_sequence_selector(context, col)
context_core.draw_shot_selector(context, col)
# AssetType selector (if context is Asset)
if context_core.is_asset_context():
context_core.draw_asset_type_selector(context, col)
context_core.draw_asset_selector(context, col)
if context_core.is_edit_context():
context_core.draw_edit_selector(context, col)
# Task Type selector
context_core.draw_task_type_selector(context, col)
if context.scene.kitsu_error.frame_range:
box = ui.draw_error_box(layout)
ui.draw_error_frame_range_outdated(box)
class KITSU_PT_set_asset(bpy.types.Panel):
"""
Panel in 3dview that enables browsing through backend data structure.
Thought of as a menu to setup a context by selecting active production
active sequence, shot etc.
"""
bl_category = "Kitsu"
bl_label = "Set Asset"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
bl_order = 25
bl_parent_id = "KITSU_PT_vi3d_context"
@classmethod
def poll(cls, context):
return context_core.is_asset_context()
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
col = layout.column()
col.prop(context.scene.kitsu, "asset_col")
col.operator(KITSU_OT_con_set_asset.bl_idname)
class KITSU_PT_comp_context(KITSU_PT_vi3d_context):
bl_space_type = "NODE_EDITOR"
class KITSU_PT_editorial_context(KITSU_PT_vi3d_context):
bl_space_type = "SEQUENCE_EDITOR"
# ---------REGISTER ----------.
# Classes that inherit from another need to be registered first for some reason.
classes = [
KITSU_PT_comp_context,
KITSU_PT_editorial_context,
KITSU_PT_vi3d_context,
KITSU_PT_set_asset,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)