2025-12-01
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user