2026-03-11_4
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"last_check": "2026-03-10 16:36:48.530366",
|
||||
"last_check": "2026-03-06 09:35:09.644798",
|
||||
"backup_date": "December-11-2025",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
bl_info = {
|
||||
"name": "Side Shelf",
|
||||
"author": "Jesse Doyle",
|
||||
"blender": (3, 2, 0),
|
||||
"description": "E Shelf on the right side N panel",
|
||||
"category": "Side Tabs"
|
||||
}
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
|
||||
|
||||
class NPanel(bpy.types.Panel):
|
||||
"""Creates a Panel in the 3D view Tools panel"""
|
||||
bl_idname = "TEST_PT_Panel"
|
||||
bl_label = "Test"
|
||||
bl_category = "Shelf"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_context = "objectmode"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
layout = self.layout
|
||||
pcoll = preview_collections["main"]
|
||||
|
||||
row = layout.row()
|
||||
triangle_icon = pcoll["triangle"]
|
||||
square_icon = pcoll["square"]
|
||||
row.scale_x = 10.0
|
||||
row.scale_y = 8.0
|
||||
|
||||
row.operator("mesh.primitive_cube_add", text="", icon_value=triangle_icon.icon_id)
|
||||
row.operator("mesh.primitive_uv_sphere_add", text="", icon_value=square_icon.icon_id,scale=10)
|
||||
|
||||
|
||||
# global variable to store icons in
|
||||
# custom_icons = None
|
||||
|
||||
# def register():
|
||||
# global custom_icons
|
||||
# custom_icons = bpy.utils.previews.new()
|
||||
|
||||
# for z in list_raw:
|
||||
# custom_icons.load(z[:-4], os.path.join(directory, z), 'IMAGE')
|
||||
|
||||
# bpy.utils.register_class(Panel)
|
||||
|
||||
# def unregister():
|
||||
# global custom_icons
|
||||
# bpy.utils.previews.remove(custom_icons)
|
||||
|
||||
preview_collections = {}
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
# Note that preview collections returned by bpy.utils.previews
|
||||
# are regular py objects - you can use them to store custom data.
|
||||
pcoll = bpy.utils.previews.new()
|
||||
|
||||
# path to the folder where the icon is
|
||||
# the path is calculated relative to this py file inside the addon folder
|
||||
|
||||
directory = os.path.join(os.path.dirname(__file__), "Data", "icons")
|
||||
list_raw = []
|
||||
onlyfiles = [f for f in listdir(directory) if isfile(join(directory, f))]
|
||||
|
||||
for f in onlyfiles:
|
||||
if f[-4:] == ".png":
|
||||
list_raw.append(f)
|
||||
|
||||
for z in list_raw:
|
||||
|
||||
# load a preview thumbnail of a file and store in the previews collection
|
||||
pcoll.load(z[:-4], os.path.join(directory, z), 'IMAGE')
|
||||
|
||||
preview_collections["main"] = pcoll
|
||||
|
||||
bpy.utils.register_class(NPanel)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
for pcoll in preview_collections.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
preview_collections.clear()
|
||||
|
||||
bpy.utils.unregister_class(NPanel)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -1,2 +0,0 @@
|
||||
# Blender Kitsu
|
||||
blender-kitsu is an add-on to interact with Kitsu from within Blender. It also has features that are not directly related to Kitsu but support certain aspects of the Blender Studio Pipeline. You can find the documentation [here](https://studio.blender.org/tools/addons/blender_kitsu).
|
||||
@@ -1,114 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from . import dependencies
|
||||
|
||||
dependencies.preload_modules()
|
||||
|
||||
from . import (
|
||||
shot_builder,
|
||||
render_review,
|
||||
lookdev,
|
||||
bkglobals,
|
||||
types,
|
||||
cache,
|
||||
models,
|
||||
playblast,
|
||||
propsdata,
|
||||
props,
|
||||
prefs,
|
||||
sqe,
|
||||
util,
|
||||
generic,
|
||||
auth,
|
||||
context,
|
||||
anim,
|
||||
tasks,
|
||||
ui,
|
||||
edit,
|
||||
)
|
||||
|
||||
|
||||
from .logger import LoggerFactory, LoggerLevelManager
|
||||
|
||||
logger = LoggerFactory.getLogger(__name__)
|
||||
|
||||
bl_info = {
|
||||
"name": "Blender Kitsu",
|
||||
"author": "Paul Golter",
|
||||
"description": "Blender addon to interact with Kitsu",
|
||||
"blender": (2, 93, 0),
|
||||
"version": (0, 1, 6),
|
||||
"location": "View3D",
|
||||
"warning": "",
|
||||
"doc_url": "",
|
||||
"tracker_url": "",
|
||||
"category": "Generic",
|
||||
}
|
||||
|
||||
_need_reload = "props" in locals()
|
||||
|
||||
if _need_reload:
|
||||
import importlib
|
||||
|
||||
lookdev.reload()
|
||||
bkglobals = importlib.reload(bkglobals)
|
||||
cache = importlib.reload(cache)
|
||||
types = importlib.reload(types)
|
||||
models = importlib.reload(models)
|
||||
playblast = importlib.reload(playblast)
|
||||
propsdata = importlib.reload(propsdata)
|
||||
props = importlib.reload(props)
|
||||
prefs = importlib.reload(prefs)
|
||||
ui = importlib.reload(ui)
|
||||
sqe.reload()
|
||||
util = importlib.reload(util)
|
||||
generic.reload()
|
||||
auth.reload()
|
||||
context.reload()
|
||||
tasks.reload()
|
||||
anim.reload()
|
||||
edit.reload()
|
||||
|
||||
|
||||
def register():
|
||||
lookdev.register()
|
||||
prefs.register()
|
||||
cache.register()
|
||||
props.register()
|
||||
sqe.register()
|
||||
generic.register()
|
||||
auth.register()
|
||||
context.register()
|
||||
# tasks.register()
|
||||
playblast.register()
|
||||
anim.register()
|
||||
shot_builder.register()
|
||||
render_review.register()
|
||||
edit.register()
|
||||
|
||||
LoggerLevelManager.configure_levels()
|
||||
logger.info("Registered blender-kitsu")
|
||||
|
||||
|
||||
def unregister():
|
||||
anim.unregister()
|
||||
# tasks.unregister()
|
||||
context.unregister()
|
||||
auth.unregister()
|
||||
generic.unregister()
|
||||
sqe.unregister()
|
||||
props.unregister()
|
||||
cache.unregister()
|
||||
prefs.unregister()
|
||||
lookdev.unregister()
|
||||
playblast.unregister()
|
||||
shot_builder.unregister()
|
||||
render_review.unregister()
|
||||
edit.unregister()
|
||||
LoggerLevelManager.restore_levels()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -1,29 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from . import opsdata, ops, ui
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def reload():
|
||||
global opsdata
|
||||
global ops
|
||||
global ui
|
||||
|
||||
opsdata = importlib.reload(opsdata)
|
||||
ops = importlib.reload(ops)
|
||||
ui = importlib.reload(ui)
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
ui.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
@@ -1,504 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import List, Set, Tuple
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import cache, util
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
from . import opsdata
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_anim_quick_duplicate(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_quick_duplicate"
|
||||
bl_label = "Quick Duplicate"
|
||||
bl_description = (
|
||||
"Duplicate the active collection and add it to the "
|
||||
"output collection of the current scene"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
act_coll = context.view_layer.active_layer_collection.collection
|
||||
|
||||
return bool(
|
||||
cache.shot_active_get()
|
||||
and context.view_layer.active_layer_collection.collection
|
||||
and not opsdata.is_item_local(act_coll)
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
act_coll = context.view_layer.active_layer_collection.collection
|
||||
amount = context.window_manager.kitsu.quick_duplicate_amount
|
||||
|
||||
if not act_coll:
|
||||
self.report({"ERROR"}, f"No collection selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if output colletion exists in scene.
|
||||
output_coll_name = cache.output_collection_name_get()
|
||||
try:
|
||||
output_coll = bpy.data.collections[output_coll_name]
|
||||
|
||||
except KeyError:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Missing output collection: {output_coll_name}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Get ref coll.
|
||||
ref_coll = opsdata.get_ref_coll(act_coll)
|
||||
|
||||
for i in range(amount):
|
||||
# Create library override.
|
||||
coll = ref_coll.override_hierarchy_create(
|
||||
context.scene, context.view_layer, reference=act_coll
|
||||
)
|
||||
|
||||
# Set color tag to be the same.
|
||||
coll.color_tag = act_coll.color_tag
|
||||
|
||||
# Link coll in output collection.
|
||||
if coll not in list(output_coll.children):
|
||||
output_coll.children.link(coll)
|
||||
|
||||
# Report.
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Created {amount} Duplicates of: {act_coll.name} and added to {output_coll.name}",
|
||||
)
|
||||
|
||||
util.ui_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_anim_check_action_names(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_check_action_names"
|
||||
bl_label = "Check Action Names "
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Inspect all action names of .blend file and check "
|
||||
"if they follow the Blender Studio naming convention"
|
||||
)
|
||||
wrong: List[Tuple[bpy.types.Action, str]] = []
|
||||
created: List[bpy.types.Action] = []
|
||||
empty_actions: List[bpy.types.Action] = []
|
||||
cleanup_empty_actions: bpy.props.BoolProperty(
|
||||
name="Delete Empty Action Data-Blocks",
|
||||
default=False,
|
||||
description="Remove any empty action data-blocks, actions that have 0 Fcurves/Keyframes. Even if the action has a fake user assigned to it",
|
||||
)
|
||||
|
||||
# List of tuples that contains the action on index 0 with the wrong name
|
||||
# and the name it should have on index 1.
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not cache.shot_active_get():
|
||||
cls.poll_message_set("No Shot selected")
|
||||
return False
|
||||
if not cache.task_type_active_get():
|
||||
cls.poll_message_set("No Task Type selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_action(self, action_name: str):
|
||||
if bpy.data.actions.get(action_name):
|
||||
return bpy.data.actions.get(action_name)
|
||||
else:
|
||||
new_action = bpy.data.actions.new(action_name)
|
||||
self.created.append(new_action)
|
||||
return new_action
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
existing_action_names = [a.name for a in bpy.data.actions]
|
||||
failed = []
|
||||
succeeded = []
|
||||
removed = []
|
||||
|
||||
# Clean-up Empty Actions
|
||||
if self.cleanup_empty_actions:
|
||||
for action in self.empty_actions:
|
||||
removed.append(action.name)
|
||||
action.use_fake_user = False
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
# Rename actions.
|
||||
for action, name in self.wrong:
|
||||
if name in existing_action_names:
|
||||
logger.warning(
|
||||
"Failed to rename action %s to %s. Action with that name already exists",
|
||||
action.name,
|
||||
name,
|
||||
)
|
||||
failed.append(action)
|
||||
continue
|
||||
|
||||
old_name = action.name
|
||||
action.name = name
|
||||
action.use_fake_user = True
|
||||
existing_action_names.append(action.name)
|
||||
succeeded.append(action)
|
||||
logger.info("Renamed action %s to %s", old_name, action.name)
|
||||
|
||||
# Report.
|
||||
report_str = f"Renamed actions: {len(succeeded)}"
|
||||
report_state = "INFO"
|
||||
|
||||
if self.cleanup_empty_actions:
|
||||
report_str += f" | Removed Empty Actions: {len(removed)}"
|
||||
if failed:
|
||||
report_state = "WARNING"
|
||||
report_str += f" | Rename Failed: {len(failed)}"
|
||||
if len(self.created) != 0:
|
||||
report_str += f" | Created Actions: {len(self.created)}"
|
||||
|
||||
self.report(
|
||||
{report_state},
|
||||
report_str,
|
||||
)
|
||||
|
||||
# Clear action names cache.
|
||||
opsdata.action_names_cache.clear()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
shot_active = cache.shot_active_get()
|
||||
self.wrong.clear()
|
||||
self.created.clear()
|
||||
no_action = []
|
||||
correct = []
|
||||
|
||||
# Clear action names cache.
|
||||
opsdata.action_names_cache.clear()
|
||||
opsdata.action_names_cache.extend([a.name for a in bpy.data.actions])
|
||||
|
||||
output_col_name = cache.output_collection_name_get()
|
||||
output_col = context.scene.collection.children.get(output_col_name)
|
||||
|
||||
if not output_col:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Missing output collection: {output_col_name}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Find all asset collections in .blend.
|
||||
asset_colls = opsdata.find_asset_collections(output_col)
|
||||
|
||||
if not asset_colls:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Failed to find any asset collections",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Collect Empty Actions
|
||||
self.empty_actions = []
|
||||
for action in bpy.data.actions:
|
||||
if len(action.fcurves) == 0:
|
||||
self.empty_actions.append(action)
|
||||
|
||||
# Find rig of each asset collection.
|
||||
asset_rigs: List[Tuple[bpy.types.Collection, bpy.types.Armature]] = []
|
||||
for coll in asset_colls:
|
||||
rigs = opsdata.find_rig(coll, log=False)
|
||||
if rigs:
|
||||
for rig in rigs:
|
||||
asset_rigs.append((coll, rig))
|
||||
|
||||
if not asset_rigs:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Failed to find any valid rigs",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# For each rig check the current action name if it matches the convention.
|
||||
for coll, rig in asset_rigs:
|
||||
if not rig.animation_data or not rig.animation_data.action:
|
||||
logger.info("%s has no animation data", rig.name)
|
||||
no_action.append(rig)
|
||||
continue
|
||||
|
||||
if rig.animation_data.action in self.empty_actions:
|
||||
continue
|
||||
|
||||
action_name_should = opsdata.gen_action_name(rig, coll, shot_active)
|
||||
action_name_is = rig.animation_data.action.name
|
||||
|
||||
# If action name does not follow convention append it to wrong list.
|
||||
if action_name_is != action_name_should:
|
||||
logger.warning(
|
||||
"Action %s should be named %s", action_name_is, action_name_should
|
||||
)
|
||||
self.wrong.append((rig.animation_data.action, action_name_should))
|
||||
|
||||
# Extend action_names_cache list so any follow up items in loop can
|
||||
# access that information and adjust postfix accordingly.
|
||||
opsdata.action_names_cache.append(action_name_should)
|
||||
continue
|
||||
|
||||
# Action name of rig is correct.
|
||||
correct.append(rig)
|
||||
|
||||
if not self.wrong and self.empty_actions == []:
|
||||
self.report({"INFO"}, "All actions names are correct, no empty actions found")
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Checked Rigs: {len(asset_rigs)} | Wrong Actions {len(correct)} | Correct Actions: {len(correct)} | No Actions: {len(no_action)}",
|
||||
)
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for action, name in self.wrong:
|
||||
row = layout.row()
|
||||
row.label(text=action.name)
|
||||
row.label(text="", icon="FORWARD")
|
||||
row.label(text=name)
|
||||
layout.prop(
|
||||
self,
|
||||
"cleanup_empty_actions",
|
||||
text=f"Delete {len(self.empty_actions)} Empty Action Data-Blocks",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_OT_anim_enforce_naming_convention(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_enforce_naming_convention"
|
||||
bl_label = "Set Name Conventions"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Fix Naming of Scene, Output Collection, Actions, and optionally Find and remove a given string"
|
||||
|
||||
remove_str: bpy.props.StringProperty(
|
||||
name="Find and Replace",
|
||||
default="",
|
||||
)
|
||||
rename_scene: bpy.props.BoolProperty(name="Rename Scene", default=True)
|
||||
rename_output_col: bpy.props.BoolProperty(
|
||||
name="Rename Output Collection", default=True
|
||||
)
|
||||
find_replace: bpy.props.BoolProperty(
|
||||
name="Find and Remove",
|
||||
default=False,
|
||||
description="Remove this string from Collection, Object and Object Data names. Used to remove suffixes from file names such as '.001'",
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "rename_scene")
|
||||
layout.prop(self, "rename_actions")
|
||||
layout.prop(self, "rename_output_col")
|
||||
layout.prop(self, "find_replace")
|
||||
if self.find_replace:
|
||||
layout.prop(self, "remove_str")
|
||||
|
||||
def rename_datablock(self, data_block, replace: str):
|
||||
# Return Early if data_block is linked but not overriden
|
||||
if data_block is None or data_block.library is not None:
|
||||
return
|
||||
if replace in data_block.name:
|
||||
data_block.name = data_block.name.replace(replace, "")
|
||||
return data_block.name
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
shot_base_name = bpy.path.basename(bpy.data.filepath).replace(".anim.blend", "")
|
||||
scene_col = context.scene.collection
|
||||
anim_suffix = "anim.output"
|
||||
|
||||
if self.find_replace:
|
||||
for col in scene_col.children_recursive:
|
||||
self.rename_datablock(col, self.remove_str)
|
||||
for obj in col.objects:
|
||||
self.rename_datablock(obj, self.remove_str)
|
||||
self.rename_datablock(obj.data, self.remove_str)
|
||||
|
||||
if self.rename_output_col:
|
||||
output_cols = [
|
||||
col
|
||||
for col in context.scene.collection.children_recursive
|
||||
if anim_suffix in col.name
|
||||
]
|
||||
if len(output_cols) != 1:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Animation Output Collection could not be found",
|
||||
)
|
||||
|
||||
return {"CANCELLED"}
|
||||
output_col = output_cols[0]
|
||||
output_col.name = f"{shot_base_name}.{anim_suffix}"
|
||||
|
||||
# Rename Scene
|
||||
if self.rename_scene:
|
||||
context.scene.name = f"{shot_base_name}.anim"
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Naming Conventions Enforced",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_unlink_collection_with_string(bpy.types.Operator):
|
||||
bl_idname = "kitsu.unlink_collection_with_string"
|
||||
bl_label = "Find and Unlink Collections"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Unlink any Collection with a given name. By default name is 'OVERRIDE_HIDDEN'"
|
||||
)
|
||||
|
||||
remove_collection_string: bpy.props.StringProperty(
|
||||
name="Find in Name",
|
||||
default='OVERRIDE_HIDDEN',
|
||||
description="Search for this string withing current scene's collections. Collection will be unlinked if it matches given string'",
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "remove_collection_string")
|
||||
|
||||
def execute(self, context):
|
||||
scene_cols = context.scene.collection.children
|
||||
cols = [col for col in scene_cols if self.remove_collection_string in col.name]
|
||||
cleaned = False
|
||||
for col in cols:
|
||||
cleaned = True
|
||||
scene_cols.unlink(col)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Removed Collection '{col.name}'",
|
||||
)
|
||||
if not cleaned:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"No Collections found containing name '{self.remove_collection_string}'",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_PG_anim_exclude_coll(bpy.types.PropertyGroup):
|
||||
exclude: bpy.props.BoolProperty(
|
||||
name="Exclude",
|
||||
description="",
|
||||
default=False,
|
||||
override={"LIBRARY_OVERRIDABLE"},
|
||||
)
|
||||
|
||||
|
||||
class KITSU_OT_anim_update_output_coll(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_update_output_coll"
|
||||
bl_label = "Update Output Collection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Scans scene for any collections that are not yet in the output collection"
|
||||
)
|
||||
output_coll = None
|
||||
asset_colls = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
output_coll_name = cache.output_collection_name_get()
|
||||
try:
|
||||
output_coll = bpy.data.collections[output_coll_name]
|
||||
except KeyError:
|
||||
output_coll = None
|
||||
|
||||
return bool(output_coll)
|
||||
|
||||
def invoke(self, context, event):
|
||||
output_coll_name = cache.output_collection_name_get()
|
||||
self.output_coll = bpy.data.collections[output_coll_name]
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def get_collections(self, context):
|
||||
self.asset_colls = opsdata.find_asset_collections_in_scene(context.scene)
|
||||
# Only take parent colls.
|
||||
childs = []
|
||||
for i in range(len(self.asset_colls)):
|
||||
coll = self.asset_colls[i]
|
||||
coll_childs = list(opsdata.traverse_collection_tree(coll))
|
||||
for j in range(i + 1, len(self.asset_colls)):
|
||||
coll_comp = self.asset_colls[j]
|
||||
if coll_comp in coll_childs:
|
||||
childs.append(coll_comp)
|
||||
|
||||
return [coll for coll in self.asset_colls if coll not in childs]
|
||||
|
||||
def draw(self, context):
|
||||
parents = self.get_collections(context)
|
||||
# Must display collections that already exist in output collection so user can exclude them
|
||||
for col in self.output_coll.children:
|
||||
parents.append(col)
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Select Collection to Exclude", icon="OUTLINER_COLLECTION")
|
||||
column = box.column(align=True)
|
||||
for col in parents:
|
||||
column.prop(col.anim_output, "exclude", text=col.name)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
# Clear Out Output Collection before Starting
|
||||
for collection in self.output_coll.children:
|
||||
self.output_coll.children.unlink(collection)
|
||||
bpy.context.view_layer.update()
|
||||
parents = self.get_collections(context)
|
||||
parents = [col for col in parents if not col.anim_output.exclude]
|
||||
for coll in parents:
|
||||
self.output_coll.children.link(coll)
|
||||
logger.info("%s linked in %s", coll.name, self.output_coll.name)
|
||||
|
||||
# Ensure Camera Rig is Linked
|
||||
for coll in [col for col in bpy.data.collections]:
|
||||
if coll.override_library:
|
||||
if (
|
||||
coll.override_library.hierarchy_root.name == "CA-camera_rig"
|
||||
): # TODO Fix this hack to be generic
|
||||
self.output_coll.children.link(coll)
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Found Asset Collections: {len(self.asset_colls)} | Added to output collection: {len(parents)}",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_OT_anim_quick_duplicate,
|
||||
KITSU_OT_anim_check_action_names,
|
||||
KITSU_OT_anim_update_output_coll,
|
||||
KITSU_OT_anim_enforce_naming_convention,
|
||||
KITSU_OT_unlink_collection_with_string,
|
||||
KITSU_PG_anim_exclude_coll,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Collection.anim_output = bpy.props.PointerProperty(
|
||||
type=KITSU_PG_anim_exclude_coll
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
del bpy.types.Collection.anim_output
|
||||
@@ -1,281 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union, Generator
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import bkglobals, util
|
||||
from ..types import Shot
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def create_collection_instance(
|
||||
context: bpy.types.Context,
|
||||
ref_coll: bpy.types.Collection,
|
||||
instance_name: str,
|
||||
) -> bpy.types.Object:
|
||||
# Use empty to instance source collection.
|
||||
instance_obj = bpy.data.objects.new(name=instance_name, object_data=None)
|
||||
instance_obj.instance_collection = ref_coll
|
||||
instance_obj.instance_type = "COLLECTION"
|
||||
|
||||
parent_collection = context.scene.collection
|
||||
parent_collection.objects.link(instance_obj)
|
||||
|
||||
logger.info(
|
||||
"Instanced collection: %s as: %s",
|
||||
ref_coll.name,
|
||||
instance_obj.name,
|
||||
)
|
||||
|
||||
return instance_obj
|
||||
|
||||
|
||||
def find_rig(coll: bpy.types.Collection, log: bool = True) -> Optional[bpy.types.Armature]:
|
||||
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(bkglobals.PREFIX_RIG):
|
||||
continue
|
||||
|
||||
valid_rigs.append(obj)
|
||||
|
||||
if not valid_rigs:
|
||||
return None
|
||||
|
||||
if log:
|
||||
for rig in valid_rigs:
|
||||
logger.info("Found rig: %s", rig.name)
|
||||
return valid_rigs
|
||||
|
||||
|
||||
def find_asset_collections(
|
||||
top_level_col: bpy.types.Collection, log: bool = True
|
||||
) -> List[bpy.types.Collection]:
|
||||
asset_colls: List[bpy.types.Collection] = []
|
||||
for coll in top_level_col.children_recursive:
|
||||
if not is_item_lib_override(coll):
|
||||
continue
|
||||
|
||||
for prefix in bkglobals.ASSET_COLL_PREFIXES:
|
||||
if not coll.name.startswith(prefix):
|
||||
continue
|
||||
|
||||
asset_colls.append(coll)
|
||||
if log:
|
||||
logger.info(
|
||||
"Found asset collections:\n%s", ", ".join([c.name for c in asset_colls])
|
||||
)
|
||||
return asset_colls
|
||||
|
||||
|
||||
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 find_asset_collections_in_scene(
|
||||
scene: bpy.types.Scene, log: bool = True
|
||||
) -> List[bpy.types.Collection]:
|
||||
asset_colls: List[bpy.types.Collection] = []
|
||||
colls: List[bpy.types.Collection] = []
|
||||
|
||||
# Get all collections that are linked in this scene.
|
||||
for coll in scene.collection.children:
|
||||
colls.extend(list(traverse_collection_tree(coll)))
|
||||
|
||||
for coll in colls:
|
||||
for prefix in bkglobals.ASSET_COLL_PREFIXES:
|
||||
if not coll.name.startswith(prefix):
|
||||
continue
|
||||
|
||||
asset_colls.append(coll)
|
||||
if log:
|
||||
logger.info(
|
||||
"Found asset collections:\n%s", ", ".join([c.name for c in asset_colls])
|
||||
)
|
||||
|
||||
return asset_colls
|
||||
|
||||
|
||||
def get_ref_coll(coll: bpy.types.Collection) -> bpy.types.Collection:
|
||||
if not coll.override_library:
|
||||
return coll
|
||||
|
||||
return coll.override_library.reference
|
||||
|
||||
|
||||
def find_asset_name(name: str) -> str:
|
||||
name = _kill_increment_end(name)
|
||||
if name.endswith(bkglobals.SPACE_REPLACER + "rig"):
|
||||
name = name[:-4]
|
||||
return name.split(bkglobals.DELIMITER)[-1] # CH-rex -> 'rex'
|
||||
|
||||
|
||||
def _kill_increment_end(str_value: str) -> str:
|
||||
match = re.search(r"\.\d\d\d$", str_value)
|
||||
if match:
|
||||
return str_value.replace(match.group(0), "")
|
||||
return str_value
|
||||
|
||||
|
||||
def is_multi_asset(asset_name: str) -> bool:
|
||||
if asset_name.startswith("thorn"):
|
||||
return True
|
||||
|
||||
if asset_name.startswith("pine_cone"):
|
||||
return True
|
||||
|
||||
if asset_name.startswith("pee_"):
|
||||
return True
|
||||
|
||||
if asset_name.lower() in bkglobals.MULTI_ASSETS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
action_names_cache: List[str] = []
|
||||
# We need this in order to increment prefixes of duplications of the same asset correctly
|
||||
# gets cleared populated during call of KITSU_OT_anim_check_action_names.
|
||||
_current_asset: str = ""
|
||||
_current_asset_idx: int = 0
|
||||
# We need these two variables to track if we are on the first asset that is currently processed
|
||||
# (if there are multiple ones) because the first one CAN get keep it postfix.
|
||||
|
||||
|
||||
def gen_action_name(
|
||||
armature: bpy.types.Armature, collection: bpy.types.Collection, shot: Shot
|
||||
) -> str:
|
||||
global action_names_cache
|
||||
global _current_asset
|
||||
global _current_asset_idx
|
||||
action_names_cache.sort()
|
||||
|
||||
def _find_postfix(action_name: str) -> Optional[str]:
|
||||
# ANI-lady_bug_A.030_0020_A.v001.
|
||||
split1 = action_name.split(bkglobals.DELIMITER)[-1] # lady_bug_A.030_0020_A.v001
|
||||
split2 = split1.split(".")[0] # lady_bug_A
|
||||
split3 = split2.split(bkglobals.SPACE_REPLACER)[-1] # A
|
||||
if len(split3) == 1:
|
||||
# is postfix
|
||||
# print(f"{action.name} found postfix: {split3}")
|
||||
return split3
|
||||
else:
|
||||
return None
|
||||
|
||||
ref_coll = get_ref_coll(collection)
|
||||
action_prefix = "ANI"
|
||||
asset_name = find_asset_name(ref_coll.name).lower()
|
||||
asset_name = asset_name.replace(".", bkglobals.SPACE_REPLACER)
|
||||
|
||||
# Track on which repition we are of the same asset.
|
||||
if asset_name == _current_asset:
|
||||
_current_asset_idx += 1
|
||||
else:
|
||||
_current_asset_idx = 0
|
||||
_current_asset = asset_name
|
||||
|
||||
version = "v001"
|
||||
shot_name = shot.name
|
||||
has_action = False
|
||||
final_postfix = ""
|
||||
|
||||
# Overwrite version v001 if there is an action which already contains a version.
|
||||
if armature.animation_data:
|
||||
if armature.animation_data.action:
|
||||
has_action = True
|
||||
version = util.get_version(armature.animation_data.action.name) or "v001"
|
||||
|
||||
# Action name for single aset.
|
||||
delimiter = bkglobals.DELIMITER # Currently set to '-'
|
||||
space = bkglobals.SPACE_REPLACER # Currently set to '_'
|
||||
action_name = (
|
||||
f"{action_prefix}{delimiter}{asset_name}{delimiter}{shot_name}{delimiter}{version}"
|
||||
)
|
||||
|
||||
if is_multi_asset(asset_name):
|
||||
existing_postfixes = []
|
||||
|
||||
# Find all actions that relate to the same asset except for the asset.
|
||||
for action_name in action_names_cache:
|
||||
# Skip action that was input as parameter of this function.
|
||||
if has_action and action_name == armature.animation_data.action.name:
|
||||
# Print(f"Skipping action same name: {action_name}").
|
||||
continue
|
||||
|
||||
# print(action_names_cache)
|
||||
if action_name.startswith(f"{action_prefix}{delimiter}{asset_name}"):
|
||||
multi_postfix = _find_postfix(action_name)
|
||||
if multi_postfix:
|
||||
# print(f"Found postfix {multi_postfix} for aseet : {asset_name}")
|
||||
existing_postfixes.append(multi_postfix)
|
||||
|
||||
# print(f"EXISTING: {existing_postfixes}")
|
||||
if existing_postfixes:
|
||||
if _current_asset_idx == 0:
|
||||
# print(f"{asset_name} is first asset can keep postfix")
|
||||
final_postfix = multi_postfix
|
||||
else:
|
||||
# Otherwise increment the postfix by one.
|
||||
existing_postfixes.sort()
|
||||
final_postfix = chr(
|
||||
ord(existing_postfixes[-1]) + 1
|
||||
) # handle postfix == Z > [
|
||||
else:
|
||||
# If there are no existing postfixes the first one is A.
|
||||
final_postfix = "A"
|
||||
|
||||
if has_action:
|
||||
# Overwrite multi_postfix if multi_postfix exists.
|
||||
current_postfix = _find_postfix(armature.animation_data.action.name)
|
||||
|
||||
# If existing action already has a postfix check if that one is in
|
||||
# existing postfixes, if not use the actions post fix.
|
||||
if current_postfix:
|
||||
if current_postfix not in existing_postfixes:
|
||||
final_postfix = current_postfix
|
||||
|
||||
# Action name for multi asset.
|
||||
action_name = f"{action_prefix}{delimiter}{asset_name}{space}{final_postfix}{delimiter}{shot_name}{delimiter}{version}"
|
||||
|
||||
return action_name
|
||||
@@ -1,103 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from . import opsdata
|
||||
import bpy
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import prefs, cache, ui
|
||||
from .ops import (
|
||||
KITSU_OT_anim_quick_duplicate,
|
||||
KITSU_OT_anim_check_action_names,
|
||||
KITSU_OT_anim_update_output_coll,
|
||||
)
|
||||
from ..generic.ops import KITSU_OT_open_path
|
||||
|
||||
|
||||
class KITSU_PT_vi3d_anim_tools(bpy.types.Panel):
|
||||
"""
|
||||
Panel in 3dview that exposes a set of tools that are useful for animation
|
||||
tasks, e.G playblast
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Animation Tools"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 30
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(
|
||||
prefs.session_auth(context)
|
||||
# HACK to accomodate custom task types @ blender studio (Anim3D, Anim2D)
|
||||
and cache.task_type_active_get().name in ['Animation', 'Layout', 'Anim3D', 'Anim2D']
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
# Scene operators.
|
||||
box = layout.box()
|
||||
box.label(text="Scene", icon="SCENE_DATA")
|
||||
|
||||
col = box.column(align=True)
|
||||
col.operator("kitsu.push_frame_range", icon="TRIA_UP")
|
||||
col.operator(
|
||||
"kitsu.pull_frame_range",
|
||||
icon="TRIA_DOWN",
|
||||
)
|
||||
|
||||
# Update output collection.
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_anim_update_output_coll.bl_idname,
|
||||
icon="FILE_REFRESH",
|
||||
)
|
||||
|
||||
# Quick duplicate.
|
||||
act_coll = context.view_layer.active_layer_collection.collection
|
||||
dupli_text = "Duplicate: Select Collection"
|
||||
|
||||
if act_coll:
|
||||
dupli_text = f"Duplicate: {act_coll.name}"
|
||||
|
||||
if act_coll and opsdata.is_item_local(act_coll):
|
||||
dupli_text = f"Duplicate: Select Overwritten Collection"
|
||||
|
||||
split = box.split(factor=0.85, align=True)
|
||||
split.operator(
|
||||
KITSU_OT_anim_quick_duplicate.bl_idname, icon="DUPLICATE", text=dupli_text
|
||||
)
|
||||
split.prop(context.window_manager.kitsu, "quick_duplicate_amount", text="")
|
||||
|
||||
# Check action names.
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_anim_check_action_names.bl_idname,
|
||||
icon="ACTION",
|
||||
text="Check Action Names",
|
||||
)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("kitsu.anim_enforce_naming_convention", icon="SORTALPHA")
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_PT_vi3d_anim_tools,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,27 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..auth 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()
|
||||
@@ -1,145 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
|
||||
import bpy
|
||||
import threading
|
||||
import gazu
|
||||
from .. import cache, prefs
|
||||
|
||||
# TODO: restructure this to not access ops_playblast_data.
|
||||
from ..playblast import opsdata as ops_playblast_data
|
||||
from ..playblast import ops as ops_playblast
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
active_thread = False
|
||||
|
||||
|
||||
class KITSU_OT_session_start(bpy.types.Operator):
|
||||
"""
|
||||
Starts the Session, which is stored in blender_kitsu addon preferences.
|
||||
Authenticates user with server until session ends.
|
||||
Host, email and password are retrieved from blender_kitsu addon preferences.
|
||||
"""
|
||||
|
||||
bl_idname = "kitsu.session_start"
|
||||
bl_label = "Start Kitsu Session"
|
||||
bl_options = {"INTERNAL"}
|
||||
bl_description = (
|
||||
"Logs in to server with the credentials that are defined in the "
|
||||
"addon preferences. Session is valid until Blender closes"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
self.thread_login(context)
|
||||
if not prefs.session_get(context).is_auth():
|
||||
self.report({"ERROR"}, "Login data not correct")
|
||||
logger.error("Login data not correct")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Init cache variables, will skip if cache already initiated.
|
||||
cache.init_cache_variables()
|
||||
|
||||
# Init startup variables, will skip if cache already initiated.
|
||||
cache.init_startup_variables(context)
|
||||
|
||||
# Init playblast version dir model.
|
||||
ops_playblast_data.init_playblast_file_model(context)
|
||||
|
||||
# Check frame range.
|
||||
ops_playblast.load_post_handler_check_frame_range(None)
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_config(self, context: bpy.types.Context) -> Dict[str, str]:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
return {
|
||||
"email": addon_prefs.email,
|
||||
"host": addon_prefs.host,
|
||||
"passwd": addon_prefs.passwd,
|
||||
}
|
||||
|
||||
def kitsu_session_start(self, context):
|
||||
session = prefs.session_get(context)
|
||||
session.set_config(self.get_config(context))
|
||||
try:
|
||||
session_data = session.start()
|
||||
self.report({"INFO"}, f"Logged in as {session_data.user['full_name']}")
|
||||
finally:
|
||||
return
|
||||
|
||||
def thread_login(self, context):
|
||||
global active_thread
|
||||
if active_thread:
|
||||
active_thread._stop()
|
||||
active_thread = threading.Thread(
|
||||
target=self.kitsu_session_start(context), daemon=True
|
||||
)
|
||||
active_thread.start()
|
||||
|
||||
|
||||
class KITSU_OT_session_end(bpy.types.Operator):
|
||||
"""
|
||||
Ends the Session which is stored in blender_kitsu addon preferences.
|
||||
"""
|
||||
|
||||
bl_idname = "kitsu.session_end"
|
||||
bl_label = "End Kitsu Session"
|
||||
bl_options = {"INTERNAL"}
|
||||
bl_description = "Logs active user out"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return prefs.session_auth(context)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
session = prefs.session_get(context)
|
||||
session.end()
|
||||
|
||||
# Clear cache variables.
|
||||
cache.clear_cache_variables()
|
||||
|
||||
# Clear startup variables.
|
||||
cache.clear_startup_variables()
|
||||
|
||||
self.report({"INFO"}, "Logged out")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def auto_login_on_file_open():
|
||||
context = bpy.context
|
||||
session = prefs.session_get(context)
|
||||
if not session.is_auth() and prefs.addon_prefs_get(context).host:
|
||||
bpy.ops.kitsu.session_start()
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_OT_session_start,
|
||||
KITSU_OT_session_end,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
# Note: Since this timer function does not repeat
|
||||
# (because it doesn't return a value)
|
||||
# it automatically un-registers after it runs.
|
||||
# FIXME: XXX This makes Blender hang if there is no Internet connectivity
|
||||
# TODO: Rewrite this, so the 'auto' login happens out of the main thread
|
||||
bpy.app.timers.register(auto_login_on_file_open, first_interval=0.2)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import prefs
|
||||
from ..auth.ops import KITSU_OT_session_end, KITSU_OT_session_start
|
||||
|
||||
|
||||
class KITSU_PT_vi3d_auth(bpy.types.Panel):
|
||||
"""
|
||||
Panel in 3dview that displays email, password and login operator.
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Login"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 10
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
session = prefs.session_get(context)
|
||||
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row(align=True)
|
||||
if not session.is_auth():
|
||||
row.label(text=f"Email: {addon_prefs.email}")
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"Host: {addon_prefs.host}")
|
||||
row = layout.row(align=True)
|
||||
row.operator(KITSU_OT_session_start.bl_idname, text="Login", icon="PLAY")
|
||||
else:
|
||||
row.label(text=f"Logged in: {session.email}")
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_session_end.bl_idname, text="Logout", icon="PANEL_CLOSE"
|
||||
)
|
||||
|
||||
|
||||
class KITSU_PT_sqe_auth(KITSU_PT_vi3d_auth):
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(not prefs.session_auth(context))
|
||||
|
||||
|
||||
class KITSU_PT_comp_auth(KITSU_PT_vi3d_auth):
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(not prefs.session_auth(context))
|
||||
|
||||
|
||||
# ---------REGISTER ----------
|
||||
# classes that inherit from another need to be registered first for some reason
|
||||
classes = [KITSU_PT_comp_auth, KITSU_PT_sqe_auth, KITSU_PT_vi3d_auth]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,88 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
FPS = 24
|
||||
VERSION_PATTERN = r"v\d\d\d"
|
||||
FRAME_START = 101
|
||||
|
||||
# Naming Conventions Set by https://studio.blender.org/tools/naming-conventions/introduction
|
||||
DELIMITER = "-" # Seperates items (e.g."{shot_name}-{shot_task}"")
|
||||
SPACE_REPLACER = "_" # Represents spaces in a single item (e.g. "my shot name" = "my_shot_name")
|
||||
|
||||
ASSET_TASK_MAPPING = {
|
||||
"geometry": "Geometry",
|
||||
"grooming": "Grooming",
|
||||
"modeling": "Modeling",
|
||||
"rigging": "Rigging",
|
||||
"sculpting": "Sculpting",
|
||||
"shading": "Shading",
|
||||
}
|
||||
|
||||
ASSET_TYPE_MAPPING = {
|
||||
"chars": "Character",
|
||||
"fx": "FX",
|
||||
"libs": "Library",
|
||||
"lgt": "Lighting",
|
||||
"props": "Prop",
|
||||
"sets": "Set",
|
||||
}
|
||||
|
||||
SEQ_TASK_MAPPING = {
|
||||
"previs": "Previsualization",
|
||||
"boards": "Boards",
|
||||
}
|
||||
|
||||
SHOT_TASK_MAPPING = {
|
||||
"anim2D": "Anim2D",
|
||||
"anim": "Animation",
|
||||
"comp": "Compositing",
|
||||
"fx": "FX",
|
||||
"layout": "Layout",
|
||||
"lighting": "Lighting",
|
||||
"previz": "Previz",
|
||||
"rendering": "Rendering",
|
||||
"smear_to_mesh": "Smear to mesh",
|
||||
"storyboard": "Storyboard",
|
||||
}
|
||||
|
||||
PREFIX_RIG = "RIG-"
|
||||
|
||||
MULTI_ASSETS = [
|
||||
"sprite",
|
||||
"snail",
|
||||
"spider",
|
||||
"peanut",
|
||||
"peanut_box",
|
||||
"pretzel",
|
||||
"corn_dart",
|
||||
"corn_darts_bag",
|
||||
"meat_stick",
|
||||
"salty_twists_bag",
|
||||
"salt_stick",
|
||||
"salt_stix_package",
|
||||
"briny_bear",
|
||||
"briny_bears_bag",
|
||||
] # list of assets that gets duplicated and therefore follows another naming sheme
|
||||
|
||||
ASSET_COLL_PREFIXES = ["CH-", "PR-", "SE-", "FX-", "EN-"]
|
||||
|
||||
# Kitsu Constants
|
||||
KITSU_TV_PROJECT = 'tvshow'
|
||||
|
||||
# Kitsu Metadata Keys
|
||||
KITSU_FILEPATH_KEY = "filepath"
|
||||
KITSU_COLLECTION_KEY = "collection"
|
||||
|
||||
RES_DIR_PATH = Path(os.path.abspath(__file__)).parent.joinpath("res")
|
||||
|
||||
SCENE_NAME_PLAYBLAST = "playblast_playback"
|
||||
PLAYBLAST_DEFAULT_STATUS = "Todo"
|
||||
|
||||
BUILD_SETTINGS_FILENAME = "settings.json"
|
||||
BUILD_HOOKS_FILENAME = "hooks.py"
|
||||
|
||||
EDIT_TASK_TYPE = "Edit"
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "blender_kitsu"
|
||||
version = "0.1.6"
|
||||
name = "Blender Kitsu"
|
||||
tagline = "Blender integration with the Kitsu production tracker system"
|
||||
maintainer = "Blender Studio"
|
||||
type = "add-on"
|
||||
website = "https://studio.blender.org/tools/addons/blender_kitsu"
|
||||
tags = ["Pipeline"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
copyright = [
|
||||
"2019-2025 Paul Golter & Blender Studio",
|
||||
]
|
||||
|
||||
wheels = [
|
||||
"./wheels/bidict-0.22.1-py3-none-any.whl",
|
||||
"./wheels/gazu-0.9.4-py2.py3-none-any.whl",
|
||||
"./wheels/python_engineio-4.5.1-py3-none-any.whl",
|
||||
"./wheels/python_socketio-5.8.0-py3-none-any.whl",
|
||||
]
|
||||
@@ -1,831 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Any, List, Optional, Union, Dict, Tuple
|
||||
|
||||
import bpy
|
||||
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from .types import (
|
||||
Project,
|
||||
Episode,
|
||||
Edit,
|
||||
Sequence,
|
||||
Shot,
|
||||
Asset,
|
||||
AssetType,
|
||||
TaskType,
|
||||
Task,
|
||||
ProjectList,
|
||||
TaskStatus,
|
||||
Cache,
|
||||
User,
|
||||
)
|
||||
from .logger import LoggerFactory
|
||||
import gazu
|
||||
|
||||
from .util import addon_prefs_get
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
# CACHE VARIABLES
|
||||
# used to cache active entities to prevent a new api request when read only
|
||||
_project_active: Project = Project()
|
||||
_episode_active: Episode = Episode()
|
||||
_sequence_active: Sequence = Sequence()
|
||||
_shot_active: Shot = Shot()
|
||||
_asset_active: Asset = Asset()
|
||||
_edit_active: Edit = Edit()
|
||||
_asset_type_active: AssetType = AssetType()
|
||||
_task_type_active: TaskType = TaskType()
|
||||
_user_active: User = User()
|
||||
_user_all_tasks: List[Task] = []
|
||||
|
||||
_cache_initialized: bool = False
|
||||
_cache_startup_initialized: bool = False
|
||||
|
||||
_episodes_enum_list: List[Tuple[str, str, str]] = []
|
||||
_sequence_enum_list: List[Tuple[str, str, str]] = []
|
||||
_shot_enum_list: List[Tuple[str, str, str]] = []
|
||||
_asset_types_enum_list: List[Tuple[str, str, str]] = []
|
||||
_asset_enum_list: List[Tuple[str, str, str]] = []
|
||||
_projects_enum_list: List[Tuple[str, str, str]] = []
|
||||
_task_types_enum_list: List[Tuple[str, str, str]] = []
|
||||
_task_types_shots_enum_list: List[Tuple[str, str, str]] = []
|
||||
_task_statuses_enum_list: List[Tuple[str, str, str]] = []
|
||||
_user_all_tasks_enum_list: List[Tuple[str, str, str]] = []
|
||||
_all_edits_enum_list: List[Tuple[str, str, str]] = []
|
||||
|
||||
_asset_cache_proj_id: str = ""
|
||||
_episode_cache_proj_id: str = ""
|
||||
_all_shot_tasks_cache_proj_id: str = ""
|
||||
_all_task_type_cache_proj_id: str = ""
|
||||
_seq_cache_proj_id: str = ""
|
||||
_seq_cache_episode_id: str = ""
|
||||
_shot_cache_seq_id: str = ""
|
||||
_task_type_cache_shot_id: str = ""
|
||||
_asset_cache_asset_type_id: str = ''
|
||||
_all_edits_cache_proj_id: str = ""
|
||||
|
||||
|
||||
def user_active_get() -> User:
|
||||
global _user_active
|
||||
|
||||
return _user_active
|
||||
|
||||
|
||||
def project_active_get() -> Project:
|
||||
global _project_active
|
||||
|
||||
return _project_active
|
||||
|
||||
|
||||
def project_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _project_active
|
||||
|
||||
_project_active = Project.by_id(entity_id)
|
||||
addon_prefs_get(context).project_active_id = entity_id
|
||||
logger.debug("Set active project to %s", _project_active.name)
|
||||
|
||||
|
||||
def project_active_reset(context: bpy.types.Context) -> None:
|
||||
global _project_active
|
||||
_project_active = Project()
|
||||
addon_prefs_get(context).project_active_id = ""
|
||||
logger.debug("Reset active project")
|
||||
|
||||
|
||||
def episode_active_get() -> Project:
|
||||
global _episode_active
|
||||
|
||||
return _episode_active
|
||||
|
||||
|
||||
def episode_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _episode_active
|
||||
|
||||
_episode_active = Episode.by_id(entity_id)
|
||||
addon_prefs_get(context).episode_active_id = entity_id
|
||||
logger.debug("Set active episode to %s", _episode_active.name)
|
||||
|
||||
|
||||
def episode_active_reset_entity():
|
||||
global _episode_active
|
||||
_episode_active = Episode()
|
||||
|
||||
|
||||
def episode_active_reset(context: bpy.types.Context) -> None:
|
||||
episode_active_reset_entity()
|
||||
context.scene.kitsu.episode_active_id = ""
|
||||
context.scene.kitsu.episode_active_name = ""
|
||||
logger.debug("Reset active episode")
|
||||
|
||||
|
||||
def sequence_active_get() -> Sequence:
|
||||
return _sequence_active
|
||||
|
||||
|
||||
def sequence_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _sequence_active
|
||||
|
||||
_sequence_active = Sequence.by_id(entity_id)
|
||||
context.scene.kitsu.sequence_active_id = entity_id
|
||||
logger.debug("Set active sequence to %s", _sequence_active.name)
|
||||
|
||||
|
||||
def sequence_active_reset_entity() -> None:
|
||||
global _sequence_active
|
||||
_sequence_active = Sequence()
|
||||
|
||||
|
||||
def sequence_active_reset(context: bpy.types.Context) -> None:
|
||||
sequence_active_reset_entity()
|
||||
context.scene.kitsu.sequence_active_id = ""
|
||||
context.scene.kitsu.sequence_active_name = ""
|
||||
logger.debug("Reset active sequence")
|
||||
|
||||
|
||||
def output_collection_name_get() -> str:
|
||||
active_shot = shot_active_get()
|
||||
task_type = task_type_active_get()
|
||||
output_col_name = active_shot.get_output_collection_name(task_type.get_short_name())
|
||||
return output_col_name
|
||||
|
||||
def shot_active_get() -> Shot:
|
||||
global _shot_active
|
||||
|
||||
return _shot_active
|
||||
|
||||
|
||||
def shot_active_pull_update() -> Shot:
|
||||
global _shot_active
|
||||
Cache.clear_all()
|
||||
shot_active_id = bpy.context.scene.kitsu.shot_active_id
|
||||
_init_cache_entity(shot_active_id, Shot, "_shot_active", "shot")
|
||||
return _shot_active
|
||||
|
||||
|
||||
def shot_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _shot_active
|
||||
|
||||
_shot_active = Shot.by_id(entity_id)
|
||||
context.scene.kitsu.shot_active_id = entity_id
|
||||
logger.debug("Set active shot to %s", _shot_active.name)
|
||||
|
||||
|
||||
def shot_active_reset_entity() -> None:
|
||||
global _shot_active
|
||||
_shot_active = Shot()
|
||||
|
||||
|
||||
def shot_active_reset(context: bpy.types.Context) -> None:
|
||||
shot_active_reset_entity()
|
||||
context.scene.kitsu.shot_active_id = ""
|
||||
context.scene.kitsu.shot_active_name = ""
|
||||
logger.debug("Reset active shot")
|
||||
|
||||
|
||||
def asset_active_get() -> Asset:
|
||||
global _asset_active
|
||||
|
||||
return _asset_active
|
||||
|
||||
|
||||
def asset_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _asset_active
|
||||
|
||||
_asset_active = Asset.by_id(entity_id)
|
||||
context.scene.kitsu.asset_active_id = entity_id
|
||||
logger.debug("Set active asset to %s", _asset_active.name)
|
||||
|
||||
|
||||
def asset_active_reset_entity() -> None:
|
||||
global _asset_active
|
||||
_asset_active = Asset()
|
||||
|
||||
|
||||
def asset_active_reset(context: bpy.types.Context) -> None:
|
||||
asset_active_reset_entity()
|
||||
context.scene.kitsu.asset_active_id = ""
|
||||
context.scene.kitsu.asset_active_name = ""
|
||||
logger.debug("Reset active asset")
|
||||
|
||||
|
||||
def asset_type_active_get() -> AssetType:
|
||||
global _asset_type_active
|
||||
|
||||
return _asset_type_active
|
||||
|
||||
|
||||
def asset_type_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _asset_type_active
|
||||
|
||||
if _asset_type_active != AssetType.by_id(entity_id):
|
||||
asset_active_reset(context)
|
||||
|
||||
_asset_type_active = AssetType.by_id(entity_id)
|
||||
context.scene.kitsu.asset_type_active_id = entity_id
|
||||
logger.debug("Set active asset type to %s", _asset_type_active.name)
|
||||
|
||||
def asset_type_active_reset_entity() -> None:
|
||||
global _asset_type_active
|
||||
_asset_type_active = AssetType()
|
||||
|
||||
|
||||
def asset_type_active_reset(context: bpy.types.Context) -> None:
|
||||
asset_type_active_reset_entity()
|
||||
context.scene.kitsu.asset_type_active_id = ""
|
||||
context.scene.kitsu.asset_type_active_name = ""
|
||||
logger.debug("Reset active asset type")
|
||||
|
||||
|
||||
def task_type_active_get() -> TaskType:
|
||||
global _task_type_active
|
||||
|
||||
return _task_type_active
|
||||
|
||||
|
||||
def task_type_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _task_type_active
|
||||
|
||||
_task_type_active = TaskType.by_id(entity_id)
|
||||
context.scene.kitsu.task_type_active_id = entity_id
|
||||
logger.debug("Set active task type to %s", _task_type_active.name)
|
||||
|
||||
|
||||
def task_type_active_reset_entity() -> None:
|
||||
global _task_type_active
|
||||
_task_type_active = TaskType()
|
||||
|
||||
|
||||
def task_type_active_reset(context: bpy.types.Context) -> None:
|
||||
task_type_active_reset_entity()
|
||||
context.scene.kitsu.task_type_active_id = ""
|
||||
context.scene.kitsu.task_type_active_name = ""
|
||||
logger.debug("Reset active task type")
|
||||
|
||||
|
||||
def edit_active_set_by_id(context: bpy.types.Context, entity_id: str) -> None:
|
||||
global _edit_active
|
||||
|
||||
_edit_active = Edit.by_id(entity_id)
|
||||
context.scene.kitsu.edit_active_id = entity_id
|
||||
logger.debug("Set active edit to %s", _task_type_active.name)
|
||||
|
||||
|
||||
def edit_active_reset_entity() -> None:
|
||||
global _edit_active
|
||||
_edit_active = TaskType()
|
||||
|
||||
|
||||
def edit_active_get() -> Project:
|
||||
global _edit_active
|
||||
|
||||
return _edit_active
|
||||
|
||||
|
||||
def edit_default_get(create: bool = False, episode_id: Optional[str] = None) -> Edit:
|
||||
global _edit_active
|
||||
episode = Episode.by_id(episode_id) if episode_id else episode_active_get()
|
||||
_edit_active = Edit.get_project_default_edit(
|
||||
project_active_get(), create=create, episode=episode
|
||||
)
|
||||
return _edit_active
|
||||
|
||||
|
||||
def task_type_active_reset(context: bpy.types.Context) -> None:
|
||||
edit_active_reset_entity()
|
||||
context.scene.kitsu.edit_active_id = ""
|
||||
context.scene.kitsu.edit_active_name = ""
|
||||
logger.debug("Reset active edit")
|
||||
|
||||
|
||||
def get_projects_enum_list(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _projects_enum_list
|
||||
|
||||
if not addon_prefs_get(context).session.is_auth():
|
||||
return []
|
||||
|
||||
projectlist = ProjectList()
|
||||
_projects_enum_list.clear()
|
||||
_projects_enum_list.extend([(p.id, p.name, p.description or "") for p in projectlist.projects])
|
||||
return _projects_enum_list
|
||||
|
||||
|
||||
def get_episodes_enum_list(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _episodes_enum_list
|
||||
global _episode_cache_proj_id
|
||||
|
||||
if not addon_prefs_get(context).session.is_auth():
|
||||
return []
|
||||
|
||||
project_active = project_active_get()
|
||||
if not project_active:
|
||||
return []
|
||||
|
||||
# Return Cached list if project hasn't changed
|
||||
if _episode_cache_proj_id == project_active.id:
|
||||
return _episodes_enum_list
|
||||
|
||||
# Update Cache Sequence ID
|
||||
_episode_cache_proj_id = project_active.id
|
||||
|
||||
_episodes_enum_list.clear()
|
||||
_episodes_enum_list.extend(
|
||||
[(e.id, e.name, e.description or "") for e in project_active.get_episodes_all()]
|
||||
)
|
||||
return _episodes_enum_list
|
||||
|
||||
|
||||
def reset_sequences_enum_list() -> None:
|
||||
global _sequence_enum_list
|
||||
global _seq_cache_proj_id
|
||||
global _seq_cache_episode_id
|
||||
|
||||
_sequence_enum_list.clear()
|
||||
_seq_cache_proj_id = ""
|
||||
_seq_cache_episode_id = ""
|
||||
|
||||
def get_sequences_enum_list(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _sequence_enum_list
|
||||
global _seq_cache_proj_id
|
||||
global _seq_cache_episode_id
|
||||
|
||||
project_active = project_active_get()
|
||||
episode_active = episode_active_get()
|
||||
if not project_active:
|
||||
return []
|
||||
|
||||
# Return Cached list if project hasn't changed
|
||||
if _seq_cache_proj_id == project_active.id and _seq_cache_episode_id == episode_active.id:
|
||||
return _sequence_enum_list
|
||||
|
||||
# Update Cache Sequence ID
|
||||
_seq_cache_proj_id = project_active.id
|
||||
|
||||
_sequence_enum_list.clear()
|
||||
|
||||
if episode_active:
|
||||
_sequence_enum_list.extend(
|
||||
[(s.id, s.name, s.description or "") for s in episode_active.get_sequences_all()]
|
||||
)
|
||||
else:
|
||||
_sequence_enum_list.extend(
|
||||
[(s.id, s.name, s.description or "") for s in project_active.get_sequences_all()]
|
||||
)
|
||||
return _sequence_enum_list
|
||||
|
||||
|
||||
def get_shots_enum_for_active_seq(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _shot_enum_list
|
||||
global _shot_cache_seq_id
|
||||
|
||||
seq_active = sequence_active_get()
|
||||
|
||||
if not seq_active:
|
||||
return []
|
||||
|
||||
# Return Cached list if sequence hasn't changed
|
||||
if _shot_cache_seq_id == seq_active.id:
|
||||
return _shot_enum_list
|
||||
|
||||
# Update Cache Sequence ID
|
||||
_shot_cache_seq_id = seq_active.id
|
||||
|
||||
_shot_enum_list.clear()
|
||||
_shot_enum_list.extend(
|
||||
[(s.id, s.name, s.description or "") for s in seq_active.get_all_shots()]
|
||||
)
|
||||
return _shot_enum_list
|
||||
|
||||
|
||||
def get_shots_enum_for_seq(
|
||||
self: bpy.types.Operator, context: bpy.types.Context, sequence: Sequence
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _shot_enum_list
|
||||
|
||||
_shot_enum_list.clear()
|
||||
_shot_enum_list.extend([(s.id, s.name, s.description or "") for s in sequence.get_all_shots()])
|
||||
return _shot_enum_list
|
||||
|
||||
|
||||
def get_assetypes_enum_list(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _asset_types_enum_list
|
||||
global _asset_cache_proj_id
|
||||
|
||||
project_active = project_active_get()
|
||||
if not project_active:
|
||||
return []
|
||||
|
||||
# Return Cached list if project hasn't changed
|
||||
if _asset_cache_proj_id == project_active.id:
|
||||
return _asset_types_enum_list
|
||||
|
||||
# Update Cache Sequence ID
|
||||
_asset_cache_proj_id = project_active.id
|
||||
|
||||
_asset_types_enum_list.clear()
|
||||
_asset_types_enum_list.extend(
|
||||
[(at.id, at.name, "") for at in project_active.get_all_asset_types()]
|
||||
)
|
||||
return _asset_types_enum_list
|
||||
|
||||
|
||||
def get_assets_enum_for_active_asset_type(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _asset_enum_list
|
||||
global _asset_cache_asset_type_id
|
||||
|
||||
project_active = project_active_get()
|
||||
asset_type_active = asset_type_active_get()
|
||||
episode_active = episode_active_get()
|
||||
|
||||
if not project_active or not asset_type_active:
|
||||
return []
|
||||
|
||||
if _asset_cache_asset_type_id == asset_type_active.id:
|
||||
return _asset_enum_list
|
||||
|
||||
_asset_cache_asset_type_id = asset_type_active.id
|
||||
|
||||
all_assets = project_active.get_all_assets_for_type(asset_type_active)
|
||||
|
||||
_asset_enum_list.clear()
|
||||
if not episode_active:
|
||||
_asset_enum_list.extend([(a.id, a.name, a.description or "") for a in all_assets])
|
||||
else:
|
||||
episode_assets = filter(
|
||||
lambda p: p.source_id == episode_active.id or p.source_id is None, all_assets
|
||||
)
|
||||
_asset_enum_list.extend([(a.id, a.name, a.description or "") for a in episode_assets])
|
||||
return _asset_enum_list
|
||||
|
||||
|
||||
def get_task_types_enum_for_current_context(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _task_types_enum_list
|
||||
global _project_active
|
||||
|
||||
# Import within function to avoid circular import
|
||||
from .context import core as context_core
|
||||
|
||||
items = []
|
||||
if context_core.is_shot_context():
|
||||
items = [
|
||||
(t.id, t.name, "")
|
||||
for t in TaskType.all_shot_task_types()
|
||||
if t.id in _project_active.task_types
|
||||
]
|
||||
|
||||
if context_core.is_asset_context():
|
||||
items = [
|
||||
(t.id, t.name, "")
|
||||
for t in TaskType.all_asset_task_types()
|
||||
if t.id in _project_active.task_types
|
||||
]
|
||||
|
||||
if context_core.is_sequence_context():
|
||||
items = [
|
||||
(t.id, t.name, "")
|
||||
for t in TaskType.all_sequence_task_types()
|
||||
if t.id in _project_active.task_types
|
||||
]
|
||||
|
||||
if context_core.is_edit_context():
|
||||
items = [
|
||||
(t.id, t.name, "")
|
||||
for t in TaskType.all_edit_task_types()
|
||||
if t.id in _project_active.task_types
|
||||
]
|
||||
|
||||
_task_types_enum_list.clear()
|
||||
_task_types_enum_list.extend(items)
|
||||
|
||||
return _task_types_enum_list
|
||||
|
||||
|
||||
def get_shot_task_types_enum(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
# Returns all avaliable task types across all shots in the current project
|
||||
global _task_types_shots_enum_list
|
||||
global _all_shot_tasks_cache_proj_id
|
||||
|
||||
project_active = project_active_get()
|
||||
|
||||
# Return Cached list if project hasn't changed
|
||||
if _all_shot_tasks_cache_proj_id == project_active.id:
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
# Update Cache project ID
|
||||
_all_shot_tasks_cache_proj_id = project_active.id
|
||||
|
||||
items = [(t.id, t.name, "") for t in TaskType.all_shot_task_types() if t.id in project_active.task_types]
|
||||
|
||||
_task_types_shots_enum_list.clear()
|
||||
_task_types_shots_enum_list.extend(items)
|
||||
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
|
||||
def get_shot_task_types_enum_for_shot( # TODO Rename
|
||||
self: bpy.types.Operator, context: bpy.types.Context, shot: Shot
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _task_types_shots_enum_list
|
||||
global _task_type_cache_shot_id
|
||||
|
||||
# Return Cached list if shot hasn't changed
|
||||
if _task_type_cache_shot_id == shot.id:
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
# Update Cache Sequence ID
|
||||
_task_type_cache_shot_id = shot.id
|
||||
|
||||
items = [(t.id, t.name, "") for t in shot.get_all_task_types()]
|
||||
|
||||
_task_types_shots_enum_list.clear()
|
||||
_task_types_shots_enum_list.extend(items)
|
||||
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
|
||||
def reset_all_edits_enum_for_active_project() -> None:
|
||||
global _all_edits_cache_proj_id
|
||||
global _all_edits_enum_list
|
||||
|
||||
_all_edits_cache_proj_id = None
|
||||
_all_edits_enum_list.clear()
|
||||
gazu.cache.clear_all()
|
||||
|
||||
|
||||
def get_all_edits_enum_for_active_project(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _project_active
|
||||
global _all_edits_cache_proj_id
|
||||
global _all_edits_enum_list
|
||||
|
||||
if _all_edits_cache_proj_id == _project_active.id:
|
||||
return _all_edits_enum_list
|
||||
|
||||
# Update Cache Project ID
|
||||
_all_edits_cache_proj_id = _project_active.id
|
||||
|
||||
items = [(t.id, t.name, "") for t in _project_active.get_all_edits()]
|
||||
|
||||
_all_edits_enum_list.clear()
|
||||
_all_edits_enum_list.extend(items)
|
||||
|
||||
return _all_edits_enum_list
|
||||
|
||||
|
||||
def get_all_task_statuses_enum(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _task_statuses_enum_list
|
||||
global _all_task_type_cache_proj_id
|
||||
|
||||
project_active = project_active_get()
|
||||
|
||||
# Return Cached list if project hasn't changed
|
||||
if _all_task_type_cache_proj_id == project_active.id:
|
||||
return _task_statuses_enum_list
|
||||
|
||||
# Update Cache project ID
|
||||
_all_task_type_cache_proj_id = project_active.id
|
||||
|
||||
items = [(t.id, t.name, "") for t in TaskStatus.all_task_statuses()]
|
||||
|
||||
_task_statuses_enum_list.clear()
|
||||
_task_statuses_enum_list.extend(items)
|
||||
|
||||
return _task_statuses_enum_list
|
||||
|
||||
|
||||
def load_user_all_tasks(context: bpy.types.Context) -> List[Task]:
|
||||
global _user_all_tasks
|
||||
global _user_active
|
||||
|
||||
tasks = _user_active.all_tasks_to_do()
|
||||
_user_all_tasks.clear()
|
||||
_user_all_tasks.extend(tasks)
|
||||
|
||||
_update_tasks_collection_prop(context)
|
||||
|
||||
logger.debug("Loaded assigned tasks for: %s", _user_active.full_name)
|
||||
|
||||
return _user_all_tasks
|
||||
|
||||
|
||||
def _update_tasks_collection_prop(context: bpy.types.Context) -> None:
|
||||
global _user_all_tasks
|
||||
addon_prefs = addon_prefs_get(bpy.context)
|
||||
tasks_coll_prop = addon_prefs.tasks
|
||||
|
||||
# Get current index.
|
||||
idx = context.window_manager.kitsu.tasks_index
|
||||
|
||||
# Clear all old tasks.
|
||||
tasks_coll_prop.clear()
|
||||
|
||||
# Populate with new tasks.
|
||||
for task in _user_all_tasks:
|
||||
item = tasks_coll_prop.add()
|
||||
item.id = task.id
|
||||
item.entity_id = task.entity_id
|
||||
item.entity_name = task.entity_name
|
||||
item.task_type_id = task.task_type_id
|
||||
item.task_type_name = task.task_type_name
|
||||
|
||||
# Update index.
|
||||
idx = len(tasks_coll_prop) - 1
|
||||
|
||||
|
||||
def get_user_all_tasks_enum(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _user_all_tasks_enum_list
|
||||
global _user_all_tasks
|
||||
|
||||
enum_items = [(t.id, t.name, "") for t in _user_all_tasks]
|
||||
|
||||
_user_all_tasks_enum_list.clear()
|
||||
_user_all_tasks_enum_list.extend(enum_items)
|
||||
|
||||
return _user_all_tasks_enum_list
|
||||
|
||||
|
||||
def get_user_all_tasks() -> List[Task]:
|
||||
global _user_all_tasks
|
||||
return _user_all_tasks
|
||||
|
||||
|
||||
def _init_cache_entity(
|
||||
entity_id: str, entity_type: Any, cache_variable_name: Any, cache_name: str
|
||||
) -> None:
|
||||
if entity_id:
|
||||
try:
|
||||
globals()[cache_variable_name] = entity_type.by_id(entity_id)
|
||||
logger.debug(
|
||||
"Initiated active %s cache to: %s",
|
||||
cache_name,
|
||||
globals()[cache_variable_name].name,
|
||||
)
|
||||
except gazu.exception.RouteNotFoundException:
|
||||
logger.error(
|
||||
"Failed to initialize active %s cache. ID not found on server: %s",
|
||||
cache_name,
|
||||
entity_id,
|
||||
)
|
||||
|
||||
|
||||
def init_startup_variables(context: bpy.types.Context) -> None:
|
||||
addon_prefs = addon_prefs_get(context)
|
||||
global _cache_startup_initialized
|
||||
global _user_active
|
||||
global _user_all_tasks
|
||||
|
||||
if not addon_prefs.session.is_auth():
|
||||
logger.debug("Skip initiating startup cache. Session not authorized")
|
||||
return
|
||||
|
||||
if _cache_startup_initialized:
|
||||
logger.debug("Startup Cache already initiated")
|
||||
return
|
||||
|
||||
# User.
|
||||
_user_active = User()
|
||||
logger.debug("Initiated active user cache to: %s", _user_active.full_name)
|
||||
|
||||
# User Tasks.
|
||||
load_user_all_tasks(context)
|
||||
logger.debug("Initiated active user tasks")
|
||||
|
||||
_cache_startup_initialized = True
|
||||
|
||||
|
||||
def clear_startup_variables():
|
||||
global _user_active
|
||||
global _user_all_tasks
|
||||
global _cache_startup_initialized
|
||||
|
||||
_user_active = User()
|
||||
logger.debug("Cleared active user cache")
|
||||
|
||||
_user_all_tasks.clear()
|
||||
_update_tasks_collection_prop(bpy.context)
|
||||
logger.debug("Cleared active user all tasks cache")
|
||||
|
||||
_cache_startup_initialized = False
|
||||
|
||||
|
||||
def init_cache_variables() -> None:
|
||||
global _project_active
|
||||
global _episode_active
|
||||
global _sequence_active
|
||||
global _shot_active
|
||||
global _asset_active
|
||||
global _asset_type_active
|
||||
global _task_type_active
|
||||
global _cache_initialized
|
||||
addon_prefs = addon_prefs_get(bpy.context)
|
||||
|
||||
if not addon_prefs.session.is_auth():
|
||||
logger.debug("Skip initiating cache. Session not authorized")
|
||||
return
|
||||
|
||||
if _cache_initialized:
|
||||
logger.debug("Cache already initiated")
|
||||
return
|
||||
|
||||
project_active_id = addon_prefs.project_active_id
|
||||
episode_active_id = bpy.context.scene.kitsu.episode_active_id
|
||||
sequence_active_id = bpy.context.scene.kitsu.sequence_active_id
|
||||
shot_active_id = bpy.context.scene.kitsu.shot_active_id
|
||||
asset_active_id = bpy.context.scene.kitsu.asset_active_id
|
||||
asset_type_active_id = bpy.context.scene.kitsu.asset_type_active_id
|
||||
task_type_active_id = bpy.context.scene.kitsu.task_type_active_id
|
||||
|
||||
_init_cache_entity(project_active_id, Project, "_project_active", "project")
|
||||
_init_cache_entity(episode_active_id, Episode, "_episode_active", "episode")
|
||||
_init_cache_entity(sequence_active_id, Sequence, "_sequence_active", "sequence")
|
||||
_init_cache_entity(asset_type_active_id, AssetType, "_asset_type_active", "asset type")
|
||||
_init_cache_entity(shot_active_id, Shot, "_shot_active", "shot")
|
||||
_init_cache_entity(asset_active_id, Asset, "_asset_active", "asset")
|
||||
_init_cache_entity(task_type_active_id, TaskType, "_task_type_active", "task type")
|
||||
|
||||
_cache_initialized = True
|
||||
|
||||
|
||||
def clear_cache_variables():
|
||||
global _project_active
|
||||
global _episode_active
|
||||
global _sequence_active
|
||||
global _shot_active
|
||||
global _asset_active
|
||||
global _asset_type_active
|
||||
global _task_type_active
|
||||
global _cache_initialized
|
||||
|
||||
_user_active = User()
|
||||
logger.debug("Cleared active user cache")
|
||||
|
||||
_shot_active = Shot()
|
||||
logger.debug("Cleared active shot cache")
|
||||
|
||||
_asset_active = Asset()
|
||||
logger.debug("Cleared active asset cache")
|
||||
|
||||
_sequence_active = Sequence()
|
||||
logger.debug("Cleared active aequence cache")
|
||||
|
||||
_asset_type_active = AssetType()
|
||||
logger.debug("Cleared active asset type cache")
|
||||
|
||||
_project_active = Project()
|
||||
logger.debug("Cleared active project cache")
|
||||
|
||||
_task_type_active = TaskType()
|
||||
logger.debug("Cleared active task type cache")
|
||||
|
||||
_cache_initialized = False
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post_handler_update_cache(dummy: Any) -> None:
|
||||
clear_cache_variables()
|
||||
init_cache_variables()
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post_handler_init_startup_variables(dummy: Any) -> None:
|
||||
init_startup_variables(bpy.context)
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def register():
|
||||
# Handlers.
|
||||
bpy.app.handlers.load_post.append(load_post_handler_update_cache)
|
||||
bpy.app.handlers.load_post.append(load_post_handler_init_startup_variables)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Clear handlers.
|
||||
bpy.app.handlers.load_post.remove(load_post_handler_init_startup_variables)
|
||||
bpy.app.handlers.load_post.remove(load_post_handler_update_cache)
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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]
|
||||
@@ -1,343 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,156 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,17 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
def preload_modules() -> None:
|
||||
"""Pre-load the datetime module from a wheel so that the API can find it."""
|
||||
import sys
|
||||
|
||||
if "gazu" in sys.modules:
|
||||
return
|
||||
|
||||
from . import wheels
|
||||
|
||||
wheels.load_wheel_global("bidict", "bidict")
|
||||
wheels.load_wheel_global("engineio", "python_engineio")
|
||||
wheels.load_wheel_global("socketio", "python_socketio")
|
||||
wheels.load_wheel_global("gazu", "gazu")
|
||||
@@ -1,27 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..edit 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()
|
||||
@@ -1,95 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from .. import prefs, propsdata
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def edit_export_get_latest(context: bpy.types.Context):
|
||||
"""Find latest render in editorial export directory"""
|
||||
|
||||
files_list = edit_export_get_all(context)
|
||||
if len(files_list) >= 1:
|
||||
files_list = sorted(files_list, reverse=True)
|
||||
return files_list[0]
|
||||
return None
|
||||
|
||||
|
||||
def edit_export_get_all(context: bpy.types.Context):
|
||||
"""Find all renders in editorial export directory"""
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
edit_export_path = propsdata.get_edit_export_dir()
|
||||
|
||||
if not edit_export_path.exists():
|
||||
print(f"Editorial export directory '{edit_export_path}' does not exist.")
|
||||
return []
|
||||
|
||||
files_list = [
|
||||
f
|
||||
for f in edit_export_path.iterdir()
|
||||
if f.is_file() and edit_export_is_valid_name(addon_prefs.edit_export_file_pattern, f.name)
|
||||
]
|
||||
return files_list
|
||||
|
||||
|
||||
def edit_export_is_valid_name(file_pattern: str, filename: str) -> bool:
|
||||
"""Verify file name matches file pattern set in preferences"""
|
||||
# Prevents un-expected matches
|
||||
file_pattern = re.escape(file_pattern)
|
||||
# Replace `#` with `\d` to represent digits
|
||||
match = re.search(file_pattern.replace('\#', '\d'), filename)
|
||||
if match:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def edit_export_import_latest(
|
||||
context: bpy.types.Context, shot
|
||||
) -> list[bpy.types.Strip]: # TODO add info to shot
|
||||
"""Loads latest export from editorial department"""
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
strip_channel = 1
|
||||
latest_file = edit_export_get_latest(context)
|
||||
if not latest_file:
|
||||
return None
|
||||
# Check if Kitsu server returned empty shot
|
||||
if shot.id == '':
|
||||
return None
|
||||
strip_filepath = latest_file.as_posix()
|
||||
strip_frame_start = addon_prefs.shot_builder_frame_offset
|
||||
|
||||
scene = context.scene
|
||||
if not scene.sequence_editor:
|
||||
scene.sequence_editor_create()
|
||||
seq_editor = scene.sequence_editor
|
||||
movie_strip = seq_editor.sequences.new_movie(
|
||||
latest_file.name,
|
||||
strip_filepath,
|
||||
strip_channel + 1,
|
||||
strip_frame_start,
|
||||
fit_method="ORIGINAL",
|
||||
)
|
||||
sound_strip = seq_editor.sequences.new_sound(
|
||||
latest_file.name,
|
||||
strip_filepath,
|
||||
strip_channel,
|
||||
strip_frame_start,
|
||||
)
|
||||
new_strips = [movie_strip, sound_strip]
|
||||
|
||||
# Update shift frame range prop.
|
||||
frame_in = shot.data.get("frame_in")
|
||||
frame_3d_start = shot.get_3d_start()
|
||||
frame_3d_offset = frame_3d_start - addon_prefs.shot_builder_frame_offset
|
||||
edit_export_offset = addon_prefs.edit_export_frame_offset
|
||||
|
||||
# Set sequence strip start kitsu data.
|
||||
for strip in new_strips:
|
||||
strip.frame_start = (
|
||||
-frame_in + (strip_frame_start * 2) + frame_3d_offset + edit_export_offset
|
||||
)
|
||||
return new_strips
|
||||
@@ -1,355 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Strip, Context
|
||||
import os
|
||||
from typing import Set, List
|
||||
from pathlib import Path
|
||||
from .. import cache, prefs, util, propsdata
|
||||
from ..types import Task, TaskStatus
|
||||
from ..playblast.core import override_render_path, override_render_format
|
||||
from . import opsdata
|
||||
from ..logger import LoggerFactory
|
||||
from .core import edit_export_import_latest, edit_export_get_all, edit_export_get_latest
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_edit_export_publish(bpy.types.Operator):
|
||||
bl_idname = "kitsu.edit_export_publish"
|
||||
bl_label = "Export & Publish"
|
||||
bl_description = (
|
||||
"Renders current VSE Edit as .mp4"
|
||||
"Saves the set version to disk and uploads it to Kitsu with the specified "
|
||||
"comment and task type. Overrides some render settings during render. "
|
||||
)
|
||||
|
||||
task_status: bpy.props.EnumProperty(name="Task Status", items=cache.get_all_task_statuses_enum)
|
||||
comment: bpy.props.StringProperty(name="Comment")
|
||||
use_frame_start: bpy.props.BoolProperty(name="Submit update to 'frame_start'.", default=False)
|
||||
frame_start: bpy.props.IntProperty(
|
||||
name="Frame Start",
|
||||
description="Send an integer for the 'frame_start' value of the current Kitsu Edit. \nThis is used by Watchtower to pad the edit in the timeline.",
|
||||
default=0,
|
||||
)
|
||||
thumbnail_frame: bpy.props.IntProperty(
|
||||
name="Thumbnail Frame",
|
||||
description="Frame to use as the thumbnail on Kitsu",
|
||||
min=0,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
kitsu_props = context.scene.kitsu
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
if not prefs.session_auth(context):
|
||||
cls.poll_message_set("Login to a Kitsu Server")
|
||||
if not cache.project_active_get():
|
||||
cls.poll_message_set("Select an active project")
|
||||
return False
|
||||
if kitsu_props.edit_active_id == "" or cache.edit_active_get().id == "":
|
||||
cls.poll_message_set("Select an edit entity from Kitsu Context Menu")
|
||||
return False
|
||||
if kitsu_props.task_type_active_id == "":
|
||||
cls.poll_message_set("Select a task type from Kitsu Context Menu")
|
||||
return False
|
||||
if not addon_prefs.is_edit_export_root_valid:
|
||||
cls.poll_message_set("Edit Export Directory is Invalid, see Add-On preferences")
|
||||
return False
|
||||
if not addon_prefs.is_edit_export_pattern_valid:
|
||||
cls.poll_message_set("Edit Export File Pattern is Invalid, see Add-On preferences")
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.thumbnail_frame = context.scene.frame_current
|
||||
|
||||
# Remove file name if set in render filepath
|
||||
dir_path = bpy.path.abspath(context.scene.render.filepath)
|
||||
if not os.path.isdir(Path(dir_path)):
|
||||
dir_path = Path(dir_path).parent
|
||||
self.render_dir = str(dir_path)
|
||||
|
||||
# 'frame_start' is optionally property appearing on all edit_entries for a project if it exists (used in watchtower)
|
||||
server_frame_start = cache.edit_active_get().get_frame_start()
|
||||
if server_frame_start is int:
|
||||
self.frame_start = server_frame_start
|
||||
self.use_frame_start = bool(server_frame_start is not None)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.prop(self, "comment")
|
||||
layout.prop(self, 'task_status')
|
||||
|
||||
# Only set `frame_start` if exists on current project
|
||||
if self.use_frame_start:
|
||||
layout.prop(self, "frame_start")
|
||||
layout.prop(self, "thumbnail_frame")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
edit_entity = cache.edit_active_get()
|
||||
task_type_entity = cache.task_type_active_get()
|
||||
kitsu_props = context.scene.kitsu
|
||||
|
||||
# Ensure thumbnail frame is not outside of frame range
|
||||
if self.thumbnail_frame not in range(
|
||||
0, (context.scene.frame_end - context.scene.frame_start) + 1
|
||||
):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Thumbnail frame '{self.thumbnail_frame}' is outside of frame range ",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Build render_path
|
||||
render_path = Path(kitsu_props.edit_export_file)
|
||||
render_path_str = render_path.as_posix()
|
||||
render_name = render_path.name
|
||||
render_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not render_path.parent.exists():
|
||||
self.report({"ERROR"}, f"Render path is not set to a directory. '{self.render_dir}'")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Render Sequence to .mp4
|
||||
with override_render_path(self, context, render_path_str):
|
||||
with override_render_format(self, context, enable_sequencer=True):
|
||||
bpy.ops.render.opengl(animation=True, sequencer=True)
|
||||
|
||||
# Create comment with video
|
||||
task_status = TaskStatus.by_id(self.task_status)
|
||||
task = Task.by_name(edit_entity, task_type_entity)
|
||||
comment = task.add_comment(
|
||||
task_status,
|
||||
comment=self.comment,
|
||||
)
|
||||
task.add_preview_to_comment(
|
||||
comment,
|
||||
render_path_str,
|
||||
self.thumbnail_frame,
|
||||
)
|
||||
# Update edit_entry's frame_start if 'frame_start' is found on server
|
||||
if self.use_frame_start:
|
||||
edit_entity.set_frame_start(self.frame_start)
|
||||
|
||||
self.report({"INFO"}, f"Submitted new comment 'Revision {kitsu_props.edit_export_version}'")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_edit_export_set_version(bpy.types.Operator):
|
||||
bl_idname = "kitsu.edit_export_set_version"
|
||||
bl_label = "Version"
|
||||
bl_property = "versions"
|
||||
bl_description = (
|
||||
"Sets version of edit export. Warning triangle in ui "
|
||||
"indicates that version already exists on disk"
|
||||
)
|
||||
|
||||
versions: bpy.props.EnumProperty(
|
||||
items=opsdata.get_edit_export_versions_enum_list, name="Versions"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
if addon_prefs.edit_export_dir == "":
|
||||
cls.poll_message_set("Edit Export Directory is not set, check add-on preferences")
|
||||
return False
|
||||
if not addon_prefs.is_edit_export_root_valid:
|
||||
cls.poll_message_set("Edit Export Directory is Invalid, see Add-On preferences")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
kitsu_props = context.scene.kitsu
|
||||
version = self.versions
|
||||
|
||||
if not version:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if kitsu_props.get('edit_export_version') == version:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Update global scene cache version prop.
|
||||
kitsu_props.edit_export_version = version
|
||||
logger.info("Set edit export version to %s", version)
|
||||
|
||||
# Redraw ui.
|
||||
util.ui_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
context.window_manager.invoke_search_popup(self) # type: ignore
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_edit_export_increment_version(bpy.types.Operator):
|
||||
bl_idname = "kitsu.edit_export_increment_version"
|
||||
bl_label = "Add Version Increment"
|
||||
bl_description = "Increment the edit export version by one"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
# Incremenet version.
|
||||
version = opsdata.add_edit_export_version_increment(context)
|
||||
|
||||
# Update cache_version prop.
|
||||
context.scene.kitsu.edit_export_version = version
|
||||
|
||||
# Report.
|
||||
self.report({"INFO"}, f"Add edit export version {version}")
|
||||
|
||||
util.ui_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_edit_export_import_latest(bpy.types.Operator):
|
||||
bl_idname = "kitsu.edit_export_import_latest"
|
||||
bl_label = "Import Latest Edit Export"
|
||||
bl_description = (
|
||||
"Find and import the latest editorial render found in the Editorial Export Directory for the current shot. "
|
||||
"Will only Import if the latest export is not already imported. "
|
||||
"Will remove any previous exports currently in the file's Video Sequence Editor"
|
||||
)
|
||||
|
||||
_existing_edit_exports = []
|
||||
_removed_movie = 0
|
||||
_removed_audio = 0
|
||||
_latest_export_name = ""
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not prefs.session_auth(context):
|
||||
cls.poll_message_set("Login to a Kitsu Server")
|
||||
return False
|
||||
if not cache.project_active_get():
|
||||
cls.poll_message_set("Select an active project")
|
||||
return False
|
||||
if cache.shot_active_get().id == "":
|
||||
cls.poll_message_set("Please set an active shot in Kitsu Context UI")
|
||||
return False
|
||||
if not prefs.addon_prefs_get(context).is_edit_export_root_valid:
|
||||
cls.poll_message_set("Edit Export Directory is Invalid, see Add-On Preferences")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_filepath(self, strip):
|
||||
if hasattr(strip, "filepath"):
|
||||
return strip.filepath
|
||||
if hasattr(strip, "sound"):
|
||||
return strip.sound.filepath
|
||||
|
||||
def compare_strip_to_path(self, strip: Strip, compare_path: Path) -> bool:
|
||||
strip_path = Path(bpy.path.abspath(self.get_filepath(strip)))
|
||||
return bool(compare_path.absolute() == strip_path.absolute())
|
||||
|
||||
def compare_strip_to_paths(self, strip: Strip, compare_paths: List[Path]) -> bool:
|
||||
for compare_path in compare_paths:
|
||||
if self.compare_strip_to_path(strip, compare_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_existing_edit_exports(
|
||||
self, context: Context, all_edit_export_paths: List[Path]
|
||||
) -> List[Strip]:
|
||||
sequences = context.scene.sequence_editor.sequences
|
||||
|
||||
# Collect Existing Edit Export
|
||||
for strip in sequences:
|
||||
if self.compare_strip_to_paths(strip, all_edit_export_paths):
|
||||
self._existing_edit_exports.append(strip)
|
||||
return self._existing_edit_exports
|
||||
|
||||
def check_if_latest_edit_export_is_imported(self, context: Context) -> bool:
|
||||
# Check if latest edit export is already loaded.
|
||||
for strip in self._existing_edit_exports:
|
||||
latest_edit_export_path = edit_export_get_latest(context)
|
||||
if self.compare_strip_to_path(strip, latest_edit_export_path):
|
||||
self._latest_export_name = latest_edit_export_path.name
|
||||
return True
|
||||
|
||||
def remove_existing_edit_exports(self, context: Context) -> None:
|
||||
# Remove Existing Strips to make way for new Strip
|
||||
sequences = context.scene.sequence_editor.sequences
|
||||
for strip in self._existing_edit_exports:
|
||||
if strip.type == "MOVIE":
|
||||
self._removed_movie += 1
|
||||
if strip.type == "SOUND":
|
||||
self._removed_audio += 1
|
||||
sequences.remove(strip)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
# Reset Values
|
||||
self._existing_edit_exports = []
|
||||
self._removed_movie = 0
|
||||
self._removed_audio = 0
|
||||
self._latest_export_name = ""
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
# Get paths to all edit exports
|
||||
all_edit_export_paths = edit_export_get_all(context)
|
||||
if all_edit_export_paths == []:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"No Edit Exports found in '{addon_prefs.edit_export_dir}' using pattern '{addon_prefs.edit_export_file_pattern}' See Add-On Preferences",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Collect all existing edit exports
|
||||
self.get_existing_edit_exports(context, all_edit_export_paths)
|
||||
|
||||
# Stop latest export is already imported
|
||||
if self.check_if_latest_edit_export_is_imported(context):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Latest Editorial Export already loaded '{self._latest_export_name}'",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Remove old edit exports
|
||||
self.remove_existing_edit_exports(context)
|
||||
|
||||
# Import new edit export
|
||||
shot = cache.shot_active_get()
|
||||
strips = edit_export_import_latest(context, shot)
|
||||
|
||||
if strips is None:
|
||||
self.report({"WARNING"}, f"Loaded Latest Editorial Export failed to import!")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Report.
|
||||
if self._removed_movie > 0 or self._removed_audio > 0:
|
||||
removed_msg = (
|
||||
f"Removed {self._removed_movie} Movie Strips and {self._removed_audio} Audio Strips"
|
||||
)
|
||||
self.report(
|
||||
{"INFO"}, f"Loaded Latest Editorial Export, '{strips[0].name}'. {removed_msg}"
|
||||
)
|
||||
else:
|
||||
self.report({"INFO"}, f"Loaded Latest Editorial Export, '{strips[0].name}'")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = [
|
||||
KITSU_OT_edit_export_publish,
|
||||
KITSU_OT_edit_export_set_version,
|
||||
KITSU_OT_edit_export_increment_version,
|
||||
KITSU_OT_edit_export_import_latest,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,97 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Any, List, Tuple
|
||||
from pathlib import Path
|
||||
from .. import prefs, propsdata
|
||||
import bpy
|
||||
|
||||
from ..models import FileListModel
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
EDIT_EXPORT_FILE_MODEL = FileListModel()
|
||||
_edit_export_enum_list: List[Tuple[str, str, str]] = []
|
||||
_edit_export_file_model_init: bool = False
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
# TODO Compare to Playblast opsdata, maybe centralize some repeated code
|
||||
|
||||
|
||||
def init_edit_export_file_model(
|
||||
context: bpy.types.Context,
|
||||
) -> None:
|
||||
global EDIT_EXPORT_FILE_MODEL
|
||||
global _edit_export_file_model_init
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
kitsu_props = context.scene.kitsu
|
||||
edit_export_dir = propsdata.get_edit_export_dir()
|
||||
|
||||
# Is None if invalid.
|
||||
if addon_prefs.edit_export_dir == "" or not edit_export_dir.exists():
|
||||
logger.error(
|
||||
"Failed to initialize edit export file model. Invalid path. Check addon preferences"
|
||||
)
|
||||
return
|
||||
|
||||
EDIT_EXPORT_FILE_MODEL.filter_name = Path(kitsu_props.edit_export_file).name
|
||||
EDIT_EXPORT_FILE_MODEL.reset()
|
||||
EDIT_EXPORT_FILE_MODEL.root_path = edit_export_dir
|
||||
|
||||
if not EDIT_EXPORT_FILE_MODEL.versions:
|
||||
EDIT_EXPORT_FILE_MODEL.append_item("v001")
|
||||
# Update edit_export_version prop.
|
||||
kitsu_props.edit_export_version = "v001"
|
||||
|
||||
else:
|
||||
# Update edit_export_version prop.
|
||||
kitsu_props.edit_export_version = EDIT_EXPORT_FILE_MODEL.versions[0]
|
||||
|
||||
_edit_export_file_model_init = True
|
||||
|
||||
|
||||
def add_edit_export_version_increment(context: bpy.types.Context) -> str:
|
||||
# Init model if it did not happen.
|
||||
if not _edit_export_file_model_init:
|
||||
init_edit_export_file_model(context)
|
||||
|
||||
# Should be already sorted.
|
||||
versions = EDIT_EXPORT_FILE_MODEL.versions
|
||||
|
||||
if len(versions) > 0:
|
||||
latest_version = versions[0]
|
||||
increment = "v{:03}".format(int(latest_version.replace("v", "")) + 1)
|
||||
else:
|
||||
increment = "v001"
|
||||
|
||||
EDIT_EXPORT_FILE_MODEL.append_item(increment)
|
||||
return increment
|
||||
|
||||
|
||||
def get_edit_export_versions_enum_list(
|
||||
self: Any,
|
||||
context: bpy.types.Context,
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _edit_export_enum_list
|
||||
global EDIT_EXPORT_FILE_MODEL
|
||||
global init_edit_export_file_model
|
||||
global _edit_export_file_model_init
|
||||
|
||||
# Init model if it did not happen.
|
||||
if not _edit_export_file_model_init:
|
||||
init_edit_export_file_model(context)
|
||||
|
||||
# Clear all versions in enum list.
|
||||
_edit_export_enum_list.clear()
|
||||
_edit_export_enum_list.extend(EDIT_EXPORT_FILE_MODEL.versions_as_enum_list)
|
||||
|
||||
return _edit_export_enum_list
|
||||
|
||||
|
||||
def add_version_custom(custom_version: str) -> None:
|
||||
global _edit_export_enum_list
|
||||
global EDIT_EXPORT_FILE_MODEL
|
||||
|
||||
EDIT_EXPORT_FILE_MODEL.append_item(custom_version)
|
||||
@@ -1,116 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from .. import prefs, ui
|
||||
from ..context import core as context_core
|
||||
from pathlib import Path
|
||||
from .ops import (
|
||||
KITSU_OT_edit_export_set_version,
|
||||
KITSU_OT_edit_export_increment_version,
|
||||
KITSU_OT_edit_export_publish,
|
||||
KITSU_OT_edit_export_import_latest,
|
||||
)
|
||||
from ..generic.ops import KITSU_OT_open_path
|
||||
|
||||
|
||||
class KITSU_PT_edit_export_publish(bpy.types.Panel):
|
||||
"""
|
||||
Panel in sequence editor that exposes a set of tools that are used to export latest edit
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Export & Publish"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 50
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(prefs.session_auth(context) and context_core.is_edit_context())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
layout = self.layout
|
||||
split_factor_small = 0.95
|
||||
|
||||
# # ERROR.
|
||||
if not addon_prefs.is_edit_export_root_valid:
|
||||
box = ui.draw_error_box(layout)
|
||||
ui.draw_error_invalid_edit_export_root_dir(box)
|
||||
|
||||
# Edit Export version op.
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_edit_export_set_version.bl_idname,
|
||||
text=context.scene.kitsu.edit_export_version,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
# Edit Export increment version op.
|
||||
row.operator(
|
||||
KITSU_OT_edit_export_increment_version.bl_idname,
|
||||
text="",
|
||||
icon="ADD",
|
||||
)
|
||||
|
||||
# Edit Export op.
|
||||
row = layout.row(align=True)
|
||||
row.operator(KITSU_OT_edit_export_publish.bl_idname, icon="RENDER_ANIMATION")
|
||||
|
||||
# Edit Export path label.
|
||||
if Path(context.scene.kitsu.edit_export_file).exists():
|
||||
split = layout.split(factor=1 - split_factor_small, align=True)
|
||||
split.label(icon="ERROR")
|
||||
sub_split = split.split(factor=split_factor_small)
|
||||
sub_split.label(text=context.scene.kitsu.edit_export_file)
|
||||
sub_split.operator(
|
||||
KITSU_OT_open_path.bl_idname, icon="FILE_FOLDER", text=""
|
||||
).filepath = context.scene.kitsu.edit_export_file
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=context.scene.kitsu.edit_export_file)
|
||||
row.operator(KITSU_OT_open_path.bl_idname, icon="FILE_FOLDER", text="").filepath = (
|
||||
context.scene.kitsu.edit_export_file
|
||||
)
|
||||
|
||||
|
||||
class KITSU_PT_edit_export_tools(bpy.types.Panel):
|
||||
"""
|
||||
Panel in sequence editor that exposes a set of tools that are used to load the latest edit
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "General Tools"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 50
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not prefs.session_auth(context):
|
||||
return False
|
||||
|
||||
if not (context_core.is_sequence_context() or context_core.is_shot_context()):
|
||||
return False
|
||||
return True
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
box = self.layout.box()
|
||||
box.label(text="General", icon="MODIFIER")
|
||||
box.operator(KITSU_OT_edit_export_import_latest.bl_idname)
|
||||
|
||||
|
||||
classes = [KITSU_PT_edit_export_publish, KITSU_PT_edit_export_tools]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,10 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
class ShotInvalidException(Exception):
|
||||
"""
|
||||
Error raised when shot is not valid. For example has no self.id.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -1,54 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
filetree_default = {
|
||||
"working": {
|
||||
"mountpoint": "",
|
||||
"root": "",
|
||||
"folder_path": {
|
||||
"shot": "<Project>/shots/production/<Sequence>/<Shot>/<TaskType>/work",
|
||||
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>/work",
|
||||
"sequence": "<Project>/shots/production/<Sequence>/<TaskType>/work",
|
||||
"style": "lowercase",
|
||||
},
|
||||
"file_name": {
|
||||
"shot": "<Shot>_<TaskType>_work",
|
||||
"asset": "<Asset>_<TaskType>_work",
|
||||
"sequence": "<Sequence>_<TaskType>_work",
|
||||
"style": "lowercase",
|
||||
},
|
||||
},
|
||||
"output": {
|
||||
"mountpoint": "",
|
||||
"root": "",
|
||||
"folder_path": {
|
||||
"shot": "<Project>/shots/production/<Sequence>/<Shot>/<TaskType>/publish",
|
||||
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>/publish",
|
||||
"sequence": "<Project>/shots/production/<Sequence>/<TaskType>/publish",
|
||||
"style": "lowercase",
|
||||
},
|
||||
"file_name": {
|
||||
"shot": "<Shot>_<TaskType>_publish",
|
||||
"asset": "<Asset>_<TaskType>_publish",
|
||||
"sequence": "<Sequence>_<TaskType>_publish",
|
||||
"style": "lowercase",
|
||||
},
|
||||
},
|
||||
"preview": {
|
||||
"mountpoint": "",
|
||||
"root": "",
|
||||
"folder_path": {
|
||||
"shot": "<Project>/shots/production/<Sequence>/<Shot>/<TaskType>/preview",
|
||||
"asset": "<Project>/assets/<AssetType>/<Asset>/<TaskType>/preview",
|
||||
"sequence": "<Project>/shots/production/<Sequence>/<TaskType>/preview",
|
||||
"style": "lowercase",
|
||||
},
|
||||
"file_name": {
|
||||
"shot": "<Shot>_<TaskType>_preview",
|
||||
"asset": "<Asset>_<TaskType>_preview",
|
||||
"sequence": "<Sequence>_<TaskType>_preview",
|
||||
"style": "lowercase",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..generic import ops
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def reload():
|
||||
global ops
|
||||
|
||||
ops = importlib.reload(ops)
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ops.unregister()
|
||||
@@ -1,79 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
|
||||
import bpy
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_open_path(bpy.types.Operator):
|
||||
bl_idname = "kitsu.open_path"
|
||||
bl_label = "Open"
|
||||
bl_description = "Opens specified path in the default system file browser"
|
||||
|
||||
filepath: bpy.props.StringProperty( # type: ignore
|
||||
name="Filepath",
|
||||
description="Filepath that will be opened in explorer",
|
||||
default="",
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
|
||||
if not self.filepath:
|
||||
self.report({"ERROR"}, "Can't open empty path in explorer")
|
||||
return {"CANCELLED"}
|
||||
|
||||
filepath = Path(self.filepath)
|
||||
if filepath.is_file():
|
||||
filepath = filepath.parent
|
||||
|
||||
if not filepath.exists():
|
||||
filepath = self._find_latest_existing_folder(filepath)
|
||||
|
||||
if sys.platform == "darwin":
|
||||
subprocess.check_call(["open", filepath.as_posix()])
|
||||
|
||||
elif sys.platform == "linux2" or sys.platform == "linux":
|
||||
subprocess.check_call(["xdg-open", filepath.as_posix()])
|
||||
|
||||
elif sys.platform == "win32":
|
||||
os.startfile(filepath.as_posix())
|
||||
|
||||
else:
|
||||
self.report(
|
||||
{"ERROR"}, f"Can't open explorer. Unsupported platform {sys.platform}"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def _find_latest_existing_folder(self, path: Path) -> Path:
|
||||
if path.exists() and path.is_dir():
|
||||
return path
|
||||
else:
|
||||
return self._find_latest_existing_folder(path.parent)
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [KITSU_OT_open_path]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,47 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import logging
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class LoggerFactory:
|
||||
|
||||
"""
|
||||
Utility class to streamline logger creation
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def getLogger(name="blender-kitsu"):
|
||||
name = name
|
||||
logger = logging.getLogger(name)
|
||||
return logger
|
||||
|
||||
|
||||
logger = LoggerFactory.getLogger(__name__)
|
||||
|
||||
|
||||
class LoggerLevelManager:
|
||||
logger_levels: List[Tuple[logging.Logger, int]] = []
|
||||
|
||||
@classmethod
|
||||
def configure_levels(cls):
|
||||
cls.logger_levels = []
|
||||
for key in logging.Logger.manager.loggerDict:
|
||||
if key.startswith("urllib3"):
|
||||
# Save logger and value.
|
||||
log = logging.getLogger(key)
|
||||
cls.logger_levels.append((log, logger.level))
|
||||
|
||||
log.setLevel(logging.CRITICAL)
|
||||
|
||||
# Set root logger level.
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
logger.info("Configured logging Levels")
|
||||
|
||||
@classmethod
|
||||
def restore_levels(cls):
|
||||
for logger, level in cls.logger_levels:
|
||||
logger.setLevel(level)
|
||||
logger.info("Restored logging Levels")
|
||||
@@ -1,41 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..lookdev import prefs
|
||||
from ..lookdev import props
|
||||
from ..lookdev import opsdata
|
||||
from ..lookdev import ops
|
||||
from ..lookdev import ui
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def reload():
|
||||
global prefs
|
||||
global props
|
||||
global opsdata
|
||||
global ops
|
||||
global ui
|
||||
|
||||
prefs = importlib.reload(prefs)
|
||||
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()
|
||||
|
||||
|
||||
def unregister():
|
||||
ops.unregister()
|
||||
ui.unregister()
|
||||
props.unregister()
|
||||
prefs.unregister()
|
||||
@@ -1,106 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib.util
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
from .. import prefs, util
|
||||
from . import opsdata
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_lookdev_set_preset(bpy.types.Operator):
|
||||
bl_idname = "kitsu.lookdev_set_preset"
|
||||
bl_label = "Render Preset"
|
||||
bl_property = "files"
|
||||
bl_description = "Sets active render settings preset that can be applied"
|
||||
|
||||
files: bpy.props.EnumProperty(items=opsdata.get_rd_settings_enum_list, name="Files")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
return addon_prefs.lookdev.is_presets_dir_valid
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
file = self.files
|
||||
|
||||
if not file:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if context.scene.lookdev.preset_file == file:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Update global scene cache version prop.
|
||||
context.scene.lookdev.preset_file = file
|
||||
logger.info("Set render preset file to %s", file)
|
||||
|
||||
# Redraw ui.
|
||||
util.ui_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
context.window_manager.invoke_search_popup(self) # type: ignore
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_lookdev_apply_preset(bpy.types.Operator):
|
||||
bl_idname = "kitsu.lookdev_apply_preset"
|
||||
bl_label = "Apply Preset"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Applies active render settings preset"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(context.scene.lookdev.preset_file)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
preset_file = context.scene.lookdev.preset_file
|
||||
preset_path = Path(preset_file).absolute()
|
||||
|
||||
if not preset_file:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Load module.
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
preset_path.name, preset_path.as_posix()
|
||||
)
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Exec module main function.
|
||||
if "main" not in dir(module):
|
||||
self.report(
|
||||
{"ERROR"}, f"{preset_path.name} does not contain a 'main' function"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
module.main()
|
||||
self.report({"INFO"}, f"Applied: {preset_path.name}")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [KITSU_OT_lookdev_set_preset, KITSU_OT_lookdev_apply_preset]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,109 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..models import FileListModel
|
||||
from ..logger import LoggerFactory
|
||||
from .. import prefs
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
RD_PRESET_FILE_MODEL = FileListModel()
|
||||
_rd_preset_enum_list: List[Tuple[str, str, str]] = []
|
||||
_rd_preset_file_model_init: bool = False
|
||||
# we need a second data dict because we want the enum properties data value to be the filepath
|
||||
# but the ui (not only in enum dropdown mode) should display the label defined in the .py
|
||||
# file with 'bl_label'. This dict is basically a mapping from filepath > label
|
||||
|
||||
_rd_preset_data_dict: Dict[str, str] = {}
|
||||
|
||||
|
||||
def init_rd_preset_file_model(
|
||||
context: bpy.types.Context,
|
||||
) -> None:
|
||||
global RD_PRESET_FILE_MODEL
|
||||
global _rd_preset_file_model_init
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
# Is None if invalid.
|
||||
if not addon_prefs.lookdev.is_presets_dir_valid:
|
||||
logger.error(
|
||||
"Failed to initialize render settings file model. Invalid path. Check addon preferences"
|
||||
)
|
||||
return
|
||||
|
||||
rd_settings_dir = addon_prefs.lookdev.presets_dir_path
|
||||
|
||||
RD_PRESET_FILE_MODEL.reset()
|
||||
RD_PRESET_FILE_MODEL.root_path = rd_settings_dir
|
||||
valid_items = [file for file in RD_PRESET_FILE_MODEL.items_as_paths if file.suffix == ".py"]
|
||||
if not valid_items:
|
||||
# Update playblast_version prop.
|
||||
context.scene.lookdev.preset_file = ""
|
||||
|
||||
else:
|
||||
# Update playblast_version prop.
|
||||
context.scene.lookdev.preset_file = valid_items[0].as_posix()
|
||||
|
||||
_rd_preset_file_model_init = True
|
||||
|
||||
|
||||
def get_rd_settings_enum_list(
|
||||
self: Any,
|
||||
context: bpy.types.Context,
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _rd_preset_enum_list
|
||||
global RD_PRESET_FILE_MODEL
|
||||
global init_rd_preset_file_model
|
||||
global _rd_preset_file_model_init
|
||||
global _rd_preset_data_dict
|
||||
|
||||
# Init model if it did not happen.
|
||||
if not _rd_preset_file_model_init:
|
||||
init_rd_preset_file_model(context)
|
||||
|
||||
# Reload model to update.
|
||||
RD_PRESET_FILE_MODEL.reload()
|
||||
|
||||
# Get all Python files. Ignore hidden files.
|
||||
py_files = [
|
||||
f
|
||||
for f in RD_PRESET_FILE_MODEL.items_as_paths
|
||||
if f.suffix == ".py" and not f.name.startswith('.')
|
||||
]
|
||||
py_labels: List[Tuple[Path, str]] = []
|
||||
|
||||
# Get bl_label of each python file, if not use file name as label.
|
||||
for file in py_files:
|
||||
spec = importlib.util.spec_from_file_location(file.name, file.as_posix())
|
||||
|
||||
# Load module.
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if "bl_label" not in dir(module):
|
||||
py_labels.append((file, file.name))
|
||||
continue
|
||||
py_labels.append((file, module.bl_label))
|
||||
|
||||
# Generate final enum list and dict from py_labels.
|
||||
enum_list = []
|
||||
data_dict = {}
|
||||
for file, label in py_labels:
|
||||
data_dict[file.name] = label
|
||||
enum_list.append((file.as_posix(), label, ""))
|
||||
|
||||
# Update global variables.
|
||||
_rd_preset_data_dict.clear()
|
||||
_rd_preset_data_dict.update(data_dict)
|
||||
_rd_preset_enum_list.clear()
|
||||
_rd_preset_enum_list.extend(enum_list)
|
||||
|
||||
print(data_dict)
|
||||
return _rd_preset_enum_list
|
||||
@@ -1,87 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from .. import prefs
|
||||
from ..props import get_safely_string_prop
|
||||
import bpy
|
||||
|
||||
from . import opsdata
|
||||
|
||||
|
||||
class LOOKDEV_preferences(bpy.types.PropertyGroup):
|
||||
|
||||
"""
|
||||
Addon preferences for lookdev module.
|
||||
"""
|
||||
|
||||
def update_rd_preset_file_model(self, context: bpy.types.Context) -> None:
|
||||
opsdata.init_rd_preset_file_model(context)
|
||||
|
||||
def set_look_dev_dir(self, input):
|
||||
self['presets_dir'] = input
|
||||
return
|
||||
|
||||
def get_look_dev_dir(self) -> str:
|
||||
proj_root = prefs.project_root_dir_get(bpy.context)
|
||||
if get_safely_string_prop(self, 'presets_dir') == "" and proj_root:
|
||||
frames_dir = proj_root.joinpath("pro/assets/scripts/render_presets/")
|
||||
if frames_dir.exists():
|
||||
return frames_dir.as_posix()
|
||||
return get_safely_string_prop(self, 'presets_dir')
|
||||
|
||||
presets_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Render Presets Directory",
|
||||
description="Directory path to folder in which render settings python files are stored",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
update=update_rd_preset_file_model,
|
||||
get=get_look_dev_dir,
|
||||
set=set_look_dev_dir,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_presets_dir_valid(self) -> bool:
|
||||
# Check if file is saved.
|
||||
if not self.presets_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.presets_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def presets_dir_path(self) -> Optional[Path]:
|
||||
if not self.presets_dir:
|
||||
return None
|
||||
return Path(os.path.abspath(bpy.path.abspath(self.presets_dir)))
|
||||
|
||||
def draw(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
) -> None:
|
||||
# Render preset.
|
||||
box = layout.box()
|
||||
box.label(text="Lookdev Tools", icon="RESTRICT_RENDER_OFF")
|
||||
box.row().prop(self, "presets_dir")
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [LOOKDEV_preferences]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Unregister classes.
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,42 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class LOOKDEV_property_group_scene(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
# Render settings.
|
||||
preset_file: bpy.props.StringProperty( # type: ignore
|
||||
name="Render Settings File",
|
||||
description="Path to file that is the active render settings preset",
|
||||
default="",
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
LOOKDEV_property_group_scene,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Scene Properties.
|
||||
bpy.types.Scene.lookdev = bpy.props.PointerProperty(
|
||||
name="Render Preset",
|
||||
type=LOOKDEV_property_group_scene,
|
||||
description="Metadata that is required for lookdev",
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,97 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import prefs, lookdev, ui, cache
|
||||
from .ops import (
|
||||
KITSU_OT_lookdev_set_preset,
|
||||
KITSU_OT_lookdev_apply_preset,
|
||||
)
|
||||
from . import opsdata
|
||||
|
||||
|
||||
class KITSU_PT_vi3d_lookdev_tools(bpy.types.Panel):
|
||||
"""
|
||||
Panel in 3dview that exposes a set of tools that are useful for general tasks
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Lookdev Tools"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 60
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(
|
||||
cache.task_type_active_get().name
|
||||
in ["Lighting", "Rendering", "Compositing"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll_error(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
return bool(not addon_prefs.lookdev.is_presets_dir_valid)
|
||||
|
||||
def draw_error(self, context: bpy.types.Context) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
layout = self.layout
|
||||
box = ui.draw_error_box(layout)
|
||||
|
||||
if not addon_prefs.lookdev.is_presets_dir_valid:
|
||||
ui.draw_error_invalid_render_preset_dir(box)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
if self.poll_error(context):
|
||||
self.draw_error(context)
|
||||
|
||||
box = layout.box()
|
||||
box.label(text="Render Settings", icon="RESTRICT_RENDER_OFF")
|
||||
|
||||
# Render settings.
|
||||
row = box.row(align=True)
|
||||
rdpreset_text = "Select Render Preset"
|
||||
if context.scene.lookdev.preset_file:
|
||||
try:
|
||||
rdpreset_text = opsdata._rd_preset_data_dict[
|
||||
Path(context.scene.lookdev.preset_file).name
|
||||
]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
row.operator(
|
||||
KITSU_OT_lookdev_set_preset.bl_idname,
|
||||
text=rdpreset_text,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
row.operator(
|
||||
KITSU_OT_lookdev_apply_preset.bl_idname,
|
||||
text="",
|
||||
icon="PLAY",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_PT_comp_lookdev_tools(KITSU_PT_vi3d_lookdev_tools):
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
|
||||
|
||||
# ---------REGISTER ----------
|
||||
# Classes that inherit from another need to be registered first for some reason.
|
||||
classes = [KITSU_PT_comp_lookdev_tools, KITSU_PT_vi3d_lookdev_tools]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,191 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Union, Optional, Dict, List, Tuple
|
||||
|
||||
from . import bkglobals
|
||||
from .logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger(__name__)
|
||||
|
||||
|
||||
class FolderListModel:
|
||||
def __init__(self):
|
||||
self.__root_path: Optional[Path] = None
|
||||
self.__folders: List[str] = []
|
||||
self.__appended: List[str] = []
|
||||
self.__combined: List[str] = []
|
||||
|
||||
def rowCount(self) -> int:
|
||||
return len(self.__combined)
|
||||
|
||||
def data(self, row: int) -> Optional[str]:
|
||||
if len(self.__combined) > 0:
|
||||
return self.__combined[row]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def root_path(self) -> Optional[Path]:
|
||||
return self.__root_path
|
||||
|
||||
@root_path.setter
|
||||
def root_path(self, path: Path) -> None:
|
||||
if not path or not path.absolute().exists():
|
||||
logger.debug("FolderListModel: Path does not exist: %s", str(path))
|
||||
self.reset()
|
||||
else:
|
||||
self.__root_path = path
|
||||
logger.debug("FolderListModel: Root path was set to %s", path.as_posix())
|
||||
self.__load_dir(self.__root_path)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.__root_path = None
|
||||
self.__folders.clear()
|
||||
self.__appended.clear()
|
||||
self.__update_combined()
|
||||
|
||||
def reload(self) -> None:
|
||||
self.__folders.clear()
|
||||
self.__appended.clear()
|
||||
self.root_path = self.__root_path
|
||||
|
||||
def __load_dir(self, path: Path) -> None:
|
||||
self.__folders = self.__detect_folders(path)
|
||||
self.__appended.clear()
|
||||
self.__update_combined()
|
||||
|
||||
def __detect_folders(self, path: Path) -> List[str]:
|
||||
if path.exists() and path.is_dir():
|
||||
# Iterate through directory and return all pathes that are dirs, only return their name.
|
||||
return sorted(
|
||||
[str(x.name) for x in path.iterdir() if x.is_dir()], reverse=True
|
||||
)
|
||||
else:
|
||||
return []
|
||||
|
||||
def append_item(self, item: str) -> None:
|
||||
self.__appended.append(item)
|
||||
self.__update_combined()
|
||||
|
||||
def __update_combined(self) -> None:
|
||||
self.__combined.clear()
|
||||
self.__combined.extend(
|
||||
sorted(list(set(self.__folders + self.__appended)), reverse=True)
|
||||
)
|
||||
|
||||
@property
|
||||
def items(self) -> List[str]:
|
||||
return self.__combined
|
||||
|
||||
@property
|
||||
def items_as_enum_list(self) -> List[Tuple[str, str, str]]:
|
||||
return [(item, item, "") for item in self.__combined]
|
||||
|
||||
|
||||
class FileListModel:
|
||||
|
||||
filter_name: str = ""
|
||||
def __init__(self):
|
||||
self.__root_path: Optional[Path] = None
|
||||
self.__files: List[str] = []
|
||||
self.__appended: List[str] = []
|
||||
self.__combined: List[str] = []
|
||||
|
||||
def rowCount(self) -> int:
|
||||
return len(self.__combined)
|
||||
|
||||
def data(self, row: int) -> Optional[str]:
|
||||
if len(self.__combined) > 0:
|
||||
return self.__combined[row]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def root_path(self) -> Optional[Path]:
|
||||
return self.__root_path
|
||||
|
||||
@root_path.setter
|
||||
def root_path(self, path: Path) -> None:
|
||||
if not path or not path.absolute().exists():
|
||||
logger.debug("FileListModel: Path does not exist: %s", str(path))
|
||||
self.reset()
|
||||
else:
|
||||
self.__root_path = path
|
||||
self.__load_dir(self.__root_path)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.__root_path = None
|
||||
self.__files.clear()
|
||||
self.__appended.clear()
|
||||
self.__update_combined()
|
||||
|
||||
def reload(self) -> None:
|
||||
self.__files.clear()
|
||||
self.__appended.clear()
|
||||
self.root_path = self.__root_path
|
||||
|
||||
def __load_dir(self, path: Path) -> None:
|
||||
self.__files = self.__detect_files(path)
|
||||
self.__appended.clear()
|
||||
self.__update_combined()
|
||||
|
||||
def __detect_files(self, path: Path) -> List[str]:
|
||||
if path.exists() and path.is_dir():
|
||||
# Iterate through directory and return all pathes that are files, only return their name.
|
||||
return sorted(
|
||||
[str(x.name) for x in path.iterdir() if x.is_file() and self.filter_name in x.name],
|
||||
reverse=True,
|
||||
)
|
||||
else:
|
||||
return []
|
||||
|
||||
def append_item(self, item: str) -> None:
|
||||
self.__appended.append(item)
|
||||
self.__update_combined()
|
||||
|
||||
def __update_combined(self) -> None:
|
||||
self.__combined.clear()
|
||||
self.__combined.extend(
|
||||
sorted(list(set(self.__files + self.__appended)), reverse=True)
|
||||
)
|
||||
|
||||
@property
|
||||
def items(self) -> List[str]:
|
||||
return self.__combined
|
||||
|
||||
@property
|
||||
def items_as_paths(self) -> List[Path]:
|
||||
if not self.__root_path:
|
||||
return []
|
||||
return [self.__root_path.joinpath(item).absolute() for item in self.items]
|
||||
|
||||
@property
|
||||
def items_as_enum_list(self) -> List[Tuple[str, str, str]]:
|
||||
return [(item, item, "") for item in self.__combined]
|
||||
|
||||
@property
|
||||
def items_as_path_enum_list(self) -> List[Tuple[str, str, str]]:
|
||||
return [(item.as_posix(), item.name, "") for item in self.items_as_paths]
|
||||
|
||||
@property
|
||||
def versions(self) -> List[str]:
|
||||
return [self._get_version(i) for i in self.__combined if self._get_version(i)]
|
||||
|
||||
@property
|
||||
def versions_as_enum_list(self) -> List[Tuple[str, str, str]]:
|
||||
return [(v, v, "") for v in self.versions]
|
||||
|
||||
def _get_version(self, str_value: str, format: type = str) -> Union[str, int, None]:
|
||||
match = re.search(bkglobals.VERSION_PATTERN, str_value)
|
||||
if match:
|
||||
version = match.group()
|
||||
if format == str:
|
||||
return version
|
||||
if format == int:
|
||||
return int(version.replace("v", ""))
|
||||
return None
|
||||
@@ -1,27 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..playblast 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()
|
||||
@@ -1,320 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from bpy.types import Context
|
||||
import contextlib
|
||||
from typing import Tuple
|
||||
from .. import prefs, cache
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_render_format(self, context: Context, enable_sequencer: bool = False):
|
||||
"""Overrides the render settings for playblast creation"""
|
||||
rd = context.scene.render
|
||||
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# For Blender 5.0 and later, use the new FFMPEG settings.
|
||||
media_type = rd.image_settings.media_type
|
||||
|
||||
use_sequencer = rd.use_sequencer = enable_sequencer
|
||||
# Format render settings.
|
||||
# percentage = rd.resolution_percentage
|
||||
file_format = rd.image_settings.file_format
|
||||
ffmpeg_constant_rate = rd.ffmpeg.constant_rate_factor
|
||||
ffmpeg_codec = rd.ffmpeg.codec
|
||||
ffmpeg_format = rd.ffmpeg.format
|
||||
ffmpeg_audio_codec = rd.ffmpeg.audio_codec
|
||||
|
||||
try:
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# For Blender 5.0 and later, use the new FFMPEG settings.
|
||||
rd.image_settings.media_type = "VIDEO"
|
||||
|
||||
# rd.resolution_percentage = 100
|
||||
rd.use_sequencer = enable_sequencer
|
||||
rd.image_settings.file_format = "FFMPEG"
|
||||
rd.ffmpeg.constant_rate_factor = "HIGH"
|
||||
rd.ffmpeg.codec = "H264"
|
||||
rd.ffmpeg.format = "MPEG4"
|
||||
rd.ffmpeg.audio_codec = "AAC"
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# rd.resolution_percentage = percentage
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# For Blender 5.0 and later, use the new FFMPEG settings.
|
||||
rd.image_settings.media_type = media_type
|
||||
|
||||
rd.use_sequencer = use_sequencer
|
||||
rd.image_settings.file_format = file_format
|
||||
rd.ffmpeg.codec = ffmpeg_codec
|
||||
rd.ffmpeg.constant_rate_factor = ffmpeg_constant_rate
|
||||
rd.ffmpeg.format = ffmpeg_format
|
||||
rd.ffmpeg.audio_codec = ffmpeg_audio_codec
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_render_path(self, context: Context, render_file_path: str):
|
||||
"""Overrides the render settings for playblast creation"""
|
||||
rd = context.scene.render
|
||||
# Filepath.
|
||||
filepath = rd.filepath
|
||||
|
||||
try:
|
||||
# Filepath.
|
||||
rd.filepath = render_file_path
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Filepath.
|
||||
rd.filepath = filepath
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_hide_viewport_overlays(self, context: Context):
|
||||
sp = context.space_data
|
||||
show_gizmo = sp.show_gizmo
|
||||
show_overlays = sp.overlay.show_overlays
|
||||
|
||||
try:
|
||||
sp.show_gizmo = False
|
||||
sp.overlay.show_overlays = False
|
||||
|
||||
yield
|
||||
finally:
|
||||
sp.show_gizmo = show_gizmo
|
||||
sp.overlay.show_overlays = show_overlays
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_metadata_stamp_settings(self, context: Context, username: str = "None"):
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
if addon_prefs.pb_manual_burn_in:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
return
|
||||
|
||||
rd = context.scene.render
|
||||
|
||||
# Get shot name for stamp note text.
|
||||
shot = cache.shot_active_get()
|
||||
|
||||
# Remember current render settings in order to restore them later.
|
||||
|
||||
# Stamp metadata settings.
|
||||
metadata_input = rd.metadata_input
|
||||
use_stamp_date = rd.use_stamp_date
|
||||
use_stamp_time = rd.use_stamp_time
|
||||
use_stamp_render_time = rd.use_stamp_render_time
|
||||
use_stamp_frame = rd.use_stamp_frame
|
||||
use_stamp_frame_range = rd.use_stamp_frame_range
|
||||
use_stamp_memory = rd.use_stamp_memory
|
||||
use_stamp_hostname = rd.use_stamp_hostname
|
||||
use_stamp_camera = rd.use_stamp_camera
|
||||
use_stamp_lens = rd.use_stamp_lens
|
||||
use_stamp_scene = rd.use_stamp_scene
|
||||
use_stamp_marker = rd.use_stamp_marker
|
||||
use_stamp_marker = rd.use_stamp_marker
|
||||
use_stamp_note = rd.use_stamp_note
|
||||
stamp_note_text = rd.stamp_note_text
|
||||
use_stamp = rd.use_stamp
|
||||
# stamp_font_size = rd.stamp_font_size
|
||||
stamp_foreground = rd.stamp_foreground
|
||||
stamp_background = rd.stamp_background
|
||||
use_stamp_labels = rd.use_stamp_labels
|
||||
try:
|
||||
# Stamp metadata settings.
|
||||
rd.metadata_input = "SCENE"
|
||||
rd.use_stamp_date = False
|
||||
rd.use_stamp_time = False
|
||||
rd.use_stamp_render_time = False
|
||||
rd.use_stamp_frame = True
|
||||
rd.use_stamp_frame_range = False
|
||||
rd.use_stamp_memory = False
|
||||
rd.use_stamp_hostname = False
|
||||
rd.use_stamp_camera = False
|
||||
rd.use_stamp_lens = True
|
||||
rd.use_stamp_scene = False
|
||||
rd.use_stamp_marker = False
|
||||
rd.use_stamp_marker = False
|
||||
rd.use_stamp_note = True
|
||||
rd.stamp_note_text = f"Shot: {shot.name} | Animator: {username}"
|
||||
rd.use_stamp = True
|
||||
rd.stamp_font_size = 24
|
||||
rd.stamp_foreground = (0.8, 0.8, 0.8, 1)
|
||||
rd.stamp_background = (0, 0, 0, 0.25)
|
||||
rd.use_stamp_labels = True
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Stamp metadata settings.
|
||||
rd.metadata_input = metadata_input
|
||||
rd.use_stamp_date = use_stamp_date
|
||||
rd.use_stamp_time = use_stamp_time
|
||||
rd.use_stamp_render_time = use_stamp_render_time
|
||||
rd.use_stamp_frame = use_stamp_frame
|
||||
rd.use_stamp_frame_range = use_stamp_frame_range
|
||||
rd.use_stamp_memory = use_stamp_memory
|
||||
rd.use_stamp_hostname = use_stamp_hostname
|
||||
rd.use_stamp_camera = use_stamp_camera
|
||||
rd.use_stamp_lens = use_stamp_lens
|
||||
rd.use_stamp_scene = use_stamp_scene
|
||||
rd.use_stamp_marker = use_stamp_marker
|
||||
rd.use_stamp_marker = use_stamp_marker
|
||||
rd.use_stamp_note = use_stamp_note
|
||||
rd.stamp_note_text = stamp_note_text
|
||||
rd.use_stamp = use_stamp
|
||||
# rd.stamp_font_size = stamp_font_size
|
||||
rd.stamp_foreground = stamp_foreground
|
||||
rd.stamp_background = stamp_background
|
||||
rd.use_stamp_labels = use_stamp_labels
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_viewport_shading(self, context: Context):
|
||||
"""Overrides the render settings for playblast creation"""
|
||||
rd = context.scene.render
|
||||
sps = context.space_data.shading
|
||||
sp = context.space_data
|
||||
|
||||
# Space data settings.
|
||||
shading_type = sps.type
|
||||
shading_light = sps.light
|
||||
studio_light = sps.studio_light
|
||||
color_type = sps.color_type
|
||||
background_type = sps.background_type
|
||||
|
||||
show_backface_culling = sps.show_backface_culling
|
||||
show_xray = sps.show_xray
|
||||
show_shadows = sps.show_shadows
|
||||
show_cavity = sps.show_cavity
|
||||
show_object_outline = sps.show_object_outline
|
||||
show_specular_highlight = sps.show_specular_highlight
|
||||
|
||||
try:
|
||||
# Space data settings.
|
||||
sps.type = "SOLID"
|
||||
sps.light = "STUDIO"
|
||||
sps.studio_light = "paint.sl"
|
||||
sps.color_type = "MATERIAL"
|
||||
sps.background_type = "THEME"
|
||||
|
||||
sps.show_backface_culling = False
|
||||
sps.show_xray = False
|
||||
sps.show_shadows = False
|
||||
sps.show_cavity = False
|
||||
sps.show_object_outline = False
|
||||
sps.show_specular_highlight = True
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Space data settings.
|
||||
sps.type = shading_type
|
||||
sps.light = shading_light
|
||||
sps.studio_light = studio_light
|
||||
sps.color_type = color_type
|
||||
sps.background_type = background_type
|
||||
|
||||
sps.show_backface_culling = show_backface_culling
|
||||
sps.show_xray = show_xray
|
||||
sps.show_shadows = show_shadows
|
||||
sps.show_cavity = show_cavity
|
||||
sps.show_object_outline = show_object_outline
|
||||
sps.show_specular_highlight = show_specular_highlight
|
||||
|
||||
|
||||
def ensure_render_path(file_path: str) -> Path:
|
||||
output_path = Path(file_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return output_path
|
||||
|
||||
|
||||
def playblast_with_scene_settings(self, context: Context, file_path: str, username: str) -> Path:
|
||||
with override_render_path(self, context, file_path):
|
||||
with override_render_format(self, context):
|
||||
with override_metadata_stamp_settings(self, context, username):
|
||||
output_path = ensure_render_path(file_path)
|
||||
bpy.ops.render.render(animation=True, use_viewport=False)
|
||||
return output_path
|
||||
|
||||
|
||||
def playblast_with_viewport_settings(self, context: Context, file_path: str, username: str) -> Path:
|
||||
with override_render_path(self, context, file_path):
|
||||
with override_render_format(self, context):
|
||||
with override_metadata_stamp_settings(self, context, username):
|
||||
output_path = ensure_render_path(file_path)
|
||||
bpy.ops.render.opengl(animation=True)
|
||||
return output_path
|
||||
|
||||
|
||||
def playblast_with_viewport_preset_settings(
|
||||
self, context: Context, file_path: str, username: str
|
||||
) -> Path:
|
||||
with override_render_path(self, context, file_path):
|
||||
with override_render_format(self, context):
|
||||
with override_metadata_stamp_settings(self, context, username):
|
||||
with override_viewport_shading(self, context):
|
||||
with override_hide_viewport_overlays(self, context):
|
||||
output_path = ensure_render_path(file_path)
|
||||
bpy.ops.render.opengl(animation=True)
|
||||
return output_path
|
||||
|
||||
|
||||
def playblast_vse(self, context: Context, file_path: str) -> Path:
|
||||
with override_render_path(self, context, file_path):
|
||||
with override_render_format(self, context, enable_sequencer=True):
|
||||
output_path = ensure_render_path(file_path)
|
||||
bpy.ops.render.opengl(animation=True, sequencer=True)
|
||||
return output_path
|
||||
|
||||
|
||||
def set_frame_range_in(frame_in: int) -> dict:
|
||||
shot = cache.shot_active_pull_update()
|
||||
shot.data["3d_start"] = frame_in
|
||||
shot.update()
|
||||
return shot
|
||||
|
||||
|
||||
def get_frame_range() -> Tuple[int, int]:
|
||||
active_shot = cache.shot_active_get()
|
||||
if not active_shot:
|
||||
return
|
||||
|
||||
# Pull update for shot.
|
||||
cache.shot_active_pull_update()
|
||||
kitsu_3d_start = active_shot.get_3d_start()
|
||||
frame_in = kitsu_3d_start
|
||||
frame_out = kitsu_3d_start + int(active_shot.nb_frames) - 1
|
||||
return frame_in, frame_out
|
||||
|
||||
|
||||
def check_frame_range(context: Context) -> bool:
|
||||
"""
|
||||
Compare the current scene's frame range with that of the active shot on kitsu.
|
||||
If there's a mismatch, set kitsu_error.frame_range -> True. This will enable
|
||||
a warning in the Animation Tools Tab UI.
|
||||
"""
|
||||
try:
|
||||
frame_in, frame_out = get_frame_range()
|
||||
except TypeError:
|
||||
return
|
||||
scene = context.scene
|
||||
|
||||
if frame_in == scene.frame_start and frame_out == scene.frame_end:
|
||||
scene.kitsu_error.frame_range = False
|
||||
return True
|
||||
|
||||
scene.kitsu_error.frame_range = True
|
||||
logger.warning("Current frame range is outdated!")
|
||||
return False
|
||||
@@ -1,680 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
import time
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from .. import bkglobals
|
||||
|
||||
from .. import (
|
||||
cache,
|
||||
util,
|
||||
prefs,
|
||||
bkglobals,
|
||||
)
|
||||
from ..logger import LoggerFactory
|
||||
from ..types import (
|
||||
Shot,
|
||||
Task,
|
||||
TaskStatus,
|
||||
TaskType,
|
||||
)
|
||||
from ..playblast.core import (
|
||||
playblast_with_scene_settings,
|
||||
playblast_with_viewport_settings,
|
||||
playblast_with_viewport_preset_settings,
|
||||
playblast_vse,
|
||||
)
|
||||
from ..context import core as context_core
|
||||
from ..playblast import opsdata, core
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_playblast_create(bpy.types.Operator):
|
||||
bl_idname = "kitsu.playblast_create"
|
||||
bl_label = "Create Playblast"
|
||||
bl_description = (
|
||||
"Creates render either from viewport in which operator was triggered"
|
||||
"or renderes with the current scene's render settings"
|
||||
"Saves the set version to disk and uploads it to Kitsu with the specified "
|
||||
"comment and task type."
|
||||
"Opens web browser or VSE after playblast if set in addon preferences"
|
||||
)
|
||||
|
||||
comment: bpy.props.StringProperty(
|
||||
name="Comment",
|
||||
description="Comment that will be appended to this playblast on Kitsu",
|
||||
default="",
|
||||
)
|
||||
confirm: bpy.props.BoolProperty(name="Confirm", default=False)
|
||||
|
||||
task_status: bpy.props.EnumProperty(items=cache.get_all_task_statuses_enum) # type: ignore
|
||||
|
||||
thumbnail_frame: bpy.props.IntProperty(
|
||||
name="Thumbnail Frame",
|
||||
description="Frame to use as the thumbnail on Kitsu",
|
||||
min=0,
|
||||
)
|
||||
thumbnail_frame_final: bpy.props.IntProperty(name="Thumbnail Frame Final")
|
||||
|
||||
_entity = None
|
||||
_task_status = None
|
||||
_task = None
|
||||
_task_type = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not prefs.session_auth(context):
|
||||
cls.poll_message_set("Not logged into Kitsu Server, see Add-On Preferences")
|
||||
return False
|
||||
|
||||
if not context.scene.kitsu.playblast_file:
|
||||
cls.poll_message_set("Invalid Playblast File/Directory, see Add-On Preferences")
|
||||
return False
|
||||
|
||||
if context.space_data.type == "VIEW_3D":
|
||||
if not context.scene.camera:
|
||||
cls.poll_message_set("No Active Camera in active Scene")
|
||||
return False
|
||||
|
||||
if context_core.is_sequence_context():
|
||||
if not cache.sequence_active_get():
|
||||
cls.poll_message_set("No Active Sequence set in Kitsu Context UI")
|
||||
return False
|
||||
|
||||
if context_core.is_shot_context():
|
||||
if not cache.shot_active_get():
|
||||
cls.poll_message_set("No Active Shot set in Kitsu Context UI")
|
||||
return False
|
||||
if not cache.task_type_active_get():
|
||||
cls.poll_message_set("No Active Task Type set in Kitsu Context UI")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_vse(self, context):
|
||||
return bool(context.space_data.type == "SEQUENCE_EDITOR")
|
||||
|
||||
def _get_kitsu_task(self, context: bpy.types.Context):
|
||||
task_type_name = cache.task_type_active_get().name
|
||||
|
||||
# Get task status 'wip' and task type 'Animation'.
|
||||
self._task_status = TaskStatus.by_id(self.task_status)
|
||||
self._task_type = TaskType.by_name(task_type_name)
|
||||
|
||||
if not self._task_type:
|
||||
raise RuntimeError("Failed to upload playblast. Task type missing on Kitsu Server")
|
||||
|
||||
# Find / get latest task
|
||||
self._task = Task.by_name(self._entity, self._task_type)
|
||||
if not self._task:
|
||||
# An Entity on the server can have 0 tasks even tough task types exist.
|
||||
# We have to create a task first before being able to upload a thumbnail.
|
||||
try:
|
||||
self._task = Task.new_task(
|
||||
self._entity, self._task_type, task_status=self._task_status
|
||||
)
|
||||
except TypeError:
|
||||
raise RuntimeError(
|
||||
f"Failed to upload playblast. Task type {self._task_type.name} not present in {self._entity.type} {self._entity.name}"
|
||||
)
|
||||
|
||||
def _get_user_name(self, context: bpy.types.Context) -> str:
|
||||
session = prefs.session_get(context)
|
||||
if len(self._task.persons) == 1:
|
||||
return self._task.persons[0]["full_name"]
|
||||
elif len(self._task.persons) >= 1:
|
||||
person = ""
|
||||
for index, user in enumerate(self._task.persons):
|
||||
person += user["full_name"]
|
||||
if index < len(self._task.persons) - 1:
|
||||
person += ", "
|
||||
return person
|
||||
else:
|
||||
return session.data.user["full_name"]
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
kitsu_scene_props = context.scene.kitsu
|
||||
render_mode = kitsu_scene_props.playblast_render_mode
|
||||
|
||||
if not self.task_status:
|
||||
self.report({"ERROR"}, "Failed to create playblast. Missing task status")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Playblast file always starts at frame 0, account for this in thumbnail frame selection
|
||||
self.thumbnail_frame_final = self.thumbnail_frame - context.scene.frame_start
|
||||
|
||||
# Ensure thumbnail frame is not outside of frame range
|
||||
if self.thumbnail_frame_final not in range(
|
||||
0, (context.scene.frame_end - context.scene.frame_start) + 1
|
||||
):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Thumbnail frame '{self.thumbnail_frame}' is outside of frame range ",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# entity is either a shot or a sequence
|
||||
self._entity = self._get_active_entity(context)
|
||||
|
||||
# get kitsu task info
|
||||
self._get_kitsu_task(context)
|
||||
|
||||
# Save playblast task status id for next time.
|
||||
kitsu_scene_props.playblast_task_status_id = self.task_status
|
||||
|
||||
logger.info("-START- Creating Playblast")
|
||||
|
||||
context.window_manager.progress_begin(0, 2)
|
||||
context.window_manager.progress_update(0)
|
||||
|
||||
playblast_file = kitsu_scene_props.playblast_file
|
||||
|
||||
username = self._get_user_name(context)
|
||||
|
||||
# Render and save playblast
|
||||
if self.is_vse(context):
|
||||
output_path = playblast_vse(self, context, playblast_file)
|
||||
else:
|
||||
if render_mode == "VIEWPORT":
|
||||
output_path = playblast_with_viewport_settings(
|
||||
self, context, playblast_file, username
|
||||
)
|
||||
elif render_mode == "VIEWPORT_PRESET":
|
||||
output_path = playblast_with_viewport_preset_settings(
|
||||
self, context, playblast_file, username
|
||||
)
|
||||
else: # render_mode == "SCENE":
|
||||
output_path = playblast_with_scene_settings(self, context, playblast_file, username)
|
||||
|
||||
context.window_manager.progress_update(1)
|
||||
|
||||
# Upload playblast
|
||||
self._upload_playblast(context, output_path)
|
||||
|
||||
if not addon_prefs.version_control:
|
||||
basename = context_core.get_versioned_file_basename(Path(bpy.data.filepath).stem)
|
||||
|
||||
version_filename = basename + "-" + kitsu_scene_props.playblast_version + ".blend"
|
||||
version_filepath = Path(bpy.data.filepath).parent.joinpath(version_filename).as_posix()
|
||||
bpy.ops.wm.save_as_mainfile(filepath=version_filepath, copy=True)
|
||||
|
||||
context.window_manager.progress_update(2)
|
||||
context.window_manager.progress_end()
|
||||
|
||||
self.report({"INFO"}, f"Created and uploaded playblast for {self._entity.name}")
|
||||
logger.info("-END- Creating Playblast")
|
||||
|
||||
# Redraw UI
|
||||
util.ui_redraw()
|
||||
|
||||
# Post playblast
|
||||
|
||||
# Open web browser
|
||||
if addon_prefs.pb_open_webbrowser:
|
||||
self._open_webbrowser()
|
||||
|
||||
# Open playblast in second scene video sequence editor.
|
||||
if addon_prefs.pb_open_vse:
|
||||
# Create new scene.
|
||||
scene_orig = bpy.context.scene
|
||||
try:
|
||||
scene_pb = bpy.data.scenes[bkglobals.SCENE_NAME_PLAYBLAST]
|
||||
except KeyError:
|
||||
# Create scene.
|
||||
bpy.ops.scene.new(type="EMPTY") # changes active scene
|
||||
scene_pb = bpy.context.scene
|
||||
scene_pb.name = bkglobals.SCENE_NAME_PLAYBLAST
|
||||
|
||||
logger.info(
|
||||
"Created new scene for playblast playback: %s", scene_pb.name
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Use existing scene for playblast playback: %s", scene_pb.name
|
||||
)
|
||||
# Change scene.
|
||||
context.window.scene = scene_pb
|
||||
|
||||
# Init video sequence editor.
|
||||
if not context.scene.sequence_editor:
|
||||
context.scene.sequence_editor_create() # what the hell
|
||||
|
||||
# Setup video sequence editor space.
|
||||
if "Video Editing" not in [ws.name for ws in bpy.data.workspaces]:
|
||||
scripts_path = bpy.utils.script_paths(use_user=False)[0]
|
||||
template_path = (
|
||||
"/startup/bl_app_templates_system/Video_Editing/startup.blend"
|
||||
)
|
||||
ws_filepath = Path(scripts_path + template_path)
|
||||
bpy.ops.workspace.append_activate(
|
||||
idname="Video Editing",
|
||||
filepath=ws_filepath.as_posix(),
|
||||
)
|
||||
else:
|
||||
context.window.workspace = bpy.data.workspaces["Video Editing"]
|
||||
|
||||
# Add movie strip
|
||||
# load movie strip file in sequence editor
|
||||
# in this case we make use of ops.sequencer.movie_strip_add because
|
||||
# it provides handy auto placing,would be hard to achieve with
|
||||
# context.scene.sequence_editor.sequences.new_movie().
|
||||
override = context.copy()
|
||||
for window in bpy.context.window_manager.windows:
|
||||
screen = window.screen
|
||||
|
||||
for area in screen.areas:
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
override["window"] = window
|
||||
override["screen"] = screen
|
||||
override["area"] = area
|
||||
|
||||
bpy.ops.sequencer.movie_strip_add(
|
||||
override,
|
||||
filepath=scene_orig.kitsu.playblast_file,
|
||||
frame_start=context.scene.frame_start,
|
||||
)
|
||||
|
||||
# Playback.
|
||||
context.scene.frame_current = context.scene.frame_start
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Initialize comment and playblast task status variable.
|
||||
self.comment = ""
|
||||
|
||||
prev_task_status_id = context.scene.kitsu.playblast_task_status_id
|
||||
|
||||
if context.scene.frame_current not in range(
|
||||
context.scene.frame_start, context.scene.frame_end
|
||||
):
|
||||
context.scene.frame_current = context.scene.frame_start
|
||||
|
||||
self.thumbnail_frame = context.scene.frame_current
|
||||
|
||||
# Only use prev_task_status_id if it exists in the task statuses enum list
|
||||
valid_ids = [status[0] for status in cache.get_all_task_statuses_enum(self, context)]
|
||||
if prev_task_status_id in valid_ids:
|
||||
self.task_status = prev_task_status_id
|
||||
else:
|
||||
# Find todo.
|
||||
todo_status = TaskStatus.by_name(bkglobals.PLAYBLAST_DEFAULT_STATUS)
|
||||
if todo_status:
|
||||
self.task_status = todo_status.id
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.prop(self, "task_status", text="Status")
|
||||
layout.prop(self, "comment")
|
||||
layout.prop(self, "thumbnail_frame")
|
||||
if not self.is_vse(context):
|
||||
layout.prop(context.scene.kitsu, "playblast_render_mode", text="Render Mode")
|
||||
|
||||
def _upload_playblast(self, context: bpy.types.Context, filepath: Path) -> None:
|
||||
# Create a comment
|
||||
comment_text = self._gen_comment_text(context, self._entity)
|
||||
comment = self._task.add_comment(
|
||||
self._task_status,
|
||||
comment=comment_text,
|
||||
)
|
||||
|
||||
# Add_preview_to_comment
|
||||
self._task.add_preview_to_comment(
|
||||
comment,
|
||||
filepath.as_posix(),
|
||||
self.thumbnail_frame_final,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Uploaded playblast for shot: {self._entity.name} under: {self._task_type.name}"
|
||||
)
|
||||
|
||||
def _gen_comment_text(self, context: bpy.types.Context, shot: Shot) -> str:
|
||||
header = f"Playblast {shot.name}: {context.scene.kitsu.playblast_version}"
|
||||
if self.comment:
|
||||
return header + f"\n\n{self.comment}"
|
||||
return header
|
||||
|
||||
def _open_webbrowser(self) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
# https://staging.kitsu.blender.cloud/productions/7838e728-312b-499a-937b-e22273d097aa/shots?search=010_0010_A
|
||||
|
||||
host_url = addon_prefs.host
|
||||
if host_url.endswith("/api"):
|
||||
host_url = host_url[:-4]
|
||||
|
||||
if host_url.endswith("/"):
|
||||
host_url = host_url[:-1]
|
||||
|
||||
url = f"{host_url}/productions/{cache.project_active_get().id}/shots?search={cache.shot_active_get().name}"
|
||||
webbrowser.open(url)
|
||||
|
||||
def _get_active_entity(self, context):
|
||||
if context_core.is_sequence_context():
|
||||
# Get sequence
|
||||
return cache.sequence_active_get()
|
||||
elif context_core.is_asset_context():
|
||||
return cache.asset_active_get()
|
||||
else:
|
||||
# Get shot.
|
||||
return cache.shot_active_get()
|
||||
|
||||
|
||||
class KITSU_OT_playblast_set_version(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_set_playblast_version"
|
||||
bl_label = "Version"
|
||||
bl_property = "versions"
|
||||
bl_description = (
|
||||
"Sets version of playblast. Warning triangle in ui "
|
||||
"indicates that version already exists on disk"
|
||||
)
|
||||
|
||||
versions: bpy.props.EnumProperty(
|
||||
items=opsdata.get_playblast_versions_enum_list, name="Versions"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
return bool(context.scene.kitsu.playblast_dir)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
kitsu_props = context.scene.kitsu
|
||||
version = self.versions
|
||||
|
||||
if not version:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if kitsu_props.get('playblast_version') == version:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Update global scene cache version prop.
|
||||
kitsu_props.playblast_version = version
|
||||
logger.info("Set playblast version to %s", version)
|
||||
|
||||
# Redraw ui.
|
||||
util.ui_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
context.window_manager.invoke_search_popup(self) # type: ignore
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_push_frame_range(bpy.types.Operator):
|
||||
bl_idname = "kitsu.push_frame_range"
|
||||
bl_label = "Push Frame Start"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Adjusts the start frame of animation file."
|
||||
|
||||
frame_start = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(prefs.session_auth(context) and cache.shot_active_get())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Set 3d_start using current scene frame start.")
|
||||
col.label(text=f"New Frame Start: {self.frame_start}", icon="ERROR")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
core.set_frame_range_in(self.frame_start)
|
||||
self.report({"INFO"}, f"Updated frame range offset {self.frame_start}")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
self.frame_start = context.scene.frame_start
|
||||
frame_in, _ = core.get_frame_range()
|
||||
if frame_in == self.frame_start:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Sever's 'Frame In' already matches current Scene's 'Frame Start' {self.frame_start}",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
|
||||
class KITSU_OT_pull_frame_range(bpy.types.Operator):
|
||||
bl_idname = "kitsu.pull_frame_range"
|
||||
bl_label = "Pull Frame Range"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Pulls frame range of active shot from the server "
|
||||
"and set the current scene's frame range to it"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(prefs.session_auth(context) and cache.shot_active_get())
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
frame_in, frame_out = core.get_frame_range()
|
||||
|
||||
# Check if current frame range matches the one for active shot.
|
||||
if core.check_frame_range(context):
|
||||
self.report({"INFO"}, f"Frame range already up to date")
|
||||
return {"FINISHED"}
|
||||
|
||||
# Update scene frame range.
|
||||
context.scene.frame_start = frame_in
|
||||
context.scene.frame_end = frame_out
|
||||
|
||||
if not core.check_frame_range(context):
|
||||
self.report(
|
||||
{"ERROR"}, f"Failed to update frame range to {frame_in} - {frame_out}"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
# Log.
|
||||
self.report({"INFO"}, f"Updated frame range {frame_in} - {frame_out}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_check_frame_range(bpy.types.Operator):
|
||||
bl_idname = "kitsu.check_frame_range"
|
||||
bl_label = "Check Frame Range"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Checks frame range of active shot from the server matches current file"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(prefs.session_auth(context) and cache.shot_active_get())
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
if core.check_frame_range(context):
|
||||
self.report({"INFO"}, f"Frame Range is accurate")
|
||||
return {"FINISHED"}
|
||||
self.report({"ERROR"}, f"Failed: Frame Range Check")
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class KITSU_OT_playblast_increment_playblast_version(bpy.types.Operator):
|
||||
bl_idname = "kitsu.anim_increment_playblast_version"
|
||||
bl_label = "Add Version Increment"
|
||||
bl_description = "Increment the playblast version by one"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
# Incremenet version.
|
||||
version = opsdata.add_playblast_version_increment(context)
|
||||
|
||||
# Update cache_version prop.
|
||||
context.scene.kitsu.playblast_version = version
|
||||
|
||||
# Report.
|
||||
self.report({"INFO"}, f"Add playblast version {version}")
|
||||
|
||||
util.ui_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post_handler_init_version_model(dummy: Any) -> None:
|
||||
opsdata.init_playblast_file_model(bpy.context)
|
||||
|
||||
|
||||
def draw_frame_range_warning(self, context):
|
||||
active_shot = cache.shot_active_get()
|
||||
layout = self.layout
|
||||
layout.alert = True
|
||||
layout.label(
|
||||
text="Frame Range on server does not match the active shot. Please 'pull' the correct frame range from the server"
|
||||
)
|
||||
layout.label(text=f" File Frame Range: {context.scene.frame_start}-{context.scene.frame_end}")
|
||||
if active_shot:
|
||||
kitsu_3d_start = active_shot.get_3d_start()
|
||||
layout.label(
|
||||
text=f'Server Frame Range: {kitsu_3d_start}-{kitsu_3d_start + int(active_shot.nb_frames) - 1}'
|
||||
)
|
||||
else:
|
||||
layout.label(text=f' Server Frame Range: not found')
|
||||
|
||||
layout.operator("kitsu.pull_frame_range", icon='TRIA_DOWN')
|
||||
|
||||
|
||||
def draw_kitsu_context_warning(self, context):
|
||||
layout = self.layout
|
||||
layout.alert = True
|
||||
layout.label(
|
||||
text="Frame Range couldn't be found because current Shot wasn't found, please select an active Shot from Kitsu Context Menu"
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def detect_kitsu_context(dummy: Any) -> None:
|
||||
# TODO Move this handler should not be part of playblast
|
||||
# Leaving this here so it can be set in order along with frame range detection
|
||||
|
||||
# Skip not logged into Kitsu
|
||||
if not prefs.session_auth(bpy.context):
|
||||
return
|
||||
|
||||
# Skip project not set
|
||||
if not cache.project_active_get():
|
||||
return
|
||||
|
||||
# Skip if project root dir is not in file path
|
||||
project_root_dir = prefs.project_root_dir_get(bpy.context)
|
||||
if not project_root_dir in Path(bpy.data.filepath).parents:
|
||||
return
|
||||
|
||||
# Skip if Unsaved File
|
||||
if not bpy.data.is_saved:
|
||||
return
|
||||
|
||||
try:
|
||||
bpy.ops.kitsu.con_detect_context()
|
||||
except RuntimeError:
|
||||
bpy.context.window_manager.popup_menu(
|
||||
draw_kitsu_context_warning,
|
||||
title="Warning: Kitsu Context Auto-Detection Failed.",
|
||||
icon='WARNING',
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post_handler_check_frame_range(dummy: Any) -> None:
|
||||
# Only show if kitsu context is detected
|
||||
cat = bpy.context.scene.kitsu.category
|
||||
project_root_dir = prefs.project_root_dir_get(bpy.context)
|
||||
shots_dir = project_root_dir.joinpath("pro/shots/")
|
||||
|
||||
# Skip if Unsaved File
|
||||
if not bpy.data.is_saved:
|
||||
return
|
||||
|
||||
# Skip if File Outside of Shots Directory
|
||||
if not shots_dir in Path(bpy.data.filepath).parents:
|
||||
return
|
||||
|
||||
# Skip if category is not SHOT
|
||||
if not cat == "SHOT":
|
||||
return
|
||||
|
||||
if not core.check_frame_range(bpy.context):
|
||||
bpy.context.window_manager.popup_menu(
|
||||
draw_frame_range_warning,
|
||||
title="Warning: Frame Range Error.",
|
||||
icon='ERROR',
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def save_pre_handler_clean_overrides(dummy: Any) -> None:
|
||||
"""
|
||||
Removes some Library Override properties that could be accidentally
|
||||
created and cause problems.
|
||||
"""
|
||||
for o in bpy.data.objects:
|
||||
if not o.override_library:
|
||||
continue
|
||||
if o.library:
|
||||
continue
|
||||
override = o.override_library
|
||||
props = override.properties
|
||||
for prop in props[:]:
|
||||
rna_path = prop.rna_path
|
||||
if rna_path in ["active_material_index", "active_material"]:
|
||||
props.remove(prop)
|
||||
linked_value = getattr(override.reference, rna_path)
|
||||
setattr(o, rna_path, linked_value)
|
||||
o.property_unset(rna_path)
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_OT_playblast_create,
|
||||
KITSU_OT_playblast_set_version,
|
||||
KITSU_OT_playblast_increment_playblast_version,
|
||||
KITSU_OT_pull_frame_range,
|
||||
KITSU_OT_push_frame_range,
|
||||
KITSU_OT_check_frame_range,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# init_playblast_file_model(bpy.context) not working because of restricted context.
|
||||
|
||||
# Handlers.
|
||||
bpy.app.handlers.load_post.append(load_post_handler_init_version_model)
|
||||
bpy.app.handlers.load_post.append(detect_kitsu_context)
|
||||
bpy.app.handlers.load_post.append(load_post_handler_check_frame_range)
|
||||
|
||||
bpy.app.handlers.save_pre.append(save_pre_handler_clean_overrides)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Clear handlers.
|
||||
bpy.app.handlers.load_post.remove(load_post_handler_check_frame_range)
|
||||
bpy.app.handlers.load_post.remove(load_post_handler_init_version_model)
|
||||
bpy.app.handlers.load_post.remove(detect_kitsu_context)
|
||||
|
||||
bpy.app.handlers.save_pre.remove(save_pre_handler_clean_overrides)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,96 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Generator
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..models import FileListModel
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
PLAYBLAST_FILE_MODEL = FileListModel()
|
||||
_playblast_enum_list: List[Tuple[str, str, str]] = []
|
||||
_playblast_file_model_init: bool = False
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def init_playblast_file_model(
|
||||
context: bpy.types.Context,
|
||||
) -> None:
|
||||
global PLAYBLAST_FILE_MODEL
|
||||
global _playblast_file_model_init
|
||||
|
||||
kitsu_props = context.scene.kitsu
|
||||
# TODO Make playblast dir variable (seq or shot)
|
||||
# Consider removing this string and use prefs instead
|
||||
|
||||
# Is None if invalid.
|
||||
if not context.scene.kitsu.playblast_dir:
|
||||
logger.error(
|
||||
"Failed to initialize playblast file model. Invalid path. Check addon preferences"
|
||||
)
|
||||
return
|
||||
|
||||
playblast_dir = Path(context.scene.kitsu.playblast_dir)
|
||||
|
||||
PLAYBLAST_FILE_MODEL.reset()
|
||||
PLAYBLAST_FILE_MODEL.root_path = playblast_dir
|
||||
|
||||
if not PLAYBLAST_FILE_MODEL.versions:
|
||||
PLAYBLAST_FILE_MODEL.append_item("v001")
|
||||
# Update playblast_version prop.
|
||||
kitsu_props.playblast_version = "v001"
|
||||
|
||||
else:
|
||||
# Update playblast_version prop.
|
||||
kitsu_props.playblast_version = PLAYBLAST_FILE_MODEL.versions[0]
|
||||
|
||||
_playblast_file_model_init = True
|
||||
|
||||
|
||||
def add_playblast_version_increment(context: bpy.types.Context) -> str:
|
||||
# Init model if it did not happen.
|
||||
if not _playblast_file_model_init:
|
||||
init_playblast_file_model(context)
|
||||
|
||||
# Should be already sorted.
|
||||
versions = PLAYBLAST_FILE_MODEL.versions
|
||||
|
||||
if len(versions) > 0:
|
||||
latest_version = versions[0]
|
||||
increment = "v{:03}".format(int(latest_version.replace("v", "")) + 1)
|
||||
else:
|
||||
increment = "v001"
|
||||
|
||||
PLAYBLAST_FILE_MODEL.append_item(increment)
|
||||
return increment
|
||||
|
||||
|
||||
def get_playblast_versions_enum_list(
|
||||
self: Any,
|
||||
context: bpy.types.Context,
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _playblast_enum_list
|
||||
global PLAYBLAST_FILE_MODEL
|
||||
global init_playblast_file_model
|
||||
global _playblast_file_model_init
|
||||
|
||||
# Init model if it did not happen.
|
||||
if not _playblast_file_model_init:
|
||||
init_playblast_file_model(context)
|
||||
|
||||
# Clear all versions in enum list.
|
||||
_playblast_enum_list.clear()
|
||||
_playblast_enum_list.extend(PLAYBLAST_FILE_MODEL.versions_as_enum_list)
|
||||
|
||||
return _playblast_enum_list
|
||||
|
||||
|
||||
def add_version_custom(custom_version: str) -> None:
|
||||
global _playblast_enum_list
|
||||
global PLAYBLAST_FILE_MODEL
|
||||
|
||||
PLAYBLAST_FILE_MODEL.append_item(custom_version)
|
||||
@@ -1,119 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..anim import opsdata
|
||||
from ..context import core as context_core
|
||||
import bpy
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import prefs, cache, ui
|
||||
from ..playblast.ops import (
|
||||
KITSU_OT_playblast_create,
|
||||
KITSU_OT_playblast_set_version,
|
||||
KITSU_OT_playblast_increment_playblast_version,
|
||||
)
|
||||
from ..generic.ops import KITSU_OT_open_path
|
||||
|
||||
|
||||
class KITSU_PT_vi3d_playblast(bpy.types.Panel):
|
||||
"""
|
||||
Panel in 3dview that exposes a set of tools that are useful for animation
|
||||
tasks, e.G playblast
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Playblast"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 50
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if context_core.is_edit_context():
|
||||
return False
|
||||
return bool(prefs.session_auth(context))
|
||||
|
||||
@classmethod
|
||||
def poll_error(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
return bool(not addon_prefs.is_playblast_root_valid or not context.scene.camera)
|
||||
|
||||
def draw_error(self, context: bpy.types.Context) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
layout = self.layout
|
||||
|
||||
box = ui.draw_error_box(layout)
|
||||
if not addon_prefs.is_playblast_root_valid:
|
||||
ui.draw_error_invalid_playblast_root_dir(box)
|
||||
if not context.scene.camera:
|
||||
ui.draw_error_no_active_camera(box)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
layout = self.layout
|
||||
split_factor_small = 0.95
|
||||
|
||||
# ERROR.
|
||||
if self.poll_error(context):
|
||||
self.draw_error(context)
|
||||
|
||||
# Playblast version op.
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_playblast_set_version.bl_idname,
|
||||
text=context.scene.kitsu.playblast_version,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
# Playblast increment version op.
|
||||
row.operator(
|
||||
KITSU_OT_playblast_increment_playblast_version.bl_idname,
|
||||
text="",
|
||||
icon="ADD",
|
||||
)
|
||||
|
||||
# Playblast op.
|
||||
row = layout.row(align=True)
|
||||
row.operator(KITSU_OT_playblast_create.bl_idname, icon="RENDER_ANIMATION")
|
||||
|
||||
# Playblast path label.
|
||||
if Path(context.scene.kitsu.playblast_file).exists():
|
||||
split = layout.split(factor=1 - split_factor_small, align=True)
|
||||
split.label(icon="ERROR")
|
||||
sub_split = split.split(factor=split_factor_small)
|
||||
sub_split.label(text=context.scene.kitsu.playblast_file)
|
||||
sub_split.operator(
|
||||
KITSU_OT_open_path.bl_idname, icon="FILE_FOLDER", text=""
|
||||
).filepath = context.scene.kitsu.playblast_file
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=context.scene.kitsu.playblast_file)
|
||||
row.operator(
|
||||
KITSU_OT_open_path.bl_idname, icon="FILE_FOLDER", text=""
|
||||
).filepath = context.scene.kitsu.playblast_file
|
||||
|
||||
|
||||
class KITSU_PT_seq_playblast(KITSU_PT_vi3d_playblast):
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not context_core.is_sequence_context():
|
||||
return False
|
||||
return bool(prefs.session_auth(context))
|
||||
|
||||
|
||||
classes = (KITSU_PT_seq_playblast, KITSU_PT_vi3d_playblast)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,818 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
import os
|
||||
|
||||
from typing import Optional, Set
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from . import cache, bkglobals, propsdata
|
||||
from .props import get_safely_string_prop
|
||||
|
||||
# TODO: restructure this to not access ops_playblast_data.
|
||||
from .playblast import opsdata as ops_playblast_data
|
||||
from .types import Session
|
||||
from .logger import LoggerFactory
|
||||
from .auth.ops import (
|
||||
KITSU_OT_session_end,
|
||||
KITSU_OT_session_start,
|
||||
)
|
||||
from .context.ops import KITSU_OT_con_productions_load
|
||||
from .lookdev.prefs import LOOKDEV_preferences
|
||||
from .util import addon_prefs_get
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def draw_file_path(
|
||||
self, layout: bpy.types.UILayout, bool_prop: bool, bool_prop_name: str, path_prop_name: str
|
||||
) -> None:
|
||||
# Draw auto-filled file path with option to override/edit this path
|
||||
icon = "MODIFIER_ON" if bool_prop else "MODIFIER_OFF"
|
||||
seq_row = layout.row(align=True)
|
||||
seq_row.prop(self, path_prop_name)
|
||||
seq_row.prop(self, bool_prop_name, icon=icon, text="")
|
||||
|
||||
|
||||
class KITSU_task(bpy.types.PropertyGroup):
|
||||
# name: StringProperty() -> Instantiated by default
|
||||
id: bpy.props.StringProperty(name="Task ID", default="")
|
||||
entity_id: bpy.props.StringProperty(name="Entity ID", default="")
|
||||
entity_name: bpy.props.StringProperty(name="Entity Name", default="")
|
||||
task_type_id: bpy.props.StringProperty(name="Task Type ID", default="")
|
||||
task_type_name: bpy.props.StringProperty(name="Task Type Name", default="")
|
||||
|
||||
|
||||
class KITSU_media_update_search_paths(bpy.types.PropertyGroup):
|
||||
# name: StringProperty() -> Instantiated by default
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="Media Update Search Path",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
description="Top level directory path in which to search for media updates",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_OT_prefs_media_search_path_add(bpy.types.Operator):
|
||||
""""""
|
||||
|
||||
bl_idname = "kitsu.prefs_media_search_path_add"
|
||||
bl_label = "Add Path"
|
||||
bl_description = "Adds new entry to media update search paths list"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = addon_prefs_get(context)
|
||||
media_update_search_paths = addon_prefs.media_update_search_paths
|
||||
|
||||
item = media_update_search_paths.add()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_prefs_media_search_path_remove(bpy.types.Operator):
|
||||
""""""
|
||||
|
||||
bl_idname = "kitsu.prefs_media_search_path_remove"
|
||||
bl_label = "Removes Path"
|
||||
bl_description = "Removes Path from media udpate search paths list"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(
|
||||
name="Index",
|
||||
description="Refers to index that will be removed from collection property",
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = addon_prefs_get(context)
|
||||
media_update_search_paths = addon_prefs.media_update_search_paths
|
||||
|
||||
media_update_search_paths.remove(self.index)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_addon_preferences(bpy.types.AddonPreferences):
|
||||
"""
|
||||
Addon preferences to kitsu. Holds variables that are important for authentication and configuring
|
||||
how some of the operators work.
|
||||
"""
|
||||
|
||||
def get_metadatastrip_file(self) -> str:
|
||||
res_dir = bkglobals.RES_DIR_PATH
|
||||
return res_dir.joinpath("metastrip.mp4").as_posix()
|
||||
|
||||
metadatastrip_file: bpy.props.StringProperty( # type: ignore
|
||||
name="Metadata Strip File",
|
||||
description=(
|
||||
"Filepath to black .mp4 file that will be used as metastrip for shots in the sequence editor"
|
||||
),
|
||||
default="",
|
||||
subtype="FILE_PATH",
|
||||
get=get_metadatastrip_file,
|
||||
)
|
||||
|
||||
def get_datadir(self) -> Path:
|
||||
"""Returns a Path where persistent application data can be stored.
|
||||
|
||||
# linux: ~/.local/share
|
||||
# macOS: ~/Library/Application Support
|
||||
# windows: C:/Users/<USER>/AppData/Roaming
|
||||
"""
|
||||
# This function is copied from the edit_breakdown addon by Inês Almeida and Francesco Siddi.
|
||||
|
||||
home = Path.home()
|
||||
|
||||
if sys.platform == "win32":
|
||||
return home / "AppData/Roaming"
|
||||
elif sys.platform == "linux":
|
||||
return home / ".local/share"
|
||||
elif sys.platform == "darwin":
|
||||
return home / "Library/Application Support"
|
||||
|
||||
def get_thumbnails_dir(self) -> str:
|
||||
"""Generate a path based on get_datadir and the current file name.
|
||||
|
||||
The path is constructed by combining the OS application data dir,
|
||||
"blender_kitsu" and a hashed version of the filepath.
|
||||
"""
|
||||
# This function is copied and modified from the edit_breakdown addon by Inês Almeida and Francesco Siddi.
|
||||
|
||||
hashed_filepath = hashlib.md5(bpy.data.filepath.encode()).hexdigest()
|
||||
storage_dir = self.get_datadir() / "blender_kitsu" / "thumbnails" / hashed_filepath
|
||||
return storage_dir.as_posix()
|
||||
|
||||
def get_sqe_render_dir(self) -> str:
|
||||
hashed_filepath = hashlib.md5(bpy.data.filepath.encode()).hexdigest()
|
||||
storage_dir = self.get_datadir() / "blender_kitsu" / "sqe_render" / hashed_filepath
|
||||
return storage_dir.absolute().as_posix()
|
||||
|
||||
def get_config_dir(self) -> str:
|
||||
if not self.is_project_root_valid:
|
||||
return ""
|
||||
return self.project_root_path.joinpath("pipeline/blender_kitsu").as_posix()
|
||||
|
||||
def init_playblast_file_model(self, context: bpy.types.Context) -> None:
|
||||
ops_playblast_data.init_playblast_file_model(context)
|
||||
|
||||
bl_idname = __package__
|
||||
|
||||
host: bpy.props.StringProperty( # type: ignore
|
||||
name="Host",
|
||||
default="",
|
||||
)
|
||||
|
||||
email: bpy.props.StringProperty( # type: ignore
|
||||
name="Email",
|
||||
default="",
|
||||
)
|
||||
|
||||
passwd: bpy.props.StringProperty( # type: ignore
|
||||
name="Password", default="", options={"HIDDEN", "SKIP_SAVE"}, subtype="PASSWORD"
|
||||
)
|
||||
|
||||
thumbnail_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Thumbnail Directory",
|
||||
description="Directory in which thumbnails will be saved",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_thumbnails_dir,
|
||||
)
|
||||
|
||||
sqe_render_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Sequence Editor Render Directory",
|
||||
description="Directory in which sequence renders will be saved",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_sqe_render_dir,
|
||||
)
|
||||
|
||||
lookdev: bpy.props.PointerProperty( # type: ignore
|
||||
name="Lookdev Preferences",
|
||||
type=LOOKDEV_preferences,
|
||||
description="Metadata that is required for lookdev",
|
||||
)
|
||||
|
||||
def set_shot_playblast_root_dir(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['shot_playblast_root_dir'] = input
|
||||
return
|
||||
|
||||
def get_shot_playblast_root_dir(
|
||||
self,
|
||||
) -> str:
|
||||
path = get_safely_string_prop(self.bl_system_properties_get(), 'shot_playblast_root_dir')
|
||||
if path == "" and self.project_root_path:
|
||||
dir = self.project_root_path.joinpath("shared/editorial/footage/pro/")
|
||||
if dir.exists():
|
||||
return dir.as_posix()
|
||||
return path
|
||||
|
||||
shot_playblast_root_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Shot Playblasts",
|
||||
description="Directory path to shot playblast root folder. Should point to: {project}/editorial/footage/pro",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
update=init_playblast_file_model,
|
||||
get=get_shot_playblast_root_dir,
|
||||
set=set_shot_playblast_root_dir,
|
||||
)
|
||||
|
||||
def set_seq_playblast_root_dir(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['seq_playblast_root_dir'] = input
|
||||
return
|
||||
|
||||
def get_seq_playblast_root_dir(
|
||||
self,
|
||||
) -> str:
|
||||
path = get_safely_string_prop(self.bl_system_properties_get(), 'seq_playblast_root_dir')
|
||||
if path == "" and self.project_root_path:
|
||||
dir = self.project_root_path.joinpath("shared/editorial/footage/pre/")
|
||||
if dir.exists():
|
||||
return dir.as_posix()
|
||||
return path
|
||||
|
||||
seq_playblast_root_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Sequence Playblasts",
|
||||
description="Directory path to sequence playblast root folder. Should point to: {project}/editorial/footage/pre",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_seq_playblast_root_dir,
|
||||
set=set_seq_playblast_root_dir,
|
||||
options=set(),
|
||||
)
|
||||
|
||||
def set_frames_root_dir(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['frames_root_dir'] = input
|
||||
return
|
||||
|
||||
def get_frames_root_dir(
|
||||
self,
|
||||
) -> str:
|
||||
path = get_safely_string_prop(self.bl_system_properties_get(), 'frames_root_dir')
|
||||
if path == "" and self.project_root_path:
|
||||
dir = self.project_root_path.joinpath("shared/editorial/footage/post/")
|
||||
if dir.exists():
|
||||
return dir.as_posix()
|
||||
return path
|
||||
|
||||
frames_root_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Rendered Frames",
|
||||
description="Directory path to rendered frames root folder. Should point to: {project}/editorial/footage/post",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_frames_root_dir,
|
||||
set=set_frames_root_dir,
|
||||
)
|
||||
|
||||
project_root_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Project Root Directory",
|
||||
description=(
|
||||
"Directory path to the root of the project"
|
||||
"In this directory blender kitsu searches for the svn/ & shared/ directories"
|
||||
"Directory should follow `you_project_name/` format without any subdirectories"
|
||||
),
|
||||
default="/data/gold/",
|
||||
subtype="DIR_PATH",
|
||||
)
|
||||
config_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Config Directory",
|
||||
description=(
|
||||
"Configuration directory of blender_kitsu"
|
||||
"See readme.md how you can configurate the addon on a per project basis"
|
||||
),
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_config_dir,
|
||||
)
|
||||
|
||||
project_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Project Active ID",
|
||||
description="Server Id that refers to the last active project",
|
||||
default="",
|
||||
)
|
||||
|
||||
enable_debug: bpy.props.BoolProperty( # type: ignore
|
||||
name="Enable Debug Operators",
|
||||
description="Enables Operatots that provide debug functionality",
|
||||
)
|
||||
show_advanced: bpy.props.BoolProperty( # type: ignore
|
||||
name="Show Advanced Settings",
|
||||
description="Show advanced settings that should already have good defaults",
|
||||
)
|
||||
shot_builder_show_advanced: bpy.props.BoolProperty( # type: ignore
|
||||
name="Show Advanced Settings",
|
||||
description="Show advanced settings that should already have good defaults",
|
||||
)
|
||||
|
||||
def set_shot_pattern(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['shot_pattern'] = input
|
||||
return
|
||||
|
||||
def get_shot_pattern(
|
||||
self,
|
||||
) -> str:
|
||||
active_project = cache.project_active_get()
|
||||
pattern = get_safely_string_prop(self.bl_system_properties_get(), 'shot_pattern')
|
||||
if pattern == "":
|
||||
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
return "<Episode>_<Sequence>_<Counter>"
|
||||
return "<Sequence>_<Counter>"
|
||||
return pattern
|
||||
|
||||
shot_pattern: bpy.props.StringProperty( # type: ignore
|
||||
name="Shot Pattern",
|
||||
description="Pattern to define how Bulk Init will name the shots. Supported wildcards: <Project>, <Episode>, <Sequence>, <Counter>",
|
||||
default="<Sequence>_<Counter>",
|
||||
get=get_shot_pattern,
|
||||
set=set_shot_pattern,
|
||||
)
|
||||
|
||||
shot_counter_digits: bpy.props.IntProperty( # type: ignore
|
||||
name="Shot Counter Digits",
|
||||
description="How many digits the counter should contain",
|
||||
default=4,
|
||||
min=0,
|
||||
)
|
||||
shot_counter_increment: bpy.props.IntProperty( # type: ignore
|
||||
name="Shot Counter Increment",
|
||||
description="By which Increment counter should be increased",
|
||||
default=10,
|
||||
step=5,
|
||||
min=0,
|
||||
)
|
||||
pb_open_webbrowser: bpy.props.BoolProperty( # type: ignore
|
||||
name="Open Webbrowser after Playblast",
|
||||
description="Toggle if the default webbrowser should be opened to kitsu after playblast creation",
|
||||
default=False,
|
||||
)
|
||||
|
||||
pb_open_vse: bpy.props.BoolProperty( # type: ignore
|
||||
name="Open Sequence Editor after Playblast",
|
||||
description="Toggle if the movie clip should be loaded in the seqeuence editor in a seperate scene after playblast creation",
|
||||
default=False,
|
||||
)
|
||||
|
||||
pb_manual_burn_in: bpy.props.BoolProperty( # type: ignore
|
||||
name="Manual Playblast Burn-Ins",
|
||||
description=(
|
||||
"Blender Kitsu will override all Shot/Sequence playblasts with it's own metadata burn in. "
|
||||
"This includes frame, lens, Shot name & Animator name at font size of 24. "
|
||||
"To use a file's metadata burn in settings during playblast enable this option"
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
media_update_search_paths: bpy.props.CollectionProperty(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',
|
||||
)
|
||||
|
||||
def set_edit_export_dir(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['edit_export_dir'] = input
|
||||
return
|
||||
|
||||
def get_edit_export_dir(
|
||||
self,
|
||||
) -> str:
|
||||
path = get_safely_string_prop(self.bl_system_properties_get(), 'edit_export_dir')
|
||||
active_project = cache.project_active_get()
|
||||
if path == "" and self.project_root_path:
|
||||
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
dir = self.project_root_path.joinpath("shared/editorial/export/<episode>/")
|
||||
else:
|
||||
dir = self.project_root_path.joinpath("shared/editorial/export/")
|
||||
return dir.as_posix()
|
||||
return path
|
||||
|
||||
edit_export_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Edit Export Directory",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
description="Directory path to editorial's export folder containing storyboard/animatic renders. Path should be similar to '{project}/shared/editorial/export/'",
|
||||
subtype="DIR_PATH",
|
||||
get=get_edit_export_dir,
|
||||
set=set_edit_export_dir,
|
||||
)
|
||||
|
||||
def set_edit_export_file_pattern(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['edit_export_file_pattern'] = input
|
||||
return
|
||||
|
||||
def get_edit_export_file_pattern(
|
||||
self,
|
||||
) -> str:
|
||||
active_project = cache.project_active_get()
|
||||
pattern = get_safely_string_prop(self.bl_system_properties_get(), 'edit_export_file_pattern')
|
||||
if pattern == "" and active_project:
|
||||
proj_name = active_project.name.replace(' ', bkglobals.SPACE_REPLACER).lower()
|
||||
# HACK for Project Gold at Blender Studio
|
||||
if proj_name == "project_gold":
|
||||
return f"gold-edit-v###.mp4"
|
||||
return f"{proj_name}-edit-v###.mp4"
|
||||
return pattern
|
||||
|
||||
edit_export_file_pattern: bpy.props.StringProperty( # type: ignore
|
||||
name="Edit Export File Pattern",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
description=(
|
||||
"File pattern for latest editorial export file. "
|
||||
"Typically '{proj_name}-edit-v###.mp4' where # represents a number. "
|
||||
"Pattern must contain exactly v### representing the version, pattern must end in .mp4"
|
||||
),
|
||||
default="",
|
||||
get=get_edit_export_file_pattern,
|
||||
set=set_edit_export_file_pattern,
|
||||
)
|
||||
|
||||
edit_export_frame_offset: bpy.props.IntProperty( # type: ignore
|
||||
name="Edit Export Offset",
|
||||
description="Shift Editorial Export by this frame-range after import",
|
||||
default=-101,
|
||||
)
|
||||
|
||||
shot_builder_frame_offset: bpy.props.IntProperty( # type: ignore
|
||||
name="Start Frame Offset",
|
||||
description="All Shots built by 'Shot_builder' should begin at this frame",
|
||||
default=101,
|
||||
)
|
||||
|
||||
session: Session = Session()
|
||||
|
||||
tasks: bpy.props.CollectionProperty(type=KITSU_task)
|
||||
|
||||
version_control: bpy.props.BoolProperty( # type: ignore
|
||||
name="Use Version Control",
|
||||
description="Indicates if SVN or GIT-LFS is being used for version control. If disabled local backups of production files will be created when Publishing to Kitsu",
|
||||
default=True,
|
||||
)
|
||||
|
||||
####################
|
||||
# Render Review
|
||||
####################
|
||||
|
||||
def set_farm_dir(self, input):
|
||||
self.bl_system_properties_get(do_create=True)['farm_output_dir'] = input
|
||||
return
|
||||
|
||||
def get_farm_dir(
|
||||
self,
|
||||
) -> str:
|
||||
path = get_safely_string_prop(self.bl_system_properties_get(), 'farm_output_dir')
|
||||
if path == "" and self.project_root_path:
|
||||
dir = self.project_root_path.joinpath("render/")
|
||||
if dir.exists():
|
||||
return dir.as_posix()
|
||||
return path
|
||||
|
||||
farm_output_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Farm Output Directory",
|
||||
description="Directory used as 'Render Output Root' when submitting job to Flamenco. Usually points to: {project}/render/",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=get_farm_dir,
|
||||
set=set_farm_dir,
|
||||
)
|
||||
|
||||
shot_dir_name: bpy.props.StringProperty(
|
||||
name="Shot Directory Name",
|
||||
description="Name of the shot directory",
|
||||
default="shots"
|
||||
)
|
||||
|
||||
seq_dir_name: bpy.props.StringProperty(
|
||||
name="Sequence Directory Name",
|
||||
description="Name of the sequence directory",
|
||||
default="sequences"
|
||||
)
|
||||
|
||||
asset_dir_name: bpy.props.StringProperty(
|
||||
name="Asset Directory Name",
|
||||
description="Name of the asset directory",
|
||||
default="assets"
|
||||
)
|
||||
|
||||
edit_dir_name: bpy.props.StringProperty(
|
||||
name="Edit Directory Name",
|
||||
description="Name of the edit directory",
|
||||
default="edit"
|
||||
)
|
||||
|
||||
shot_name_filter: bpy.props.StringProperty( # type: ignore
|
||||
name="Shot Name Filter",
|
||||
description="Shot name must include this string, otherwise it will be ignored",
|
||||
default="",
|
||||
)
|
||||
use_video: bpy.props.BoolProperty(
|
||||
name="Use Video",
|
||||
description="Load video versions of renders rather than image sequences for faster playback",
|
||||
)
|
||||
use_video_latest_only: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
name="Latest Only",
|
||||
description="Only load video files for the latest versions by default, to avoid running out of memory and crashing",
|
||||
)
|
||||
|
||||
match_latest_length: bpy.props.BoolProperty( # type: ignore
|
||||
default=True,
|
||||
name="Match Latest Length",
|
||||
description="Only include renders that are matching the last rendered length for a given shot, (i.e. missing frames)",
|
||||
)
|
||||
|
||||
versions_max_count: bpy.props.IntProperty(
|
||||
name="Max Versions",
|
||||
description="Desired number of versions to load for each shot",
|
||||
min=1,
|
||||
max=128,
|
||||
default=32,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=True, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
project_active = cache.project_active_get()
|
||||
|
||||
# Login.
|
||||
box = col.box()
|
||||
box.label(text="Login and Host Settings", icon="URL")
|
||||
if not self.session.is_auth():
|
||||
box.row().prop(self, "host")
|
||||
box.row().prop(self, "email")
|
||||
box.row().prop(self, "passwd")
|
||||
box.row().operator(KITSU_OT_session_start.bl_idname, text="Login", icon="PLAY")
|
||||
else:
|
||||
row = box.row()
|
||||
row.prop(self, "host")
|
||||
row.enabled = False
|
||||
box.row().label(text=f"Logged in: {self.session.email}")
|
||||
box.row().operator(KITSU_OT_session_end.bl_idname, text="Logout", icon="PANEL_CLOSE")
|
||||
|
||||
# Project
|
||||
box = col.box()
|
||||
box.label(text="Project", icon="FILEBROWSER")
|
||||
row = box.row(align=True)
|
||||
|
||||
if not project_active:
|
||||
prod_load_text = "Select Project"
|
||||
else:
|
||||
prod_load_text = project_active.name
|
||||
|
||||
row.operator(
|
||||
KITSU_OT_con_productions_load.bl_idname,
|
||||
text=prod_load_text,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
box.row().prop(self, "project_root_dir")
|
||||
box.prop(self, 'shot_dir_name')
|
||||
box.prop(self, 'seq_dir_name')
|
||||
box.prop(self, 'asset_dir_name')
|
||||
box.prop(self, 'edit_dir_name')
|
||||
|
||||
# Previews
|
||||
box = col.box()
|
||||
box.label(text="Previews", icon="RENDER_ANIMATION")
|
||||
box.row().prop(self, "shot_playblast_root_dir")
|
||||
box.row().prop(self, "seq_playblast_root_dir")
|
||||
box.row().prop(self, "frames_root_dir")
|
||||
box.row().prop(self, "pb_open_webbrowser")
|
||||
box.row().prop(self, "pb_open_vse")
|
||||
box.row().prop(self, "pb_manual_burn_in")
|
||||
|
||||
# Editorial Settings
|
||||
box = col.box()
|
||||
box.label(text="Editorial", icon="SEQ_SEQUENCER")
|
||||
box.row().prop(self, "edit_export_dir", text="Export Directory")
|
||||
file_pattern_row = box.row(align=True)
|
||||
file_pattern_row.alert = not self.is_edit_export_pattern_valid
|
||||
file_pattern_row.prop(self, "edit_export_file_pattern", text="Export File Pattern")
|
||||
box.row().prop(self, "edit_export_frame_offset")
|
||||
|
||||
# Render Review
|
||||
self.draw_render_review(col)
|
||||
|
||||
# Shot_Builder settings.
|
||||
box = col.box()
|
||||
box.label(text="Shot Builder", icon="MOD_BUILD")
|
||||
box.prop(self, "shot_builder_frame_offset")
|
||||
row = box.row(align=True)
|
||||
# Avoids circular import error
|
||||
from .shot_builder.ops import (
|
||||
KITSU_OT_build_config_save_settings,
|
||||
KITSU_OT_build_config_save_hooks,
|
||||
KITSU_OT_build_config_save_templates,
|
||||
)
|
||||
|
||||
box.row().operator(KITSU_OT_build_config_save_hooks.bl_idname, icon='FILE_SCRIPT')
|
||||
box.row().operator(KITSU_OT_build_config_save_settings.bl_idname, icon="TEXT")
|
||||
box.row().operator(KITSU_OT_build_config_save_templates.bl_idname, icon="FILE_BLEND")
|
||||
|
||||
# Lookdev tools settings.
|
||||
self.lookdev.draw(context, col)
|
||||
|
||||
# Sequence editor include paths.
|
||||
box = col.box()
|
||||
box.label(text="Media Update Search Paths", icon="SEQUENCE")
|
||||
box.label(
|
||||
text="Only the movie strips that have their source media coming from one of these folders (recursive) will be checked for media updates"
|
||||
)
|
||||
|
||||
for i, item in enumerate(self.media_update_search_paths):
|
||||
row = box.row()
|
||||
row.prop(item, "filepath", text="")
|
||||
row.operator(
|
||||
KITSU_OT_prefs_media_search_path_remove.bl_idname,
|
||||
text="",
|
||||
icon="X",
|
||||
emboss=False,
|
||||
).index = i
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.operator(
|
||||
KITSU_OT_prefs_media_search_path_add.bl_idname,
|
||||
text="",
|
||||
icon="ADD",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
# Misc settings.
|
||||
box = col.box()
|
||||
box.label(text="Miscellaneous", icon="MODIFIER")
|
||||
box.row().prop(self, "thumbnail_dir")
|
||||
box.row().prop(self, "sqe_render_dir")
|
||||
box.row().prop(self, "enable_debug")
|
||||
box.row().prop(self, "show_advanced")
|
||||
|
||||
if self.show_advanced:
|
||||
box.row().prop(self, "shot_pattern")
|
||||
box.row().prop(self, "shot_counter_digits")
|
||||
box.row().prop(self, "shot_counter_increment")
|
||||
box.row().prop(self, "version_control")
|
||||
|
||||
def draw_render_review(self, layout: bpy.types.UILayout) -> None:
|
||||
box = layout.box()
|
||||
box.label(text="Render Review", icon="FILEBROWSER")
|
||||
|
||||
# Farm outpur dir.
|
||||
box.row().prop(self, "farm_output_dir")
|
||||
|
||||
if not self.farm_output_dir:
|
||||
row = box.row()
|
||||
row.label(text="Please specify the Farm Output Directory", icon="ERROR")
|
||||
|
||||
if not bpy.data.filepath and self.farm_output_dir.startswith("//"):
|
||||
row = box.row()
|
||||
row.label(
|
||||
text="In order to use a relative path the current file needs to be saved.",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
box.row().prop(self, "shot_name_filter")
|
||||
box.row().prop(self, "versions_max_count", slider=True)
|
||||
|
||||
@property
|
||||
def shot_playblast_root_path(self) -> Optional[Path]:
|
||||
if not self.is_playblast_root_valid:
|
||||
return None
|
||||
return Path(os.path.abspath(bpy.path.abspath(self.shot_playblast_root_dir)))
|
||||
|
||||
@property
|
||||
def seq_playblast_root_path(self) -> Optional[Path]:
|
||||
if not self.is_playblast_root_valid:
|
||||
return None
|
||||
return Path(os.path.abspath(bpy.path.abspath(self.seq_playblast_root_dir)))
|
||||
|
||||
@property
|
||||
def is_playblast_root_valid(self) -> bool:
|
||||
if not self.shot_playblast_root_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.shot_playblast_root_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_edit_export_root_valid(self) -> bool:
|
||||
if self.edit_export_dir.strip() == "":
|
||||
return False
|
||||
edit_export_path = Path(self.edit_export_dir)
|
||||
if '<episode>' in edit_export_path.parts:
|
||||
edit_export_path = propsdata.get_edit_export_dir()
|
||||
if edit_export_path.parent.exists():
|
||||
return True
|
||||
if not edit_export_path.exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_edit_export_pattern_valid(self) -> bool:
|
||||
if not self.edit_export_file_pattern.endswith(".mp4"):
|
||||
return False
|
||||
if not "###" in self.edit_export_file_pattern:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def project_root_path(self) -> Optional[Path]:
|
||||
if not self.project_root_dir:
|
||||
return None
|
||||
return Path(os.path.abspath(bpy.path.abspath(self.project_root_dir)))
|
||||
|
||||
@property
|
||||
def is_project_root_valid(self) -> bool:
|
||||
if not self.project_root_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.project_root_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_config_dir_valid(self) -> bool:
|
||||
if not self.config_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.config_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def farm_output_path(self) -> Optional[Path]:
|
||||
if not self.is_farm_output_valid:
|
||||
return None
|
||||
return Path(os.path.abspath(bpy.path.abspath(self.farm_output_dir)))
|
||||
|
||||
@property
|
||||
def is_farm_output_valid(self) -> bool:
|
||||
|
||||
# Check if file is saved.
|
||||
if not self.farm_output_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.farm_output_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def session_get(context: bpy.types.Context) -> Session:
|
||||
"""
|
||||
Shortcut to get session from blender_kitsu addon preferences
|
||||
"""
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
return prefs.session # type: ignore
|
||||
|
||||
|
||||
def project_root_dir_get(context: bpy.types.Context):
|
||||
addon_prefs = addon_prefs_get(context)
|
||||
return Path(addon_prefs.project_root_dir).joinpath('svn').resolve()
|
||||
|
||||
|
||||
def session_auth(context: bpy.types.Context) -> bool:
|
||||
"""
|
||||
Shortcut to check if session is authorized
|
||||
"""
|
||||
return session_get(context).is_auth()
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_OT_prefs_media_search_path_remove,
|
||||
KITSU_OT_prefs_media_search_path_add,
|
||||
KITSU_task,
|
||||
KITSU_media_update_search_paths,
|
||||
KITSU_addon_preferences,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Log user out.
|
||||
addon_prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
if addon_prefs.session.is_auth():
|
||||
addon_prefs.session.end()
|
||||
|
||||
# Unregister classes.
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,949 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Any, Union, List, Dict, Optional, Tuple
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import propsdata, bkglobals
|
||||
from .logger import LoggerFactory
|
||||
from . import cache
|
||||
from .types import Sequence
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_sequence_colors(bpy.types.PropertyGroup):
|
||||
# name: StringProperty() -> Instantiated by default
|
||||
color: bpy.props.FloatVectorProperty(
|
||||
name="Sequence Color",
|
||||
subtype="COLOR",
|
||||
default=(1.0, 1.0, 1.0),
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
description="Sequence color that will be used as line overlay",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_property_group_sequence(bpy.types.PropertyGroup):
|
||||
"""
|
||||
Property group that will be registered on sequence strips.
|
||||
They hold metadata that will be used to compose a data structure that can
|
||||
be pushed to backend.
|
||||
"""
|
||||
def _get_shot_description(self):
|
||||
return self.shot_description
|
||||
|
||||
def _get_sequence_entity(self):
|
||||
try:
|
||||
return Sequence.by_id(self.sequence_id)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
manual_shot_name: bpy.props.StringProperty(
|
||||
name="Shot",
|
||||
description="Enter a new Shot name to submit to Kitsu Server",
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
###########
|
||||
# Shot
|
||||
###########
|
||||
shot_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Shot ID",
|
||||
description="ID that refers to the strip's shot on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_shot_via_name(self):
|
||||
return get_safely_string_prop(self, "shot_name")
|
||||
|
||||
def set_shot_via_name(self, input):
|
||||
seq = self._get_sequence_entity()
|
||||
if seq is None:
|
||||
return
|
||||
|
||||
# Attempt to set with matching Kitsu Entry
|
||||
kitsu_set = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_shots_enum_for_seq(self, bpy.context, seq),
|
||||
name_prop='shot_name',
|
||||
id_prop='shot_id',
|
||||
)
|
||||
|
||||
# Set manually so users can submit new shots
|
||||
if not kitsu_set:
|
||||
self['shot_name'] = input
|
||||
return
|
||||
|
||||
def get_shot_search_list(self, context, edit_text):
|
||||
seq = self._get_sequence_entity()
|
||||
if seq is None:
|
||||
return []
|
||||
return get_enum_item_names(cache.get_shots_enum_for_seq(self, bpy.context, seq))
|
||||
|
||||
shot_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Shot",
|
||||
description="Name that refers to the strip's shot on server, Reminder: press ENTER to save shot name after entering",
|
||||
default="",
|
||||
get=get_shot_via_name,
|
||||
set=set_shot_via_name,
|
||||
options=set(),
|
||||
search=get_shot_search_list,
|
||||
search_options={'SORT', 'SUGGESTION'},
|
||||
)
|
||||
|
||||
shot_description: bpy.props.StringProperty(name="Description", default="", options={"HIDDEN"}) # type: ignore
|
||||
|
||||
###########
|
||||
# Sequence
|
||||
###########
|
||||
sequence_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Seq ID",
|
||||
description="ID that refers to the active sequence on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_sequences_via_name(self):
|
||||
return get_safely_string_prop(self, "sequence_name")
|
||||
|
||||
def set_sequences_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_sequences_enum_list(self, bpy.context),
|
||||
name_prop='sequence_name',
|
||||
id_prop='sequence_id',
|
||||
)
|
||||
return
|
||||
|
||||
def get_sequence_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context))
|
||||
|
||||
sequence_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Sequence",
|
||||
description="Sequence",
|
||||
default="",
|
||||
get=get_sequences_via_name,
|
||||
set=set_sequences_via_name,
|
||||
options=set(),
|
||||
search=get_sequence_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
# Project.
|
||||
project_name: bpy.props.StringProperty(name="Project", default="") # type: ignore
|
||||
project_id: bpy.props.StringProperty(name="Project ID", default="") # type: ignore
|
||||
|
||||
# Meta.
|
||||
initialized: bpy.props.BoolProperty( # type: ignore
|
||||
name="Initialized", default=False, description="Is Kitsu shot"
|
||||
)
|
||||
linked: bpy.props.BoolProperty( # type: ignore
|
||||
name="Linked", default=False, description="Is linked to an ID on server"
|
||||
)
|
||||
|
||||
# Frame range.
|
||||
frame_start_offset: bpy.props.IntProperty(name="Frame Start Offset")
|
||||
|
||||
# Media.
|
||||
media_outdated: bpy.props.BoolProperty(
|
||||
name="Source Media Outdated",
|
||||
default=False,
|
||||
description="Indicated if there is a newer version of the source media available",
|
||||
)
|
||||
|
||||
# Display props.
|
||||
shot_description_display: bpy.props.StringProperty(name="Description", get=_get_shot_description) # type: ignore
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.shot,
|
||||
"sequence_name": self.sequence,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
self.shot_id = ""
|
||||
self.shot_name = ""
|
||||
self.shot_description = ""
|
||||
|
||||
self.sequence_id = ""
|
||||
self.sequence_name = ""
|
||||
|
||||
self.project_name = ""
|
||||
self.project_id = ""
|
||||
|
||||
self.initialized = False
|
||||
self.linked = False
|
||||
|
||||
self.frame_start_offset = 0
|
||||
|
||||
def unlink(self):
|
||||
self.sequence_id = ""
|
||||
|
||||
self.project_name = ""
|
||||
self.project_id = ""
|
||||
|
||||
self.linked = False
|
||||
|
||||
|
||||
def set_kitsu_entity_id_via_enum_name(
|
||||
self,
|
||||
items: List[Tuple[str, str, str]],
|
||||
input_name: str,
|
||||
name_prop: str,
|
||||
id_prop: str,
|
||||
) -> str:
|
||||
"""Set the ID and Name of a Kitsu entity by finding the matching ID for a given name
|
||||
and updating both values in their corrisponding String Properties (Kitsu Context Properties)
|
||||
|
||||
Args:
|
||||
items (List[Tuple[str, str, str]]): Enum items in the format List[Tuple[id, name, description]]
|
||||
input_name (str): Name, used to find matching ID in a tuple
|
||||
name_prop (str): Name of String Property where Kitsu Enitity's Name is stored
|
||||
id_prop (str): Name of String Property where Kitsu Enitity's ID is stored
|
||||
|
||||
Returns:
|
||||
str: ID of Kitsu entity
|
||||
"""
|
||||
|
||||
if input_name == "":
|
||||
self[id_prop] = ""
|
||||
self[name_prop] = input_name
|
||||
return
|
||||
for key, value, _ in items:
|
||||
if value == input_name:
|
||||
self[id_prop] = key
|
||||
self[name_prop] = input_name
|
||||
return key
|
||||
|
||||
|
||||
def get_enum_item_names(enum_items: List[Tuple[str, str, str]]) -> List[str]:
|
||||
"""Return a list of names from a list of enum items used by (Kitsu Context Properties)
|
||||
|
||||
Args:
|
||||
enum_items (List[Tuple[str, str, str]]): Enum items in the format List[Tuple[id, name, description]]
|
||||
|
||||
Returns:
|
||||
List[str]: List of avaliable names
|
||||
"""
|
||||
return [item[1] for item in enum_items]
|
||||
|
||||
|
||||
def get_safely_string_prop(self, name: str) -> str:
|
||||
"""Return Value of String Property, and return "" if value isn't set"""
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
|
||||
class KITSU_property_group_scene(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
################################
|
||||
# Kitsu Context Properties
|
||||
################################
|
||||
"""
|
||||
Kitsu Context Properties
|
||||
|
||||
These are properties used to store/manage the current "Context"
|
||||
for a Kitsu Entity. Kitsu Entities represents things like Asset,
|
||||
Shot and Sequence for a given production.
|
||||
|
||||
Each Entity has an 'ID' Property which is used internally by the add-on
|
||||
and a 'Name' Property which is used as part of the user interface. When a user selects
|
||||
the 'Name' of a Kitsu Entity a custom Set function on the property will also update
|
||||
the Entity's ID.
|
||||
|
||||
NOTE: It would be nice to have searchable enums instead of doing all this work manually.
|
||||
"""
|
||||
|
||||
asset_col: bpy.props.PointerProperty(type=bpy.types.Collection, name="Collection")
|
||||
|
||||
###########
|
||||
# Sequence
|
||||
###########
|
||||
sequence_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Sequence ID",
|
||||
description="ID that refers to the active sequence on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_sequences_via_name(self):
|
||||
return get_safely_string_prop(self, "sequence_active_name")
|
||||
|
||||
def set_sequences_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_sequences_enum_list(self, bpy.context),
|
||||
name_prop='sequence_active_name',
|
||||
id_prop='sequence_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.sequence_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.sequence_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_sequence_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context))
|
||||
|
||||
sequence_active_name: bpy.props.StringProperty(
|
||||
name="Sequence",
|
||||
description="Sequence",
|
||||
default="", # type: ignore
|
||||
get=get_sequences_via_name,
|
||||
set=set_sequences_via_name,
|
||||
options=set(),
|
||||
search=get_sequence_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
###########
|
||||
# Episode
|
||||
###########
|
||||
episode_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Episode ID",
|
||||
description="ID that refers to the active episode on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_episode_via_name(self):
|
||||
return get_safely_string_prop(self, "episode_active_name")
|
||||
|
||||
def set_episode_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_episodes_enum_list(self, bpy.context),
|
||||
name_prop='episode_active_name',
|
||||
id_prop='episode_active_id',
|
||||
)
|
||||
|
||||
# Clear active shot when sequence changes.
|
||||
if cache.episode_active_get().id != key:
|
||||
cache.sequence_active_reset(bpy.context)
|
||||
cache.asset_type_active_reset(bpy.context)
|
||||
cache.shot_active_reset(bpy.context)
|
||||
cache.asset_active_reset(bpy.context)
|
||||
|
||||
if key:
|
||||
cache.episode_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.episode_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_episode_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_episodes_enum_list(self, bpy.context))
|
||||
|
||||
episode_active_name: bpy.props.StringProperty(
|
||||
name="Episode",
|
||||
description="Selet Active Episode",
|
||||
default="", # type: ignore
|
||||
get=get_episode_via_name,
|
||||
set=set_episode_via_name,
|
||||
options=set(),
|
||||
search=get_episode_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
###########
|
||||
# Shot
|
||||
###########
|
||||
shot_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Shot ID",
|
||||
description="IDthat refers to the active shot on server",
|
||||
default="",
|
||||
# update=propsdata.on_shot_change,
|
||||
)
|
||||
|
||||
def get_shot_via_name(self):
|
||||
return get_safely_string_prop(self, "shot_active_name")
|
||||
|
||||
def set_shot_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_shots_enum_for_active_seq(self, bpy.context),
|
||||
name_prop='shot_active_name',
|
||||
id_prop='shot_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.shot_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.shot_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_shot_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_shots_enum_for_active_seq(self, bpy.context))
|
||||
|
||||
shot_active_name: bpy.props.StringProperty(
|
||||
name="Shot",
|
||||
description="Shot",
|
||||
default="", # type: ignore
|
||||
get=get_shot_via_name,
|
||||
set=set_shot_via_name,
|
||||
options=set(),
|
||||
search=get_shot_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
###########
|
||||
# Asset
|
||||
###########
|
||||
asset_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Asset ID",
|
||||
description="ID that refers to the active asset on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_asset_via_name(self):
|
||||
return get_safely_string_prop(self, "asset_active_name")
|
||||
|
||||
def set_asset_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_assets_enum_for_active_asset_type(self, bpy.context),
|
||||
name_prop='asset_active_name',
|
||||
id_prop='asset_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.asset_active_set_by_id(bpy.context, key) # TODO
|
||||
else:
|
||||
cache.asset_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_asset_type_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_assets_enum_for_active_asset_type(self, bpy.context))
|
||||
|
||||
asset_active_name: bpy.props.StringProperty(
|
||||
name="Asset",
|
||||
description="Active Asset",
|
||||
default="", # type: ignore
|
||||
get=get_asset_via_name,
|
||||
set=set_asset_via_name,
|
||||
options=set(),
|
||||
search=get_asset_type_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
############
|
||||
# Asset Type
|
||||
############
|
||||
|
||||
asset_type_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Asset Type ID",
|
||||
description="ID that refers to the active asset type on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_asset_type_via_name(self):
|
||||
return get_safely_string_prop(self, "asset_type_active_name")
|
||||
|
||||
def set_asset_type_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_assetypes_enum_list(self, bpy.context),
|
||||
name_prop='asset_type_active_name',
|
||||
id_prop='asset_type_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.asset_type_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.asset_type_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_asset_type_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_assetypes_enum_list(self, bpy.context))
|
||||
|
||||
asset_type_active_name: bpy.props.StringProperty(
|
||||
name="Asset Type",
|
||||
description="Active Asset Type Name",
|
||||
default="", # type: ignore
|
||||
get=get_asset_type_via_name,
|
||||
set=set_asset_type_via_name,
|
||||
options=set(),
|
||||
search=get_asset_type_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
############
|
||||
# Task Type
|
||||
############
|
||||
|
||||
task_type_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Task Type ID",
|
||||
description="ID that refers to the active task type on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_task_type_via_name(self):
|
||||
return get_safely_string_prop(self, "task_type_active_name")
|
||||
|
||||
def set_task_type_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_task_types_enum_for_current_context(self, bpy.context),
|
||||
name_prop='task_type_active_name',
|
||||
id_prop='task_type_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.task_type_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.task_type_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_task_type_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_task_types_enum_for_current_context(self, bpy.context))
|
||||
|
||||
task_type_active_name: bpy.props.StringProperty(
|
||||
name="Task Type",
|
||||
description="Active Task Type Name",
|
||||
default="", # type: ignore
|
||||
get=get_task_type_via_name,
|
||||
set=set_task_type_via_name,
|
||||
options=set(),
|
||||
search=get_task_type_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
category: bpy.props.EnumProperty( # type: ignore
|
||||
name="Type",
|
||||
description="Kitsu entity type",
|
||||
items=(
|
||||
("ASSET", "Asset", "Asset related tasks", 0),
|
||||
("SHOT", "Shot", "Shot related tasks", 1),
|
||||
("SEQ", "Sequence", "Sequence related tasks", 2),
|
||||
("EDIT", "Edit", "Edit related tasks", 3),
|
||||
),
|
||||
default="SHOT",
|
||||
update=propsdata.reset_all_kitsu_props,
|
||||
)
|
||||
|
||||
############
|
||||
# Edit
|
||||
############
|
||||
edit_active_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Active Edit ID",
|
||||
description="ID that refers to the active edit on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_edit_via_name(self):
|
||||
return get_safely_string_prop(self, "edit_active_name")
|
||||
|
||||
def set_edit_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_all_edits_enum_for_active_project(self, bpy.context),
|
||||
name_prop='edit_active_name',
|
||||
id_prop='edit_active_id',
|
||||
)
|
||||
if key:
|
||||
cache.edit_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.edit_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_edit_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_all_edits_enum_for_active_project(self, bpy.context))
|
||||
|
||||
edit_active_name: bpy.props.StringProperty(
|
||||
name="Edit",
|
||||
description="Active Edit Name",
|
||||
default="", # type: ignore
|
||||
get=get_edit_via_name,
|
||||
set=set_edit_via_name,
|
||||
options=set(),
|
||||
search=get_edit_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
edit_export_version: bpy.props.StringProperty(name="Version", default="v001")
|
||||
|
||||
edit_export_file: bpy.props.StringProperty( # type: ignore
|
||||
name="Edit Export Filepath",
|
||||
description="Output filepath of Edit Export",
|
||||
default="",
|
||||
subtype="FILE_PATH",
|
||||
get=propsdata.get_edit_export_file,
|
||||
)
|
||||
|
||||
# Thumbnail props.
|
||||
task_type_thumbnail_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Thumbnail Task Type ID",
|
||||
description="ID that refers to the task type on server for which thumbnails will be uploaded",
|
||||
default="",
|
||||
)
|
||||
|
||||
task_type_thumbnail_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Thumbnail Task Type Name",
|
||||
description="Name that refers to the task type on server for which thumbnails will be uploaded",
|
||||
default="",
|
||||
)
|
||||
|
||||
# Sqe render props.
|
||||
task_type_sqe_render_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Sqe Render Task Type ID",
|
||||
description="ID that refers to the task type on server for which the sqe render will be uploaded",
|
||||
default="",
|
||||
)
|
||||
|
||||
task_type_sqe_render_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Sqe Render Task Type Name",
|
||||
description="Name that refers to the task type on server for which the sqe render will be uploaded",
|
||||
default="",
|
||||
)
|
||||
|
||||
# Playblast props.
|
||||
|
||||
playblast_version: bpy.props.StringProperty(name="Version", default="v001")
|
||||
|
||||
playblast_dir: bpy.props.StringProperty( # type: ignore
|
||||
name="Playblast Directory",
|
||||
description="Directory in which playblasts will be saved",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
get=propsdata.get_playblast_dir,
|
||||
)
|
||||
|
||||
playblast_file: bpy.props.StringProperty( # type: ignore
|
||||
name="Playblast Filepath",
|
||||
description="Output filepath of playblast",
|
||||
default="",
|
||||
subtype="FILE_PATH",
|
||||
get=propsdata.get_playblast_file,
|
||||
)
|
||||
|
||||
playblast_task_status_id: bpy.props.StringProperty( # type: ignore
|
||||
name="Plablast Task Status ID",
|
||||
description="ID that refers to the task status on server which the playblast will set",
|
||||
default="",
|
||||
)
|
||||
|
||||
playblast_render_mode: bpy.props.EnumProperty( # type: ignore
|
||||
name="Playblast Render Mode",
|
||||
description="Choose to either Render Playblast from current Viewport or use scene's render settings",
|
||||
items=[
|
||||
(
|
||||
"SCENE",
|
||||
"Scene",
|
||||
"Render using the scene's render settings, playblast will match 'Render Animation's' output exactly",
|
||||
),
|
||||
(
|
||||
"VIEWPORT",
|
||||
"Viewport",
|
||||
"Render from the current viewport, with viewport shading settings and viewport overlays; what you see is what you get",
|
||||
),
|
||||
(
|
||||
"VIEWPORT_PRESET",
|
||||
"Viewport with Preset Shading",
|
||||
"Render from the current viewport with Add-On's default shading settings, and automatically hide all overlays",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Sequence editor tools.
|
||||
pull_edit_channel: bpy.props.IntProperty(
|
||||
name="Channel",
|
||||
description="On which channel the operator will create the color strips",
|
||||
default=1,
|
||||
min=1,
|
||||
max=32,
|
||||
)
|
||||
|
||||
sequence_colors: bpy.props.CollectionProperty(type=KITSU_sequence_colors)
|
||||
|
||||
|
||||
class KITSU_property_group_error(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
frame_range: bpy.props.BoolProperty( # type: ignore
|
||||
name="Frame Range Error",
|
||||
description="Indicates if the scene frame range does not match the one in Kitsu",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class KITSU_property_group_window_manager(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
tasks_index: bpy.props.IntProperty(name="Tasks Index", default=0)
|
||||
|
||||
quick_duplicate_amount: bpy.props.IntProperty(
|
||||
name="Amount",
|
||||
default=1,
|
||||
min=1,
|
||||
description="Specifies the number of copies that will be created",
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
pass
|
||||
|
||||
|
||||
def _add_window_manager_props():
|
||||
# Multi Edit Properties.
|
||||
bpy.types.WindowManager.show_advanced = bpy.props.BoolProperty(
|
||||
name="Show Advanced",
|
||||
description="Shows advanced options to fine control shot pattern",
|
||||
default=False,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.var_use_custom_seq = bpy.props.BoolProperty(
|
||||
name="Use Custom",
|
||||
description="Enables to type in custom sequence name for <Sequence> wildcard",
|
||||
default=False,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.var_use_custom_project = bpy.props.BoolProperty(
|
||||
name="Use Custom",
|
||||
description="Enables to type in custom project name for <Project> wildcard",
|
||||
default=False,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.var_sequence_custom = bpy.props.StringProperty( # type: ignore
|
||||
name="Custom Sequence Variable",
|
||||
description="Value that will be used to insert in <Sequence> wildcard if custom sequence is enabled",
|
||||
default="",
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.var_project_custom = bpy.props.StringProperty( # type: ignore
|
||||
name="Custom Project Variable",
|
||||
description="Value that will be used to insert in <Project> wildcard if custom project is enabled",
|
||||
default="",
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.shot_counter_start = bpy.props.IntProperty(
|
||||
description="Value that defines where the shot counter starts",
|
||||
step=10,
|
||||
min=0,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.shot_preview = bpy.props.StringProperty(
|
||||
name="Shot Pattern",
|
||||
description="Preview result of current settings on how a shot will be named",
|
||||
get=propsdata._gen_shot_preview,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.var_project_active = bpy.props.StringProperty(
|
||||
name="Active Project",
|
||||
description="Value that will be inserted in <Project> wildcard",
|
||||
get=propsdata._get_project_active,
|
||||
)
|
||||
|
||||
###########
|
||||
# Sequence
|
||||
###########
|
||||
bpy.types.WindowManager.selected_sequence_id = bpy.props.StringProperty( # type: ignore
|
||||
name="Active Sequence ID",
|
||||
description="ID that refers to the active sequence on server",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_sequences_via_name(self):
|
||||
return get_safely_string_prop(self, "selected_sequence_name")
|
||||
|
||||
def set_sequences_via_name(self, input):
|
||||
key = set_kitsu_entity_id_via_enum_name(
|
||||
self=self,
|
||||
input_name=input,
|
||||
items=cache.get_sequences_enum_list(self, bpy.context),
|
||||
name_prop='selected_sequence_name',
|
||||
id_prop='selected_sequence_id',
|
||||
)
|
||||
if key:
|
||||
cache.sequence_active_set_by_id(bpy.context, key)
|
||||
else:
|
||||
cache.sequence_active_reset_entity()
|
||||
return
|
||||
|
||||
def get_sequence_search_list(self, context, edit_text):
|
||||
return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context))
|
||||
|
||||
bpy.types.WindowManager.selected_sequence_name = bpy.props.StringProperty(
|
||||
name="Sequence",
|
||||
description="Name of Sequence the generated Shots will be assinged to",
|
||||
default="", # type: ignore
|
||||
get=get_sequences_via_name,
|
||||
set=set_sequences_via_name,
|
||||
options=set(),
|
||||
search=get_sequence_search_list,
|
||||
search_options={'SORT'},
|
||||
)
|
||||
|
||||
# Advanced delete props.
|
||||
bpy.types.WindowManager.advanced_delete = bpy.props.BoolProperty(
|
||||
name="Advanced Delete",
|
||||
description="Checkbox to show advanced shot deletion operations",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
def _clear_window_manager_props():
|
||||
del bpy.types.WindowManager.show_advanced
|
||||
del bpy.types.WindowManager.var_use_custom_seq
|
||||
del bpy.types.WindowManager.var_use_custom_project
|
||||
del bpy.types.WindowManager.var_sequence_custom
|
||||
del bpy.types.WindowManager.var_project_custom
|
||||
del bpy.types.WindowManager.shot_counter_start
|
||||
del bpy.types.WindowManager.shot_preview
|
||||
del bpy.types.WindowManager.var_project_active
|
||||
del bpy.types.WindowManager.selected_sequence_id
|
||||
del bpy.types.WindowManager.selected_sequence_name
|
||||
|
||||
|
||||
def _calc_kitsu_3d_start(self):
|
||||
"""
|
||||
Calculates strip.kitsu_3d_start, little hack because it seems like we cant access the strip from a property group
|
||||
But we need acess to seqeuence properties.
|
||||
"""
|
||||
return int(self.frame_final_start - self.frame_start + bkglobals.FRAME_START)
|
||||
|
||||
|
||||
def _calc_kitsu_frame_end(self):
|
||||
"""
|
||||
Calculates strip.kitsu_frame_end, little hack because it seems like we cant access the strip from a property group
|
||||
But we need acess to seqeuence properties.
|
||||
"""
|
||||
frame_start = _calc_kitsu_3d_start(self)
|
||||
frame_end_final = frame_start + (self.frame_final_duration - 1)
|
||||
return int(frame_end_final)
|
||||
|
||||
|
||||
def _get_frame_final_duration(self):
|
||||
return self.frame_final_duration
|
||||
|
||||
|
||||
def _get_strip_filepath(self):
|
||||
return self.filepath
|
||||
|
||||
|
||||
@persistent
|
||||
def update_sequence_colors_coll_prop(dummy: Any) -> None:
|
||||
sqe = bpy.context.scene.sequence_editor
|
||||
if not sqe:
|
||||
return
|
||||
sequences = sqe.sequences_all
|
||||
sequence_colors = bpy.context.scene.kitsu.sequence_colors
|
||||
existings_seq_ids: List[str] = []
|
||||
|
||||
# Append missing sequences to scene.kitsu.seqeuence_colors.
|
||||
for seq in sequences:
|
||||
if not seq.kitsu.sequence_id:
|
||||
continue
|
||||
|
||||
if seq.kitsu.sequence_id not in sequence_colors.keys():
|
||||
logger.info("Added %s to scene.kitsu.seqeuence_colors", seq.kitsu.sequence_name)
|
||||
item = sequence_colors.add()
|
||||
item.name = seq.kitsu.sequence_id
|
||||
|
||||
existings_seq_ids.append(seq.kitsu.sequence_id)
|
||||
|
||||
# Delete sequence colors that are not in edit anymore.
|
||||
existings_seq_ids = set(existings_seq_ids)
|
||||
|
||||
to_be_removed = [seq_id for seq_id in sequence_colors.keys() if seq_id not in existings_seq_ids]
|
||||
|
||||
for seq_id in to_be_removed:
|
||||
idx = sequence_colors.find(seq_id)
|
||||
if idx == -1:
|
||||
continue
|
||||
|
||||
sequence_colors.remove(idx)
|
||||
logger.info(
|
||||
"Removed %s from scene.kitsu.seqeuence_colors. Is not used in the sequence editor anymore",
|
||||
seq_id,
|
||||
)
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
KITSU_sequence_colors,
|
||||
KITSU_property_group_sequence,
|
||||
KITSU_property_group_scene,
|
||||
KITSU_property_group_error,
|
||||
KITSU_property_group_window_manager,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# FRAME RANGE PROPERTIES
|
||||
# because we cant acess strip properties from a sequence group we need to create this properties
|
||||
# directly on the strip, as we need strip properties to calculate
|
||||
bpy.types.Strip.kitsu_3d_start = bpy.props.IntProperty(
|
||||
name="3D In",
|
||||
get=_calc_kitsu_3d_start,
|
||||
)
|
||||
|
||||
bpy.types.Strip.kitsu_frame_end = bpy.props.IntProperty(
|
||||
name="3D Out",
|
||||
get=_calc_kitsu_frame_end,
|
||||
)
|
||||
bpy.types.Strip.kitsu_frame_duration = bpy.props.IntProperty(
|
||||
name="Duration",
|
||||
get=_get_frame_final_duration,
|
||||
)
|
||||
|
||||
# Used in general tools panel next to sqe_change_strip_source operator.
|
||||
bpy.types.MovieStrip.filepath_display = bpy.props.StringProperty(
|
||||
name="Filepath Display", get=_get_strip_filepath
|
||||
)
|
||||
|
||||
# Sequence Properties.
|
||||
bpy.types.Strip.kitsu = bpy.props.PointerProperty(
|
||||
name="Kitsu",
|
||||
type=KITSU_property_group_sequence,
|
||||
description="Metadata that is required for blender_kitsu",
|
||||
)
|
||||
|
||||
# Scene Properties.
|
||||
bpy.types.Scene.kitsu = bpy.props.PointerProperty(
|
||||
name="Kitsu",
|
||||
type=KITSU_property_group_scene,
|
||||
description="Metadata that is required for blender_kitsu",
|
||||
)
|
||||
# Window Manager.
|
||||
bpy.types.WindowManager.kitsu = bpy.props.PointerProperty(
|
||||
name="Kitsu",
|
||||
type=KITSU_property_group_window_manager,
|
||||
description="Metadata that is required for blender_kitsu",
|
||||
)
|
||||
|
||||
# Error Properties.
|
||||
bpy.types.Scene.kitsu_error = bpy.props.PointerProperty(
|
||||
name="Kitsu Error",
|
||||
type=KITSU_property_group_error,
|
||||
description="Error property group",
|
||||
)
|
||||
|
||||
# Window Manager Properties.
|
||||
_add_window_manager_props()
|
||||
|
||||
# Handlers.
|
||||
bpy.app.handlers.load_post.append(update_sequence_colors_coll_prop)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# Clear properties.
|
||||
_clear_window_manager_props()
|
||||
|
||||
# Handlers.
|
||||
bpy.app.handlers.load_post.remove(update_sequence_colors_coll_prop)
|
||||
@@ -1,225 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from . import bkglobals
|
||||
from . import cache, prefs
|
||||
|
||||
# TODO: restructure that to not import from anim.
|
||||
from .playblast import ops as ops_playblast
|
||||
from .playblast import opsdata as ops_playblast_data
|
||||
from .logger import LoggerFactory
|
||||
from .context import core as context_core
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
# Get functions for window manager properties.
|
||||
def _get_project_active(self):
|
||||
return cache.project_active_get().name
|
||||
|
||||
|
||||
def _resolve_pattern(pattern: str, var_lookup_table: Dict[str, str]) -> str:
|
||||
matches = re.findall(r"\<(\w+)\>", pattern)
|
||||
matches = list(set(matches))
|
||||
# If no variable detected just return value.
|
||||
if len(matches) == 0:
|
||||
return pattern
|
||||
else:
|
||||
result = pattern
|
||||
for to_replace in matches:
|
||||
if to_replace in var_lookup_table:
|
||||
to_insert = var_lookup_table[to_replace]
|
||||
result = result.replace("<{}>".format(to_replace), to_insert)
|
||||
else:
|
||||
logger.warning("Failed to resolve variable: %s not defined!", to_replace)
|
||||
return ""
|
||||
return result
|
||||
|
||||
|
||||
def _get_sequences(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
addon_prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
project_active = cache.project_active_get()
|
||||
|
||||
if not project_active or not addon_prefs.session.is_auth:
|
||||
return [("None", "None", "")]
|
||||
|
||||
enum_list = [(s.name, s.name, "") for s in project_active.get_sequences_all()]
|
||||
return enum_list
|
||||
|
||||
|
||||
def _gen_shot_preview(self: Any) -> str:
|
||||
addon_prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
shot_counter_increment = addon_prefs.shot_counter_increment
|
||||
shot_counter_digits = addon_prefs.shot_counter_digits
|
||||
shot_counter_start = self.shot_counter_start
|
||||
shot_pattern = addon_prefs.shot_pattern
|
||||
examples: List[str] = []
|
||||
sequence = self.selected_sequence_name
|
||||
episode = cache.episode_active_get()
|
||||
var_project = (
|
||||
self.var_project_custom if self.var_use_custom_project else self.var_project_active
|
||||
)
|
||||
var_sequence = self.var_sequence_custom if self.var_use_custom_seq else sequence
|
||||
var_lookup_table = {
|
||||
"Sequence": var_sequence,
|
||||
"Project": var_project,
|
||||
"Episode": episode.name,
|
||||
}
|
||||
|
||||
for count in range(3):
|
||||
counter_number = shot_counter_start + (shot_counter_increment * count)
|
||||
counter = str(counter_number).rjust(shot_counter_digits, "0")
|
||||
var_lookup_table["Counter"] = counter
|
||||
examples.append(_resolve_pattern(shot_pattern, var_lookup_table))
|
||||
|
||||
return " | ".join(examples) + "..."
|
||||
|
||||
|
||||
def get_task_type_name_file_suffix() -> str:
|
||||
name = cache.task_type_active_get().name.lower()
|
||||
|
||||
task_mappings = {**bkglobals.SHOT_TASK_MAPPING, **bkglobals.SEQ_TASK_MAPPING}
|
||||
for key, value in task_mappings.items():
|
||||
if name == value.lower():
|
||||
return key
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def get_playblast_dir(self: Any) -> str:
|
||||
# shared/editorial/footage/{dev,pre,pro,post}
|
||||
# shared/editorial/<episode>/footage/{dev,pre,pro,post}
|
||||
|
||||
# shared/editorial/footage//110_rextoria/110_0030/110_0030-anim
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
if not addon_prefs.is_playblast_root_valid:
|
||||
return ""
|
||||
|
||||
episode = cache.episode_active_get()
|
||||
sequence = cache.sequence_active_get()
|
||||
shot = cache.shot_active_get()
|
||||
delimiter = bkglobals.DELIMITER
|
||||
|
||||
# Start building path
|
||||
if context_core.is_sequence_context():
|
||||
playblast_dir = addon_prefs.seq_playblast_root_path
|
||||
else:
|
||||
playblast_dir = addon_prefs.shot_playblast_root_path
|
||||
|
||||
playblast_dir = set_episode_variable(playblast_dir)
|
||||
|
||||
if context_core.is_sequence_context():
|
||||
playblast_dir = playblast_dir / sequence.name / 'sequence_previews'
|
||||
return playblast_dir.as_posix()
|
||||
|
||||
task_type_name_suffix = get_task_type_name_file_suffix()
|
||||
|
||||
playblast_dir = (
|
||||
playblast_dir / sequence.name / shot.name / f"{shot.name}{delimiter}{task_type_name_suffix}"
|
||||
)
|
||||
return playblast_dir.as_posix()
|
||||
|
||||
|
||||
def get_playblast_file(self: Any) -> str:
|
||||
if not self.playblast_dir:
|
||||
return ""
|
||||
|
||||
task_type_name_suffix = get_task_type_name_file_suffix()
|
||||
version = self.playblast_version
|
||||
shot = cache.shot_active_get()
|
||||
episode = cache.episode_active_get()
|
||||
sequence = cache.sequence_active_get()
|
||||
delimiter = bkglobals.DELIMITER
|
||||
|
||||
# 070_0010_A-anim-v001.mp4.
|
||||
|
||||
kitsu_props = bpy.context.scene.kitsu
|
||||
kitsu_props.get("category")
|
||||
|
||||
if context_core.is_sequence_context():
|
||||
# Assuming sequences called 000_name, get 000
|
||||
entity_name = sequence.name.split('_')[0]
|
||||
if episode:
|
||||
# If episode is present, append to the name
|
||||
entity_name = f"{episode.name}_{entity_name}"
|
||||
elif context_core.is_shot_context():
|
||||
entity_name = shot.name
|
||||
else:
|
||||
entity_name = ''
|
||||
|
||||
file_name = f"{entity_name}{delimiter}{task_type_name_suffix}{delimiter}{version}.mp4"
|
||||
|
||||
return Path(self.playblast_dir).joinpath(file_name).as_posix()
|
||||
|
||||
|
||||
def get_edit_export_dir() -> str:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
return set_episode_variable(Path(addon_prefs.edit_export_dir))
|
||||
|
||||
|
||||
def get_edit_export_file(self: Any) -> str:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
export_dir = get_edit_export_dir()
|
||||
version = self.edit_export_version
|
||||
file_pattern = addon_prefs.edit_export_file_pattern
|
||||
file_name = file_pattern.replace('v###', version)
|
||||
return export_dir.joinpath(file_name).as_posix()
|
||||
|
||||
|
||||
_active_category_cache_init: bool = False
|
||||
_active_category_cache: str = ""
|
||||
|
||||
|
||||
def reset_task_type(self: Any, context: bpy.types.Context) -> None:
|
||||
global _active_category_cache_init
|
||||
global _active_category_cache
|
||||
|
||||
if not _active_category_cache_init:
|
||||
_active_category_cache = self.category
|
||||
_active_category_cache_init = True
|
||||
return
|
||||
|
||||
if self.category == _active_category_cache:
|
||||
return None
|
||||
|
||||
cache.task_type_active_reset(context)
|
||||
_active_category_cache = self.category
|
||||
return None
|
||||
|
||||
|
||||
def on_shot_change(self: Any, context: bpy.types.Context) -> None:
|
||||
# Reset versions.
|
||||
ops_playblast_data.init_playblast_file_model(context)
|
||||
|
||||
# Check frame range.
|
||||
ops_playblast.load_post_handler_check_frame_range(context)
|
||||
|
||||
|
||||
def reset_all_kitsu_props(self: Any, context: bpy.types.Context) -> None:
|
||||
cache.sequence_active_reset(context)
|
||||
cache.asset_type_active_reset(context)
|
||||
cache.shot_active_reset(context)
|
||||
cache.asset_active_reset(context)
|
||||
cache.episode_active_reset(context)
|
||||
cache.task_type_active_reset(context)
|
||||
|
||||
|
||||
def set_episode_variable(base_path: Path) -> Path:
|
||||
episode = cache.episode_active_get()
|
||||
active_project = cache.project_active_get()
|
||||
if not (
|
||||
episode
|
||||
and '<episode>' in base_path.parts
|
||||
and active_project.production_type == bkglobals.KITSU_TV_PROJECT
|
||||
):
|
||||
return base_path
|
||||
i = base_path.parts.index('<episode>')
|
||||
return Path(*base_path.parts[:i]).joinpath(episode.name, *base_path.parts[i + 1 :])
|
||||
@@ -1,44 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from . import (
|
||||
util,
|
||||
props,
|
||||
opsdata,
|
||||
checksqe,
|
||||
ops,
|
||||
ui,
|
||||
draw,
|
||||
)
|
||||
|
||||
|
||||
_need_reload = "ops" in locals()
|
||||
|
||||
|
||||
if _need_reload:
|
||||
import importlib
|
||||
|
||||
util = importlib.reload(util)
|
||||
props = importlib.reload(props)
|
||||
opsdata = importlib.reload(opsdata)
|
||||
checksqe = importlib.reload(checksqe)
|
||||
ops = importlib.reload(ops)
|
||||
ui = importlib.reload(ui)
|
||||
draw = importlib.reload(draw)
|
||||
|
||||
|
||||
def register():
|
||||
props.register()
|
||||
ops.register()
|
||||
ui.register()
|
||||
draw.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
draw.unregister()
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
props.unregister()
|
||||
@@ -1,78 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def _do_ranges_collide(range1: range, range2: range) -> bool:
|
||||
"""Whether the two ranges collide with each other ."""
|
||||
# usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140)
|
||||
# first and last frame can be the same for each strip
|
||||
range2 = range(range2.start + 1, range2.stop - 1)
|
||||
|
||||
if not range1:
|
||||
return True # empty range is subset of anything
|
||||
|
||||
if not range2:
|
||||
return False # non-empty range can't be subset of empty range
|
||||
|
||||
if len(range1) > 1 and range1.step % range2.step:
|
||||
return False # must have a single value or integer multiple step
|
||||
|
||||
if range(range1.start + 1, range1.stop - 1) == range2:
|
||||
return True
|
||||
|
||||
if range2.start in range1 or range2[-1] in range1:
|
||||
return True
|
||||
|
||||
return range1.start in range2 or range1[-1] in range2
|
||||
|
||||
|
||||
def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]:
|
||||
"""
|
||||
Scans sequence editor and returns a dictionary. It contains a key for each channel
|
||||
and a list of ranges with the occupied frame ranges as values.
|
||||
"""
|
||||
# {'1': [(101, 213), (300, 320)]}.
|
||||
ranges: Dict[str, List[range]] = {}
|
||||
|
||||
# Populate ranges.
|
||||
for strip in context.scene.sequence_editor.sequences_all:
|
||||
ranges.setdefault(str(strip.channel), [])
|
||||
ranges[str(strip.channel)].append(
|
||||
range(strip.frame_final_start, strip.frame_final_end + 1)
|
||||
)
|
||||
|
||||
# Sort ranges tuple list.
|
||||
for channel in ranges:
|
||||
liste = ranges[channel]
|
||||
ranges[channel] = sorted(liste, key=lambda item: item.start)
|
||||
|
||||
return ranges
|
||||
|
||||
|
||||
def get_occupied_ranges_for_strips(sequences: List[bpy.types.Strip]) -> List[range]:
|
||||
"""
|
||||
Scans input list of sequences and returns a list of ranges that represent the occupied frame ranges.
|
||||
"""
|
||||
ranges: List[range] = []
|
||||
|
||||
# Populate ranges.
|
||||
for strip in sequences:
|
||||
ranges.append(range(strip.frame_final_start, strip.frame_final_end + 1))
|
||||
|
||||
# Sort ranges tuple list.
|
||||
ranges.sort(key=lambda item: item.start)
|
||||
return ranges
|
||||
|
||||
|
||||
def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool:
|
||||
for r in occupied_ranges:
|
||||
# Range(101, 150).
|
||||
if _do_ranges_collide(range_to_check, r):
|
||||
return True
|
||||
continue
|
||||
return False
|
||||
@@ -1,207 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import typing
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
APPROVED_COLOR = (0.24, 1, 0.139, 0.7)
|
||||
PUSHED_TO_EDIT_COLOR = (0.8, .8, 0.1, 0.5)
|
||||
|
||||
|
||||
Float2 = typing.Tuple[float, float]
|
||||
Float3 = typing.Tuple[float, float, float]
|
||||
Float4 = typing.Tuple[float, float, float, float]
|
||||
LINE_WIDTH = 6
|
||||
|
||||
|
||||
class LineDrawer:
|
||||
|
||||
def draw(self, coords: typing.List[Float2], colors: typing.List[Float4]):
|
||||
line_shader = gpu.shader.from_builtin('FLAT_COLOR')
|
||||
global LINE_WIDTH
|
||||
|
||||
if not coords:
|
||||
return
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(LINE_WIDTH)
|
||||
|
||||
batch = batch_for_shader(
|
||||
line_shader, 'LINES',
|
||||
{"pos": coords, "color": colors}
|
||||
)
|
||||
batch.draw(line_shader)
|
||||
|
||||
def get_strip_rectf(strip) -> Float4:
|
||||
# Get x and y in terms of the grid's frames and channels.
|
||||
x1 = strip.frame_final_start
|
||||
x2 = strip.frame_final_end
|
||||
# Seems to be a 5 % offset from channel top start of strip.
|
||||
y1 = strip.channel + 0.05
|
||||
y2 = strip.channel - 0.05 + 1
|
||||
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
def line_in_strip(
|
||||
strip_coords: Float4,
|
||||
pixel_size_x: float,
|
||||
color: Float4,
|
||||
line_height_factor: float,
|
||||
out_coords: typing.List[Float2],
|
||||
out_colors: typing.List[Float4],
|
||||
):
|
||||
# Strip coords.
|
||||
s_x1, s_y1, s_x2, s_y2 = strip_coords
|
||||
|
||||
# Calculate line height with factor.
|
||||
line_y = (1 - line_height_factor) * s_y1 + line_height_factor * s_y2
|
||||
|
||||
# if strip is shorter than line_width use stips s_x2
|
||||
# line_x2 = s_x1 + line_width if (s_x2 - s_x1 > line_width) else s_x2
|
||||
line_x2 = s_x2
|
||||
|
||||
# Be careful not to draw over the current frame line.
|
||||
cf_x = bpy.context.scene.frame_current_final
|
||||
|
||||
# TODO(Sybren): figure out how to pass one colour per line,
|
||||
# instead of one colour per vertex.
|
||||
out_coords.append((s_x1, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
if s_x1 < cf_x < line_x2:
|
||||
# Bad luck, the line passes our strip, so draw two lines.
|
||||
out_coords.append((cf_x - pixel_size_x, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
out_coords.append((cf_x + pixel_size_x, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
out_coords.append((line_x2, line_y))
|
||||
out_colors.append(color)
|
||||
|
||||
|
||||
def draw_callback_px(line_drawer: LineDrawer):
|
||||
global LINE_WIDTH
|
||||
|
||||
context = bpy.context
|
||||
|
||||
if not context.scene.sequence_editor:
|
||||
return
|
||||
|
||||
# From . import shown_strips.
|
||||
|
||||
region = context.region
|
||||
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
|
||||
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
|
||||
one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1)
|
||||
pixel_size_x = one_pixel_further_x - xwin1
|
||||
|
||||
# Strips = shown_strips(context).
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
coords = [] # type: typing.List[Float2]
|
||||
colors = [] # type: typing.List[Float4]
|
||||
|
||||
# Collect all the lines (vertex coords + vertex colours) to draw.
|
||||
for strip in strips:
|
||||
|
||||
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords.
|
||||
strip_coords = get_strip_rectf(strip)
|
||||
|
||||
# Check if any of the coordinates are out of bounds.
|
||||
if (
|
||||
strip_coords[0] > xwin2
|
||||
or strip_coords[2] < xwin1
|
||||
or strip_coords[1] > ywin2
|
||||
or strip_coords[3] < ywin1
|
||||
):
|
||||
continue
|
||||
|
||||
if strip.rr.is_approved:
|
||||
line_in_strip(
|
||||
strip_coords,
|
||||
pixel_size_x,
|
||||
APPROVED_COLOR,
|
||||
0.05,
|
||||
coords,
|
||||
colors,
|
||||
)
|
||||
elif strip.rr.is_pushed_to_edit:
|
||||
line_in_strip(
|
||||
strip_coords,
|
||||
pixel_size_x,
|
||||
PUSHED_TO_EDIT_COLOR,
|
||||
0.05,
|
||||
coords,
|
||||
colors,
|
||||
)
|
||||
|
||||
line_drawer.draw(coords, colors)
|
||||
|
||||
|
||||
def tag_redraw_all_sequencer_editors():
|
||||
context = bpy.context
|
||||
|
||||
# Py cant access notifiers.
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
# This is a list so it can be changed instead of set
|
||||
# if it is only changed, it does not have to be declared as a global everywhere
|
||||
cb_handle = []
|
||||
|
||||
|
||||
def callback_enable():
|
||||
global cb_handle
|
||||
|
||||
if cb_handle:
|
||||
return
|
||||
|
||||
# Doing GPU stuff in the background crashes Blender, so let's not.
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
line_drawer = LineDrawer()
|
||||
cb_handle[:] = (
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_add(
|
||||
draw_callback_px, (line_drawer,), "WINDOW", "POST_VIEW"
|
||||
),
|
||||
)
|
||||
|
||||
tag_redraw_all_sequencer_editors()
|
||||
|
||||
|
||||
def callback_disable():
|
||||
global cb_handle
|
||||
|
||||
if not cb_handle:
|
||||
return
|
||||
|
||||
try:
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], "WINDOW")
|
||||
except ValueError:
|
||||
# Thrown when already removed.
|
||||
pass
|
||||
cb_handle.clear()
|
||||
|
||||
tag_redraw_all_sequencer_editors()
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def register():
|
||||
callback_enable()
|
||||
|
||||
|
||||
def unregister():
|
||||
callback_disable()
|
||||
@@ -1,8 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
class NoImageStripAvailableException(Exception):
|
||||
"""
|
||||
Error raised when trying to gather image sequence in folder but no files are existent
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,431 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Set, Union, Optional, List, Dict, Any, Tuple
|
||||
|
||||
import bpy
|
||||
|
||||
from . import vars, checksqe, util
|
||||
from .. import prefs, cache
|
||||
from ..sqe import opsdata as sqe_opsdata
|
||||
from .exception import NoImageStripAvailableException
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
copytree_list: List[Path] = []
|
||||
copytree_num_of_items: int = 0
|
||||
copytree_prev_printed_len: int = 0
|
||||
|
||||
|
||||
def copytree_verbose(src: Union[str, Path], dest: Union[str, Path], **kwargs):
|
||||
_copytree_init_progress_update(Path(src))
|
||||
shutil.copytree(src, dest, copy_function=_copy2_tree_progress, **kwargs)
|
||||
_copytree_clear_progress_update()
|
||||
|
||||
|
||||
def _copytree_init_progress_update(source_dir: Path):
|
||||
global copytree_num_of_items
|
||||
file_list = [f for f in source_dir.glob("**/*") if f.is_file()]
|
||||
copytree_num_of_items = len(file_list)
|
||||
|
||||
|
||||
def _copy2_tree_progress(src, dst):
|
||||
"""
|
||||
Function that can be used for copy_function
|
||||
argument on shutil.copytree function.
|
||||
Logs every item that is currently copied.
|
||||
"""
|
||||
global copytree_num_of_items
|
||||
global copytree_list
|
||||
global copytree_prev_printed_len
|
||||
|
||||
term_col_len = shutil.get_terminal_size(fallback=(0, 0))[0]
|
||||
if term_col_len != 0:
|
||||
delete_lines = math.ceil(copytree_prev_printed_len / term_col_len)
|
||||
else:
|
||||
delete_lines = 0
|
||||
|
||||
copytree_list.append(Path(src))
|
||||
progress = round((len(copytree_list) * 100) / copytree_num_of_items)
|
||||
delete_prev_line = "\033[1A\x1b[2K"
|
||||
message = "Copying %s (%i%%)" % (src, progress)
|
||||
logger.warn(delete_lines * delete_prev_line + message)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
copytree_prev_printed_len = len(message)
|
||||
|
||||
|
||||
def _copytree_clear_progress_update():
|
||||
global copytree_num_of_items
|
||||
global copytree_list
|
||||
global copytree_prev_printed_len
|
||||
|
||||
copytree_num_of_items = 0
|
||||
copytree_list.clear()
|
||||
copytree_prev_printed_len = 0
|
||||
|
||||
|
||||
def get_valid_cs_sequences(
|
||||
context: bpy.types.Context, sequence_list: List[bpy.types.Strip] = []
|
||||
) -> List[bpy.types.Strip]:
|
||||
|
||||
sequences: List[bpy.types.Strip] = []
|
||||
|
||||
if sequence_list:
|
||||
sequences = sequence_list
|
||||
else:
|
||||
sequences = context.selected_sequences or context.scene.sequence_editor.sequences_all
|
||||
|
||||
if cache.project_active_get():
|
||||
|
||||
valid_sequences = [
|
||||
s
|
||||
for s in sequences
|
||||
if s.type in ["MOVIE", "IMAGE"] and not s.mute and not s.kitsu.initialized
|
||||
]
|
||||
else:
|
||||
valid_sequences = [s for s in sequences if s.type in ["MOVIE", "IMAGE"] and not s.mute]
|
||||
|
||||
return valid_sequences
|
||||
|
||||
|
||||
def get_frames_root_dir(strip: bpy.types.Strip) -> Path:
|
||||
# sf = shot_frames | fo = farm_output.
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
fo_dir = get_strip_folder(strip)
|
||||
sf_dir = addon_prefs.frames_root_dir / fo_dir.parent.relative_to(fo_dir.parents[3])
|
||||
|
||||
return sf_dir
|
||||
|
||||
|
||||
def get_strip_folder(strip: bpy.types.Strip) -> Path:
|
||||
if hasattr(strip, 'directory'):
|
||||
return Path(strip.directory)
|
||||
else:
|
||||
return Path(strip.filepath).parent
|
||||
|
||||
|
||||
def get_shot_previews_path(strip: bpy.types.Strip) -> Path:
|
||||
# Fo > farm_output.
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
fo_dir = get_strip_folder(strip)
|
||||
shot_previews_dir = addon_prefs.shot_playblast_root_dir / fo_dir.parent.relative_to(
|
||||
fo_dir.parents[3]
|
||||
)
|
||||
|
||||
return shot_previews_dir
|
||||
|
||||
|
||||
def get_shot_dot_task_type(path: Path):
|
||||
return path.parent.name
|
||||
|
||||
|
||||
def get_farm_output_mp4_path(strip: bpy.types.Strip) -> Path:
|
||||
render_dir = get_strip_folder(strip)
|
||||
return get_farm_output_mp4_path_from_folder(render_dir)
|
||||
|
||||
|
||||
def get_farm_output_mp4_path_from_folder(render_dir: str) -> Path:
|
||||
render_dir = Path(render_dir)
|
||||
shot_name = render_dir.parent.name
|
||||
|
||||
# 070_0040_A.lighting-101-136.mp4 #farm always does .lighting not .comp
|
||||
# because flamenco writes in and out frame in filename we need check the first and
|
||||
# last frame in the folder
|
||||
preview_seq = get_best_preview_sequence(render_dir)
|
||||
|
||||
mp4_filename = f"{shot_name}-{int(preview_seq[0].stem)}-{int(preview_seq[-1].stem)}.mp4"
|
||||
|
||||
return render_dir / mp4_filename
|
||||
|
||||
|
||||
def get_best_preview_sequence(dir: Path) -> List[Path]:
|
||||
|
||||
files: List[List[Path]] = gather_files_by_suffix(
|
||||
dir, output=dict, search_suffixes=[".jpg", ".png"]
|
||||
)
|
||||
if not files:
|
||||
raise NoImageStripAvailableException(f"No preview files found in: {dir.as_posix()}")
|
||||
|
||||
# Select the right images sequence.
|
||||
if len(files) == 1:
|
||||
# If only one image sequence available take that.
|
||||
preview_seq = files[list(files.keys())[0]]
|
||||
|
||||
# Both jpg and png available.
|
||||
else:
|
||||
# If same amount of frames take png.
|
||||
if len(files[".jpg"]) == len(files[".png"]):
|
||||
preview_seq = files[".png"]
|
||||
else:
|
||||
# If not, take whichever is longest.
|
||||
preview_seq = [files[".jpg"], files[".png"]].sort(key=lambda x: len(x))[-1]
|
||||
|
||||
return preview_seq
|
||||
|
||||
|
||||
def get_shot_frames_backup_path(strip: bpy.types.Strip) -> Path:
|
||||
fs_dir = get_frames_root_dir(strip)
|
||||
return fs_dir.parent / f"_backup.{fs_dir.name}"
|
||||
|
||||
|
||||
def get_shot_frames_metadata_path(strip: bpy.types.Strip) -> Path:
|
||||
fs_dir = get_frames_root_dir(strip)
|
||||
return fs_dir.parent / "metadata.json"
|
||||
|
||||
|
||||
def get_shot_previews_metadata_path(strip: bpy.types.Strip) -> Path:
|
||||
fs_dir = get_shot_previews_path(strip)
|
||||
return fs_dir / "metadata.json"
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
with open(path.as_posix(), "r") as file:
|
||||
obj = json.load(file)
|
||||
return obj
|
||||
|
||||
|
||||
def save_to_json(obj: Any, path: Path) -> None:
|
||||
with open(path.as_posix(), "w") as file:
|
||||
json.dump(obj, file, indent=4)
|
||||
|
||||
|
||||
def update_sequence_statuses(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Strip]:
|
||||
return update_is_approved(context), update_is_pushed_to_edit(context)
|
||||
|
||||
|
||||
def update_is_approved(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Strip]:
|
||||
sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render]
|
||||
|
||||
approved_strips = []
|
||||
|
||||
for s in sequences:
|
||||
metadata_path = get_shot_frames_metadata_path(s)
|
||||
if not metadata_path.exists():
|
||||
continue
|
||||
json_obj = load_json(metadata_path) # TODO: prevent opening same json multi times
|
||||
|
||||
if Path(json_obj["source_current"]) == get_strip_folder(s):
|
||||
s.rr.is_approved = True
|
||||
approved_strips.append(s)
|
||||
logger.info("Detected approved strip: %s", s.name)
|
||||
else:
|
||||
s.rr.is_approved = False
|
||||
|
||||
return approved_strips
|
||||
|
||||
|
||||
def update_is_pushed_to_edit(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Strip]:
|
||||
sequences = [s for s in context.scene.sequence_editor.sequences_all if s.rr.is_render]
|
||||
|
||||
pushed_strips = []
|
||||
|
||||
for s in sequences:
|
||||
metadata_path = get_shot_previews_metadata_path(s)
|
||||
if not metadata_path.exists():
|
||||
continue
|
||||
|
||||
json_obj = load_json(metadata_path)
|
||||
|
||||
valid_paths = {Path(value).parent for _key, value in json_obj.items()}
|
||||
|
||||
if get_strip_folder(s) in valid_paths:
|
||||
s.rr.is_pushed_to_edit = True
|
||||
pushed_strips.append(s)
|
||||
logger.info("Detected pushed strip: %s", s.name)
|
||||
else:
|
||||
s.rr.is_pushed_to_edit = False
|
||||
|
||||
return pushed_strips
|
||||
|
||||
|
||||
def gather_files_by_suffix(
|
||||
dir: Path, output=str, search_suffixes: List[str] = [".jpg", ".png", ".exr"]
|
||||
) -> Union[str, List, Dict]:
|
||||
"""
|
||||
Gathers files in dir that end with an extension in search_suffixes.
|
||||
Supported values for output: str, list, dict
|
||||
"""
|
||||
|
||||
files: Dict[str, List[Path]] = {}
|
||||
|
||||
# Gather files.
|
||||
for f in dir.iterdir():
|
||||
if not f.is_file():
|
||||
continue
|
||||
|
||||
for suffix in search_suffixes:
|
||||
if f.suffix == suffix:
|
||||
files.setdefault(suffix, [])
|
||||
files[suffix].append(f)
|
||||
|
||||
# Sort.
|
||||
for suffix, file_list in files.items():
|
||||
files[suffix] = sorted(file_list, key=lambda f: f.name)
|
||||
|
||||
# Return.
|
||||
if output == str:
|
||||
return_str = ""
|
||||
for suffix, file_list in files.items():
|
||||
return_str += f" | {suffix}: {len(file_list)}"
|
||||
|
||||
# Replace first occurence, we dont want that at the beginning.
|
||||
return_str = return_str.replace(" | ", "", 1)
|
||||
|
||||
return return_str
|
||||
|
||||
elif output == dict:
|
||||
return files
|
||||
|
||||
elif output == list:
|
||||
output_list = []
|
||||
for suffix, file_list in files.items():
|
||||
output_list.append(file_list)
|
||||
|
||||
return output_list
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Supported output types are: str, dict, list. {str(output)} not implemented yet."
|
||||
)
|
||||
|
||||
|
||||
def gen_frames_found_text(dir: Path, search_suffixes: List[str] = [".jpg", ".png", ".exr"]) -> str:
|
||||
files_dict = gather_files_by_suffix(dir, output=dict, search_suffixes=search_suffixes)
|
||||
|
||||
frames_found_text = "" # frames found text will be used in ui
|
||||
for suffix, file_list in files_dict.items():
|
||||
frames_found_text += f" | {suffix}: {len(file_list)}"
|
||||
|
||||
# Replace first occurence, we dont want that at the beginning.
|
||||
frames_found_text = frames_found_text.replace(
|
||||
" | ",
|
||||
"",
|
||||
1,
|
||||
)
|
||||
return frames_found_text
|
||||
|
||||
|
||||
def is_sequence_dir(dir: Path) -> bool:
|
||||
return dir.parent.name == "shots"
|
||||
|
||||
|
||||
def is_shot_dir(dir: Path) -> bool:
|
||||
return dir.parent.parent.name == "shots"
|
||||
|
||||
|
||||
def get_shot_name_from_dir(dir: Path) -> str:
|
||||
return dir.stem # 060_0010_A.lighting > 060_0010_A
|
||||
|
||||
|
||||
def get_image_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]:
|
||||
image_editor = None
|
||||
|
||||
for area in context.screen.areas:
|
||||
if area.type == "IMAGE_EDITOR":
|
||||
image_editor = area
|
||||
|
||||
return image_editor
|
||||
|
||||
|
||||
def get_sqe_editor(context: bpy.types.Context) -> Optional[bpy.types.Area]:
|
||||
sqe_editor = None
|
||||
|
||||
for area in context.screen.areas:
|
||||
if area.type == "SEQUENCE_EDITOR":
|
||||
sqe_editor = area
|
||||
|
||||
return sqe_editor
|
||||
|
||||
|
||||
def fit_frame_range_to_strips(
|
||||
context: bpy.types.Context, strips: Optional[List[bpy.types.Strip]] = None
|
||||
) -> Tuple[int, int]:
|
||||
def get_sort_tuple(strip: bpy.types.Strip) -> Tuple[int, int]:
|
||||
return (strip.frame_final_start, strip.frame_final_duration)
|
||||
|
||||
if not strips:
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
if not strips:
|
||||
return (0, 0)
|
||||
|
||||
strips = list(strips)
|
||||
strips.sort(key=get_sort_tuple)
|
||||
|
||||
context.scene.frame_start = strips[0].frame_final_start
|
||||
context.scene.frame_end = strips[-1].frame_final_end - 1
|
||||
|
||||
return (context.scene.frame_start, context.scene.frame_end)
|
||||
|
||||
|
||||
def get_top_level_valid_strips_continious(
|
||||
context: bpy.types.Context,
|
||||
) -> List[bpy.types.Strip]:
|
||||
|
||||
sequences_tmp = get_valid_cs_sequences(
|
||||
context, sequence_list=list(context.scene.sequence_editor.sequences_all)
|
||||
)
|
||||
|
||||
sequences_tmp.sort(key=lambda s: (s.channel, s.frame_final_start), reverse=True)
|
||||
sequences: List[bpy.types.Strip] = []
|
||||
|
||||
for strip in sequences_tmp:
|
||||
|
||||
occ_ranges = checksqe.get_occupied_ranges_for_strips(sequences)
|
||||
s_range = range(strip.frame_final_start, strip.frame_final_end + 1)
|
||||
|
||||
if not checksqe.is_range_occupied(s_range, occ_ranges):
|
||||
sequences.append(strip)
|
||||
|
||||
return sequences
|
||||
|
||||
|
||||
def setup_color_management(context: bpy.types.Context) -> None:
|
||||
if context.scene.view_settings.view_transform != 'Standard':
|
||||
context.scene.view_settings.view_transform = 'Standard'
|
||||
logger.info("Set view transform to: Standard")
|
||||
|
||||
|
||||
def is_active_project() -> bool:
|
||||
return bool(cache.project_active_get())
|
||||
|
||||
|
||||
def link_strip_by_name(
|
||||
context: bpy.types.Context,
|
||||
strip: bpy.types.Strip,
|
||||
shot_name: str,
|
||||
sequence_name: str,
|
||||
) -> None:
|
||||
# Get seq and shot.
|
||||
active_project = cache.project_active_get()
|
||||
seq = active_project.get_sequence_by_name(sequence_name)
|
||||
shot = active_project.get_shot_by_name(seq, shot_name)
|
||||
|
||||
if not shot:
|
||||
logger.error("Unable to find shot %s on kitsu", shot_name)
|
||||
return
|
||||
|
||||
sqe_opsdata.link_metadata_strip(context, shot, seq, strip)
|
||||
|
||||
# Log.
|
||||
t = "Linked strip: %s to shot: %s with ID: %s" % (
|
||||
strip.name,
|
||||
shot.name,
|
||||
shot.id,
|
||||
)
|
||||
logger.info(t)
|
||||
util.redraw_ui()
|
||||
@@ -1,86 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Set, Union, Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class RR_isolate_collection_prop(bpy.types.PropertyGroup):
|
||||
mute: bpy.props.BoolProperty()
|
||||
|
||||
|
||||
class RR_property_group_scene(bpy.types.PropertyGroup):
|
||||
""""""
|
||||
|
||||
render_dir: bpy.props.StringProperty(name="Render Directory", subtype="DIR_PATH")
|
||||
isolate_view: bpy.props.CollectionProperty(type=RR_isolate_collection_prop)
|
||||
|
||||
@property
|
||||
def render_dir_path(self):
|
||||
if not self.is_render_dir_valid:
|
||||
return None
|
||||
return Path(bpy.path.abspath(self.render_dir)).absolute()
|
||||
|
||||
@property
|
||||
def is_render_dir_valid(self) -> bool:
|
||||
if not self.render_dir:
|
||||
return False
|
||||
|
||||
if not bpy.data.filepath and self.render_dir.startswith("//"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RR_property_group_sequence(bpy.types.PropertyGroup):
|
||||
"""
|
||||
Property group that will be registered on sequence strips.
|
||||
"""
|
||||
|
||||
is_render: bpy.props.BoolProperty(name="Is Render")
|
||||
is_approved: bpy.props.BoolProperty(name="Is Approved")
|
||||
is_pushed_to_edit: bpy.props.BoolProperty(name="Is Pushed To Edit")
|
||||
frames_found_text: bpy.props.StringProperty(name="Frames Found")
|
||||
shot_name: bpy.props.StringProperty(name="Shot")
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
RR_isolate_collection_prop,
|
||||
RR_property_group_scene,
|
||||
RR_property_group_sequence,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Scene Properties.
|
||||
bpy.types.Scene.rr = bpy.props.PointerProperty(
|
||||
name="Render Review",
|
||||
type=RR_property_group_scene,
|
||||
description="Metadata that is required for render_review",
|
||||
)
|
||||
|
||||
# Strip Properties.
|
||||
bpy.types.Strip.rr = bpy.props.PointerProperty(
|
||||
name="Render Review",
|
||||
type=RR_property_group_sequence,
|
||||
description="Metadata that is required for render_review",
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,166 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Set, Union, Optional, List, Dict, Any
|
||||
|
||||
import bpy
|
||||
|
||||
from .ops import (
|
||||
RR_OT_sqe_create_review_session,
|
||||
RR_OT_setup_review_workspace,
|
||||
RR_OT_sqe_inspect_exr_sequence,
|
||||
RR_OT_sqe_clear_exr_inspect,
|
||||
RR_OT_sqe_approve_render,
|
||||
RR_OT_sqe_update_sequence_statuses,
|
||||
RR_OT_open_path,
|
||||
RR_OT_sqe_push_to_edit,
|
||||
)
|
||||
from . import opsdata
|
||||
from .. import prefs
|
||||
|
||||
|
||||
class RR_PT_render_review(bpy.types.Panel):
|
||||
""" """
|
||||
|
||||
bl_category = "Render Review"
|
||||
bl_label = "Render Review"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 10
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
|
||||
# Label and setup workspace.
|
||||
row = box.row(align=True)
|
||||
row.label(text="Review", icon="CAMERA_DATA")
|
||||
row.operator(RR_OT_setup_review_workspace.bl_idname, text="", icon="WINDOW")
|
||||
|
||||
# Render dir prop.
|
||||
row = box.row(align=True)
|
||||
row.prop(context.scene.rr, "render_dir")
|
||||
|
||||
# Create session.
|
||||
render_dir = context.scene.rr.render_dir_path
|
||||
text = f"Invalid Render Directory"
|
||||
if render_dir:
|
||||
if opsdata.is_sequence_dir(render_dir):
|
||||
text = f"Review Sequence: {render_dir.name}"
|
||||
elif opsdata.is_shot_dir(render_dir):
|
||||
text = f"Review Shot: {render_dir.stem}"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_create_review_session.bl_idname, text=text, icon="PLAY")
|
||||
row = box.row(align = True)
|
||||
row.prop(addon_prefs, 'match_latest_length')
|
||||
row = box.row(align=True)
|
||||
row.prop(addon_prefs, 'use_video')
|
||||
if addon_prefs.use_video:
|
||||
row.prop(addon_prefs, 'use_video_latest_only')
|
||||
row = box.row(align = False)
|
||||
row.prop(addon_prefs, 'shot_name_filter')
|
||||
|
||||
# Warning if kitsu on but not logged in.
|
||||
if not prefs.session_auth(context):
|
||||
row = box.split(align=True, factor=0.7)
|
||||
row.label(text="Kitsu enabled but not logged in", icon="ERROR")
|
||||
row.operator("kitsu.session_start", text="Login")
|
||||
|
||||
elif not opsdata.is_active_project():
|
||||
row = box.row(align=True)
|
||||
row.label(text="Kitsu enabled but no active project", icon="ERROR")
|
||||
|
||||
sqe = context.scene.sequence_editor
|
||||
if not sqe:
|
||||
return
|
||||
active_strip = sqe.active_strip
|
||||
if active_strip and active_strip.rr.is_render:
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text=f"Render: {active_strip.rr.shot_name}", icon="RESTRICT_RENDER_OFF")
|
||||
box.separator()
|
||||
|
||||
# Render dir name label and open file op.
|
||||
row = box.row(align=True)
|
||||
directory = opsdata.get_strip_folder(active_strip)
|
||||
row.label(text=f"Folder: {directory.name}")
|
||||
row.operator(
|
||||
RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="", emboss=False
|
||||
).filepath = bpy.path.abspath(directory.as_posix())
|
||||
|
||||
# Nr of frames.
|
||||
box.row(align=True).label(text=f"Frames: {active_strip.rr.frames_found_text}")
|
||||
|
||||
# Inspect exr.
|
||||
text = "Inspect EXR"
|
||||
icon = "VIEWZOOM"
|
||||
if not opsdata.get_image_editor(context):
|
||||
text = "Inspect EXR: Needs Image Editor"
|
||||
icon = "ERROR"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_inspect_exr_sequence.bl_idname, icon=icon, text=text)
|
||||
row.operator(RR_OT_sqe_clear_exr_inspect.bl_idname, text="", icon="X")
|
||||
|
||||
# Approve render & udpate approved.
|
||||
row = box.row(align=True)
|
||||
|
||||
text = "Push To Edit & Approve Render"
|
||||
if active_strip.rr.is_pushed_to_edit:
|
||||
text = "Approve Render"
|
||||
row.operator(RR_OT_sqe_approve_render.bl_idname, icon="CHECKMARK", text=text)
|
||||
row.operator(RR_OT_sqe_update_sequence_statuses.bl_idname, text="", icon="FILE_REFRESH")
|
||||
|
||||
# Push to edit.
|
||||
if not addon_prefs.shot_playblast_root_dir:
|
||||
shot_previews_dir = "" # ops handle invalid path
|
||||
else:
|
||||
shot_previews_dir = Path(opsdata.get_shot_previews_path(active_strip)).as_posix()
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(RR_OT_sqe_push_to_edit.bl_idname, icon="EXPORT")
|
||||
row.operator(RR_OT_open_path.bl_idname, icon="FILEBROWSER", text="").filepath = (
|
||||
shot_previews_dir
|
||||
)
|
||||
|
||||
# Push strip to Kitsu.
|
||||
box.row().operator('kitsu.sqe_push_shot', icon='URL')
|
||||
|
||||
|
||||
def RR_topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
op = layout.operator(RR_OT_setup_review_workspace.bl_idname, text="Render Review")
|
||||
|
||||
|
||||
# ----------------REGISTER--------------.
|
||||
|
||||
classes = [
|
||||
RR_PT_render_review,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# Append to topbar file new.
|
||||
bpy.types.TOPBAR_MT_file_new.append(RR_topbar_file_new_draw_handler)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
# Remove to topbar file new.
|
||||
bpy.types.TOPBAR_MT_file_new.remove(RR_topbar_file_new_draw_handler)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,28 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
from typing import Union, Dict, List, Any
|
||||
import bpy
|
||||
from . import vars
|
||||
|
||||
|
||||
def redraw_ui() -> None:
|
||||
"""
|
||||
Forces blender to redraw the UI.
|
||||
"""
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def get_version(str_value: str, format: type = str) -> Union[str, int, None]:
|
||||
match = re.search(vars.VERSION_PATTERN, str_value)
|
||||
if match:
|
||||
version = match.group()
|
||||
if format == str:
|
||||
return version
|
||||
if format == int:
|
||||
return int(version.replace("v", ""))
|
||||
return None
|
||||
@@ -1,8 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
RESOLUTION = (2048, 858)
|
||||
VERSION_PATTERN = r"v\d\d\d"
|
||||
FPS = 24
|
||||
DELIMITER = "-"
|
||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from . import ops, ui
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
ui.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
@@ -1,45 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from .. import prefs, bkglobals
|
||||
from ..types import Shot
|
||||
from . import core, config
|
||||
|
||||
|
||||
def get_shot_assets(
|
||||
scene: bpy.types.Scene,
|
||||
output_collection: bpy.types.Collection,
|
||||
shot: Shot,
|
||||
):
|
||||
kitsu_assets = shot.get_all_assets()
|
||||
|
||||
for kitsu_asset in kitsu_assets:
|
||||
asset_path = kitsu_asset.data.get(bkglobals.KITSU_FILEPATH_KEY)
|
||||
collection_name = kitsu_asset.data.get(bkglobals.KITSU_COLLECTION_KEY)
|
||||
if not asset_path or not collection_name:
|
||||
print(
|
||||
f"Asset '{kitsu_asset.name}' is missing filepath or collection metadata. Skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
filepath = prefs.project_root_dir_get(bpy.context).joinpath(asset_path).absolute()
|
||||
if not filepath.exists():
|
||||
print(f"Asset '{kitsu_asset.name}' filepath '{str(filepath)}' does not exist. Skipping")
|
||||
continue
|
||||
|
||||
if config.ASSET_TYPE_TO_OVERRIDE.get(collection_name.split('-')[0]):
|
||||
linked_collection = core.link_and_override_collection(
|
||||
collection_name=collection_name, file_path=str(filepath), scene=scene
|
||||
)
|
||||
core.add_action_to_armature(linked_collection, shot)
|
||||
print(f"'{collection_name}': Successfully Linked & Overridden")
|
||||
else:
|
||||
linked_collection = core.link_data_block(
|
||||
file_path=str(filepath),
|
||||
data_block_name=collection_name,
|
||||
data_block_type="Collection",
|
||||
)
|
||||
print(f"'{collection_name}': Successfully Linked")
|
||||
output_collection.children.link(linked_collection)
|
||||
@@ -1,121 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from .. import bkglobals
|
||||
from . import core
|
||||
import json
|
||||
|
||||
|
||||
OUTPUT_COL_CREATE = {
|
||||
"anim": True,
|
||||
"comp": False,
|
||||
"fx": True,
|
||||
"layout": True,
|
||||
"lighting": False,
|
||||
"previz": True,
|
||||
"rendering": False,
|
||||
"smear_to_mesh": False,
|
||||
"storyboard": True,
|
||||
}
|
||||
|
||||
OUTPUT_COL_LINK_MAPPING = {
|
||||
"anim": None,
|
||||
"comp": ['anim', 'fx'],
|
||||
"fx": ['anim'],
|
||||
"layout": None,
|
||||
"lighting": ['anim'],
|
||||
"previz": None,
|
||||
"rendering": None,
|
||||
"smear_to_mesh": None,
|
||||
"storyboard": None,
|
||||
}
|
||||
|
||||
LOAD_EDITORIAL_REF = {
|
||||
"anim": True,
|
||||
"comp": False,
|
||||
"fx": False,
|
||||
"layout": True,
|
||||
"lighting": False,
|
||||
"previz": False,
|
||||
"rendering": False,
|
||||
"smear_to_mesh": False,
|
||||
"storyboard": False,
|
||||
}
|
||||
|
||||
ASSET_TYPE_TO_OVERRIDE = {
|
||||
"CH": True, # Character
|
||||
"PR": True, # Rigged Prop
|
||||
"LI": True, # Library/Environment Asset
|
||||
"SE": False, # Set
|
||||
"LG": True, # Lighting Rig
|
||||
"CA": True, # Camera Rig
|
||||
}
|
||||
|
||||
|
||||
def settings_filepath_get() -> Path:
|
||||
directory = core.get_shot_builder_config_dir(bpy.context)
|
||||
json_file_path = directory.joinpath(bkglobals.BUILD_SETTINGS_FILENAME)
|
||||
return json_file_path
|
||||
|
||||
|
||||
def settings_dict_get(file_path_str: str = "") -> dict:
|
||||
if file_path_str == "":
|
||||
json_file_path = settings_filepath_get()
|
||||
else:
|
||||
json_file_path = Path(file_path_str)
|
||||
if not json_file_path.exists():
|
||||
return
|
||||
return json.load(open(json_file_path))
|
||||
|
||||
|
||||
def settings_load(json_file_path: str = ""):
|
||||
global OUTPUT_COL_CREATE
|
||||
global OUTPUT_COL_LINK_MAPPING
|
||||
global LOAD_EDITORIAL_REF
|
||||
global ASSET_TYPE_TO_OVERRIDE
|
||||
|
||||
json_content = settings_dict_get(json_file_path)
|
||||
|
||||
if not json_content:
|
||||
return
|
||||
try:
|
||||
OUTPUT_COL_CREATE = json_content["OUTPUT_COL_CREATE"]
|
||||
OUTPUT_COL_LINK_MAPPING = json_content["OUTPUT_COL_LINK_MAPPING"]
|
||||
LOAD_EDITORIAL_REF = json_content["LOAD_EDITORIAL_REF"]
|
||||
ASSET_TYPE_TO_OVERRIDE = json_content["ASSET_TYPE_TO_OVERRIDE"]
|
||||
return True
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
|
||||
def filepath_get(filename: str = "") -> Path:
|
||||
config_dir = core.get_shot_builder_config_dir(bpy.context)
|
||||
if not config_dir.exists():
|
||||
config_dir.mkdir(parents=True)
|
||||
return config_dir.joinpath(filename)
|
||||
|
||||
|
||||
def example_filepath_get(filename: str = "") -> Path:
|
||||
config_dir = Path(__file__).parent
|
||||
return config_dir.joinpath(f"config_examples/{filename}")
|
||||
|
||||
|
||||
def copy_json_file(source_file: Path, target_file: Path) -> None:
|
||||
# Read contents
|
||||
with source_file.open() as source:
|
||||
contents = source.read()
|
||||
|
||||
# Write contents to target file
|
||||
with target_file.open('w') as target:
|
||||
target.write(contents)
|
||||
|
||||
|
||||
def template_example_dir_get() -> Path:
|
||||
return Path(__file__).parent.joinpath(f"templates")
|
||||
|
||||
|
||||
def template_dir_get() -> Path:
|
||||
return core.get_shot_builder_config_dir(bpy.context).joinpath("templates")
|
||||
@@ -1,53 +0,0 @@
|
||||
import bpy
|
||||
|
||||
# The import paths here are relative to the "blender_kitsu/shot_builder/hooks.py" file.
|
||||
# The example imports here imports the "blender_kitsu/shot_builder/hooks.py" and "blender_kistsu/types.py" file.
|
||||
from .hooks import hook
|
||||
from ..types import Shot, Asset
|
||||
|
||||
import logging
|
||||
|
||||
'''
|
||||
Arguments to use in hooks
|
||||
scene: bpy.types.Scene # current scene
|
||||
shot: Shot class from blender_kitsu.types.py
|
||||
prod_path: str # path to production root dir (your_project/svn/)
|
||||
shot_path: str # path to shot file (your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_task_name}.blend})
|
||||
|
||||
Notes
|
||||
matching_task_type = ['anim', 'lighting', 'fx', 'comp'] # either use list or just one string
|
||||
output_col_name = shot.get_output_collection_name(task_type_short_name="anim")
|
||||
|
||||
|
||||
|
||||
|
||||
'''
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------- Global Hook ----------
|
||||
|
||||
|
||||
@hook()
|
||||
def set_eevee_render_engine(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
By default, we set EEVEE as the renderer.
|
||||
"""
|
||||
scene.render.engine = 'BLENDER_EEVEE'
|
||||
print("HOOK SET RENDER ENGINE")
|
||||
|
||||
|
||||
# ---------- Overrides for animation files ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def test_args(
|
||||
scene: bpy.types.Scene, shot: Shot, prod_path: str, shot_path: str, **kwargs
|
||||
):
|
||||
"""
|
||||
Set output parameters for animation rendering
|
||||
"""
|
||||
print(f"Scene = {scene.name}")
|
||||
print(f"Shot = {shot.name}")
|
||||
print(f"Prod Path = {prod_path}")
|
||||
print(f"Shot Path = {shot_path}")
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"OUTPUT_COL_CREATE": {
|
||||
"anim": true,
|
||||
"comp": false,
|
||||
"fx": true,
|
||||
"layout": true,
|
||||
"lighting": false,
|
||||
"previz": true,
|
||||
"rendering": false,
|
||||
"smear_to_mesh": false,
|
||||
"storyboard": true
|
||||
},
|
||||
"OUTPUT_COL_LINK_MAPPING": {
|
||||
"anim": null,
|
||||
"comp": [
|
||||
"anim",
|
||||
"fx"
|
||||
],
|
||||
"fx": [
|
||||
"anim"
|
||||
],
|
||||
"layout": null,
|
||||
"lighting": [
|
||||
"anim"
|
||||
],
|
||||
"previz": null,
|
||||
"rendering": null,
|
||||
"smear_to_mesh": null,
|
||||
"storyboard": null
|
||||
},
|
||||
"LOAD_EDITORIAL_REF": {
|
||||
"anim": true,
|
||||
"comp": false,
|
||||
"fx": false,
|
||||
"layout": true,
|
||||
"lighting": false,
|
||||
"previz": false,
|
||||
"rendering": false,
|
||||
"smear_to_mesh": false,
|
||||
"storyboard": false
|
||||
},
|
||||
"ASSET_TYPE_TO_OVERRIDE": {
|
||||
"CH": true,
|
||||
"PR": true,
|
||||
"LI": true,
|
||||
"SE": false,
|
||||
"LG": true,
|
||||
"CA": true
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
from .. import bkglobals
|
||||
from ..types import (
|
||||
Sequence,
|
||||
Shot,
|
||||
TaskType,
|
||||
)
|
||||
|
||||
from ..cache import Project
|
||||
from . import config
|
||||
from .. import prefs
|
||||
from ..anim import opsdata as anim_opsdata
|
||||
|
||||
#################
|
||||
# Constants
|
||||
#################
|
||||
CAMERA_NAME = 'CAM-camera'
|
||||
|
||||
|
||||
def get_shot_builder_config_dir(context: bpy.types.Context) -> Path:
|
||||
"""Returns directory Shot Builder Hooks are stored in
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): Blender Context
|
||||
|
||||
Returns:
|
||||
Path: Path object to Shot Builder Hooks Directory
|
||||
"""
|
||||
root_dir = prefs.project_root_dir_get(context)
|
||||
return root_dir.joinpath("pro/config/shot_builder")
|
||||
|
||||
|
||||
def get_file_dir(seq: Sequence, shot: Shot, task_type: TaskType) -> Path:
|
||||
"""Returns Path to Directory for Current Shot, will ensure that
|
||||
file path exists if it does not.
|
||||
|
||||
Args:
|
||||
seq (Sequence): Sequence Class from blender_kitsu.types
|
||||
shot (Shot): Shot Class from blender_kitsu.types
|
||||
task_type TaskType Class from blender_kitsu.types
|
||||
|
||||
Returns:
|
||||
Path: Returns Path for Shot Directory
|
||||
"""
|
||||
project_root_dir = prefs.project_root_dir_get(bpy.context)
|
||||
all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots')
|
||||
shot_dir = all_shots_dir.joinpath(seq.name).joinpath(shot.name)
|
||||
if not shot_dir.exists():
|
||||
shot_dir.mkdir(parents=True)
|
||||
return shot_dir
|
||||
|
||||
|
||||
def set_render_engine(scene: bpy.types.Scene, engine='CYCLES'):
|
||||
scene.render.engine = engine
|
||||
|
||||
|
||||
def remove_all_data():
|
||||
for lib in bpy.data.libraries:
|
||||
bpy.data.libraries.remove(lib)
|
||||
|
||||
for col in bpy.data.collections:
|
||||
bpy.data.collections.remove(col)
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
for action in bpy.data.actions:
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
|
||||
def set_shot_scene(context: bpy.types.Context, scene_name: str) -> bpy.types.Scene:
|
||||
print(f"create scene with name {scene_name}")
|
||||
for scene in bpy.data.scenes:
|
||||
scene.name = 'REMOVE-' + scene.name
|
||||
|
||||
keep_scene = bpy.data.scenes.new(name=scene_name)
|
||||
for scene in bpy.data.scenes:
|
||||
if scene.name == scene_name:
|
||||
continue
|
||||
print(f"remove scene {scene.name}")
|
||||
bpy.data.scenes.remove(scene)
|
||||
|
||||
context.window.scene = keep_scene
|
||||
return keep_scene
|
||||
|
||||
|
||||
def set_resolution_and_fps(project: Project, scene: bpy.types.Scene):
|
||||
scene.render.fps = int(project.fps) # set fps
|
||||
resolution = project.resolution.split('x')
|
||||
scene.render.resolution_x = int(resolution[0])
|
||||
scene.render.resolution_y = int(resolution[1])
|
||||
scene.render.resolution_percentage = 100
|
||||
|
||||
|
||||
def set_frame_range(shot: Shot, scene: bpy.types.Scene):
|
||||
if not shot.nb_frames:
|
||||
return
|
||||
kitsu_start_3d = shot.get_3d_start()
|
||||
scene.frame_start = kitsu_start_3d
|
||||
scene.frame_end = kitsu_start_3d + shot.nb_frames - 1
|
||||
scene.frame_current = kitsu_start_3d
|
||||
|
||||
|
||||
def link_data_block(file_path: str, data_block_name: str, data_block_type: str):
|
||||
bpy.ops.wm.link(
|
||||
filepath=file_path,
|
||||
directory=file_path + "/" + data_block_type,
|
||||
filename=data_block_name,
|
||||
instance_collections=False,
|
||||
)
|
||||
# TODO This doesn't return anything but collections
|
||||
return bpy.data.collections.get(data_block_name)
|
||||
|
||||
|
||||
def link_and_override_collection(
|
||||
file_path: str, collection_name: str, scene: bpy.types.Scene
|
||||
) -> bpy.types.Collection:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
file_path (str): File Path to .blend file to link from
|
||||
collection_name (str): Name of collection to link from given filepath
|
||||
scene (bpy.types.Scene): Current Scene to link collection to
|
||||
|
||||
Returns:
|
||||
bpy.types.Collection: Overriden Collection linked to Scene Collection
|
||||
"""
|
||||
collection = link_data_block(file_path, collection_name, "Collection")
|
||||
override_collection = collection.override_hierarchy_create(
|
||||
scene, bpy.context.view_layer, do_fully_editable=True
|
||||
)
|
||||
scene.collection.children.unlink(collection)
|
||||
# Make library override.
|
||||
return override_collection
|
||||
|
||||
|
||||
def link_camera_rig(
|
||||
scene: bpy.types.Scene,
|
||||
output_collection: bpy.types.Collection,
|
||||
):
|
||||
"""
|
||||
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.
|
||||
project_path = prefs.project_root_dir_get(bpy.context)
|
||||
path = f"{project_path}/pro/assets/cam/camera_rig.blend"
|
||||
|
||||
if not Path(path).exists():
|
||||
camera_data = bpy.data.cameras.new(name=CAMERA_NAME)
|
||||
camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data)
|
||||
scene.collection.objects.link(camera_object)
|
||||
output_collection.objects.link(camera_object)
|
||||
return
|
||||
|
||||
collection_name = "CA-camera_rig" # TODO Rename the asset itself, this breaks convention
|
||||
|
||||
override_camera_col = link_and_override_collection(
|
||||
file_path=path, collection_name=collection_name, scene=scene
|
||||
)
|
||||
output_collection.children.link(override_camera_col)
|
||||
|
||||
# Set the camera of the camera rig as active scene camera.
|
||||
camera = override_camera_col.objects.get(CAMERA_NAME)
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
def create_task_type_output_collection(
|
||||
scene: bpy.types.Scene, shot: Shot, task_type: TaskType
|
||||
) -> bpy.types.Collection:
|
||||
collections = bpy.data.collections
|
||||
output_col_name = shot.get_output_collection_name(task_type.get_short_name())
|
||||
|
||||
if not collections.get(output_col_name):
|
||||
bpy.data.collections.new(name=output_col_name)
|
||||
output_collection = collections.get(output_col_name)
|
||||
|
||||
output_collection.use_fake_user = True
|
||||
if not scene.collection.children.get(output_col_name):
|
||||
scene.collection.children.link(output_collection)
|
||||
|
||||
for view_layer in scene.view_layers:
|
||||
view_layer_output_collection = view_layer.layer_collection.children.get(output_col_name)
|
||||
view_layer_output_collection.exclude = True
|
||||
return output_collection
|
||||
|
||||
|
||||
def link_task_type_output_collections(shot: Shot, task_type: TaskType):
|
||||
task_type_short_name = task_type.get_short_name()
|
||||
if config.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name) == None:
|
||||
return
|
||||
for short_name in config.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name):
|
||||
external_filepath = shot.get_filepath(bpy.context, short_name)
|
||||
if not Path(external_filepath).exists():
|
||||
print(f"Unable to link output collection for {Path(external_filepath).name}")
|
||||
continue
|
||||
file_path = external_filepath.__str__()
|
||||
colection_name = shot.get_output_collection_name(short_name)
|
||||
link_data_block(file_path, colection_name, 'Collection')
|
||||
|
||||
|
||||
def add_action_to_armature(collection: bpy.types.Collection, shot_active: Shot):
|
||||
for obj in collection.all_objects:
|
||||
# Skip Armatures that are hidden from viewport because they aren't intended to be animated
|
||||
if obj.type == 'ARMATURE' and not obj.hide_viewport:
|
||||
if not obj.animation_data:
|
||||
obj.animation_data_create()
|
||||
name = anim_opsdata.gen_action_name(obj, collection, shot_active)
|
||||
if obj.animation_data.action and obj.animation_data.action.name == name:
|
||||
continue
|
||||
obj.animation_data.action = bpy.data.actions.new(name=name)
|
||||
@@ -1,22 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import functools
|
||||
|
||||
|
||||
def save_shot_builder_file(file_path: str) -> bool:
|
||||
"""Save Shot File within Folder of matching name.
|
||||
Set Shot File to relative Paths."""
|
||||
if Path(file_path).exists():
|
||||
print(f"Shot Builder cannot overwrite existing file '{file_path}'")
|
||||
return False
|
||||
dir_path = Path(file_path).parent
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
bpy.app.timers.register(
|
||||
functools.partial(bpy.ops.wm.save_mainfile, filepath=file_path, relative_remap=True),
|
||||
first_interval=0.1,
|
||||
)
|
||||
return True
|
||||
@@ -1,162 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import *
|
||||
|
||||
import typing
|
||||
import types
|
||||
from collections.abc import Iterable
|
||||
import importlib.util
|
||||
from .. import prefs
|
||||
import logging
|
||||
from .core import get_shot_builder_config_dir
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Wildcard:
|
||||
pass
|
||||
|
||||
|
||||
class DoNotMatch:
|
||||
pass
|
||||
|
||||
|
||||
MatchCriteriaType = typing.Union[
|
||||
str, typing.List[str], typing.Type[Wildcard], typing.Type[DoNotMatch]
|
||||
]
|
||||
"""
|
||||
The MatchCriteriaType is a type definition for the parameters of the `hook` decorator.
|
||||
|
||||
The matching parameters can use multiple types to detect how the matching criteria
|
||||
would work.
|
||||
|
||||
* `str`: would perform an exact string match.
|
||||
* `typing.Iterator[str]`: would perform an exact string match with any of the given strings.
|
||||
* `typing.Type[Wildcard]`: would match any type for this parameter. This would be used so a hook
|
||||
is called for any value.
|
||||
* `typing.Type[DoNotMatch]`: would ignore this hook when matching the hook parameter. This is the default
|
||||
value for the matching criteria and would normally not be set directly in a
|
||||
production configuration.
|
||||
"""
|
||||
|
||||
MatchingRulesType = typing.Dict[str, MatchCriteriaType]
|
||||
"""
|
||||
Hooks are stored as `_shot_builder_rules' attribute on the function.
|
||||
The MatchingRulesType is the type definition of the `_shot_builder_rules` attribute.
|
||||
"""
|
||||
|
||||
HookFunction = typing.Callable[[typing.Any], None]
|
||||
|
||||
|
||||
def _match_hook_parameter(
|
||||
hook_criteria: MatchCriteriaType, match_query: typing.Optional[str]
|
||||
) -> bool:
|
||||
if hook_criteria == None:
|
||||
return True
|
||||
if hook_criteria == DoNotMatch:
|
||||
return match_query is None
|
||||
if hook_criteria == Wildcard:
|
||||
return True
|
||||
if isinstance(hook_criteria, str):
|
||||
return match_query == hook_criteria
|
||||
if isinstance(hook_criteria, list):
|
||||
return match_query in hook_criteria
|
||||
logger.error(f"Incorrect matching criteria {hook_criteria}, {match_query}")
|
||||
return False
|
||||
|
||||
|
||||
class Hooks:
|
||||
def __init__(self):
|
||||
self._hooks: typing.List[HookFunction] = []
|
||||
|
||||
def matches(
|
||||
self,
|
||||
hook: HookFunction,
|
||||
match_task_type: typing.Optional[str] = None,
|
||||
match_asset_type: typing.Optional[str] = None,
|
||||
**kwargs: typing.Optional[str],
|
||||
) -> bool:
|
||||
assert not kwargs
|
||||
rules = typing.cast(MatchingRulesType, getattr(hook, '_shot_builder_rules'))
|
||||
return all(
|
||||
(
|
||||
_match_hook_parameter(rules['match_task_type'], match_task_type),
|
||||
_match_hook_parameter(rules['match_asset_type'], match_asset_type),
|
||||
)
|
||||
)
|
||||
|
||||
def filter(self, **kwargs: typing.Optional[str]) -> typing.Iterator[HookFunction]:
|
||||
for hook in self._hooks:
|
||||
if self.matches(hook=hook, **kwargs):
|
||||
yield hook
|
||||
|
||||
def execute_hooks(
|
||||
self, match_task_type: str = None, match_asset_type: str = None, *args, **kwargs
|
||||
) -> None:
|
||||
for hook in self._hooks:
|
||||
if self.matches(
|
||||
hook, match_task_type=match_task_type, match_asset_type=match_asset_type
|
||||
):
|
||||
hook(*args, **kwargs)
|
||||
|
||||
def load_hooks(self, context):
|
||||
shot_builder_config_dir = get_shot_builder_config_dir(context)
|
||||
if not shot_builder_config_dir.exists():
|
||||
print("Shot Builder Hooks directory does not exist. See add-on preferences")
|
||||
return
|
||||
hook_file_path = shot_builder_config_dir.resolve() / "hooks.py"
|
||||
module_name = __package__ + ".production_hooks"
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, hook_file_path)
|
||||
hooks_module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = hooks_module
|
||||
spec.loader.exec_module(hooks_module)
|
||||
|
||||
self.register_hooks(hooks_module)
|
||||
except FileNotFoundError:
|
||||
raise Exception("Production has no `hooks.py` configuration file")
|
||||
|
||||
return False
|
||||
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def register_hooks(self, module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
self.register(module_item)
|
||||
|
||||
|
||||
def hook(
|
||||
match_task_type: MatchCriteriaType = None,
|
||||
match_asset_type: MatchCriteriaType = None,
|
||||
) -> typing.Callable[[types.FunctionType], types.FunctionType]:
|
||||
"""
|
||||
Decorator to add custom logic when building a shot.
|
||||
|
||||
Hooks are used to extend the configuration that would be not part of the core logic of the shot builder tool.
|
||||
"""
|
||||
rules = {
|
||||
'match_task_type': match_task_type,
|
||||
'match_asset_type': match_asset_type,
|
||||
}
|
||||
|
||||
def wrapper(func: types.FunctionType) -> types.FunctionType:
|
||||
setattr(func, '_shot_builder_rules', rules)
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
@@ -1,632 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import shutil
|
||||
from .. import bkglobals, prefs, cache
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Tuple, Set, cast
|
||||
from . import core, config
|
||||
from ..context import core as context_core
|
||||
|
||||
from ..edit.core import edit_export_import_latest
|
||||
from .file_save import save_shot_builder_file
|
||||
from .template import replace_workspace_with_template
|
||||
from .assets import get_shot_assets
|
||||
from .hooks import Hooks
|
||||
|
||||
ACTIVE_PROJECT = None
|
||||
|
||||
|
||||
def get_shots_for_seq(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
if self.seq_id != '':
|
||||
seq = ACTIVE_PROJECT.get_sequence(self.seq_id)
|
||||
shot_enum = cache.get_shots_enum_for_seq(self, context, seq)
|
||||
if shot_enum != []:
|
||||
return shot_enum
|
||||
return [('NONE', "No Shots Found", '')]
|
||||
|
||||
|
||||
def get_tasks_for_shot(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
global ACTIVE_PROJECT
|
||||
if not (self.shot_id == '' or self.shot_id == 'NONE'):
|
||||
shot = ACTIVE_PROJECT.get_shot(self.shot_id)
|
||||
task_enum = cache.get_shot_task_types_enum_for_shot(self, context, shot)
|
||||
if task_enum != []:
|
||||
return task_enum
|
||||
return [('NONE', "No Tasks Found", '')]
|
||||
|
||||
|
||||
class KITSU_OT_build_config_base_class(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
if not prefs.session_auth(context):
|
||||
cls.poll_message_set("Login to a Kitsu Server")
|
||||
if not cache.project_active_get():
|
||||
cls.poll_message_set("Select an active project")
|
||||
return False
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
cls.poll_message_set(
|
||||
"Check project root directory is configured in 'Blender Kitsu' addon preferences."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class KITSU_OT_build_config_save_hooks(KITSU_OT_build_config_base_class):
|
||||
bl_idname = "kitsu.build_config_save_hooks"
|
||||
bl_label = "Save Shot Builder Hook File"
|
||||
bl_description = "Save hook.py file to `your_project_name/svn/pro/config/shot_builder` directory. Hooks are used to customize shot builder behaviour."
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
hooks_target_filepath = config.filepath_get(bkglobals.BUILD_HOOKS_FILENAME)
|
||||
if hooks_target_filepath.exists():
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
f"{hooks_target_filepath.name} already exists, cannot overwrite",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
hooks_example = config.example_filepath_get(bkglobals.BUILD_HOOKS_FILENAME)
|
||||
if not hooks_example.exists():
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
f"Cannot find {hooks_target_filepath.name} example file",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
config.copy_json_file(hooks_example, hooks_target_filepath)
|
||||
self.report({'INFO'}, f"Hook File saved to {str(hooks_target_filepath)}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_build_config_save_settings(KITSU_OT_build_config_base_class):
|
||||
bl_idname = "kitsu.build_config_save_settings"
|
||||
bl_label = "Save Shot Builder Settings File"
|
||||
bl_description = "Save settings.json file to `your_project_name/svn/pro/config/shot_builder` Config are used to customize shot builder behaviour."
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
settings_target_filepath = config.filepath_get(bkglobals.BUILD_SETTINGS_FILENAME)
|
||||
if settings_target_filepath.exists():
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
f"{settings_target_filepath.name} already exists, cannot overwrite",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
settings_example = config.example_filepath_get(bkglobals.BUILD_SETTINGS_FILENAME)
|
||||
if not settings_example.exists():
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Cannot find example settings file",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
config.copy_json_file(settings_example, settings_target_filepath)
|
||||
self.report({'INFO'}, f"Settings File saved to {str(settings_target_filepath)}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_build_config_save_templates(KITSU_OT_build_config_base_class):
|
||||
bl_idname = "kitsu.build_config_save_templates"
|
||||
bl_label = "Create Shot Builder Template Files"
|
||||
bl_description = (
|
||||
"Save template files for shot builder in config directory."
|
||||
"Templates are used to customize the workspaces for each per task type"
|
||||
"Template names match each task type found on Kitsu Server"
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
source_dir = config.template_example_dir_get()
|
||||
target_dir = config.template_dir_get()
|
||||
|
||||
# Ensure Target Directory Exists
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_files = list(source_dir.glob('*.blend')) + list(source_dir.glob('*.md'))
|
||||
for source_file in source_files:
|
||||
target_file = target_dir.joinpath(source_file.name)
|
||||
if target_file.exists():
|
||||
print(f"Cannot overwrite file {str(target_file)}")
|
||||
continue
|
||||
shutil.copy2(source_file, target_dir.joinpath(source_file.name))
|
||||
|
||||
self.report({'INFO'}, f"Saved template files to {str(target_dir)}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_build_new_file_baseclass(bpy.types.Operator):
|
||||
bl_idname = "kitsu.build_new_file"
|
||||
bl_label = "Build New File"
|
||||
bl_description = "Build a new file based on the current context and save it"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
_kitsu_context_type = "" # Default context for this operator
|
||||
_current_kitsu_context = ""
|
||||
|
||||
production_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Production",
|
||||
description="Name of the production to create a shot file for",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
save_file: bpy.props.BoolProperty( # type:ignore
|
||||
name="Save after building.",
|
||||
description="Automatically save build file after 'Shot Builder' is complete.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
global ACTIVE_PROJECT
|
||||
|
||||
# Temporarily change kitsu context to asset
|
||||
self._current_kitsu_context = context.scene.kitsu.category
|
||||
context.scene.kitsu.category = self._kitsu_context_type
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
project = cache.project_active_get()
|
||||
ACTIVE_PROJECT = project
|
||||
|
||||
if addon_prefs.session.is_auth() is False:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if project.id == "":
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the project root directory. \nCheck project root directory is configured in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.production_name = project.name
|
||||
|
||||
return cast(Set[str], context.window_manager.invoke_props_dialog(self, width=400))
|
||||
|
||||
def cancel(self, context: bpy.types.Context):
|
||||
# Restore kitsu context if cancelled
|
||||
context.scene.kitsu.category = self._current_kitsu_context
|
||||
|
||||
|
||||
class KITSU_OT_build_new_asset(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.build_new_asset"
|
||||
bl_label = "Build New Asset"
|
||||
bl_description = "Build a New Asset file, based on project data from Kitsu Server"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
_kitsu_context_type = "ASSET" # Default context for this operator
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
|
||||
context_core.draw_asset_type_selector(context, col)
|
||||
context_core.draw_asset_selector(context, col)
|
||||
col.prop(self, "save_file")
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
# Get Properties
|
||||
scene = context.scene
|
||||
asset_type = cache.asset_type_active_get()
|
||||
asset = cache.asset_active_get()
|
||||
|
||||
if asset_type.id == "" or asset.id == "":
|
||||
self.report({'ERROR'}, "Please select a asset type and asset to build a shot file")
|
||||
return {'CANCELLED'}
|
||||
|
||||
asset_file_path_str = asset.get_filepath(context)
|
||||
|
||||
replace_workspace_with_template(context, "Asset")
|
||||
|
||||
# Remove All Collections from Scene
|
||||
for collection in context.scene.collection.children:
|
||||
context.scene.collection.children.unlink(collection)
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
# Remove All Objects from Scene
|
||||
for object in context.scene.objects:
|
||||
context.scene.objects.unlink(object)
|
||||
bpy.data.objects.remove(object)
|
||||
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
asset_collection = bpy.data.collections.new(asset.get_collection_name())
|
||||
context.scene.collection.children.link(asset_collection)
|
||||
|
||||
# Set Kitsu Context
|
||||
scene.kitsu.category = "ASSET"
|
||||
scene.kitsu.asset_type_active_name = asset_type.name
|
||||
scene.kitsu.asset_active_name = asset.name
|
||||
scene.kitsu.asset_col = asset_collection
|
||||
|
||||
relative_path = Path(asset_file_path_str).relative_to(prefs.project_root_dir_get(context))
|
||||
asset.set_asset_path(str(relative_path), asset_collection.name)
|
||||
|
||||
# Save File
|
||||
if self.save_file:
|
||||
if not save_shot_builder_file(file_path=asset_file_path_str):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Failed to save file at path `{asset_file_path_str}`",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({"INFO"}, f"Successfully Built Shot:`{asset.name}`")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_open_asset_file(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.open_asset_file"
|
||||
bl_label = "Open Asset File"
|
||||
bl_description = "Open an Asset File from the current project"
|
||||
|
||||
_kitsu_context_type = "ASSET"
|
||||
|
||||
save_current: bpy.props.BoolProperty( # type: ignore
|
||||
name="Save Current File",
|
||||
description="Automatically save the current file before opening a new one.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
context_core.draw_asset_type_selector(context, col)
|
||||
context_core.draw_asset_selector(context, col)
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
col.prop(self, "save_current")
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
asset = cache.asset_active_get()
|
||||
asset_file_path_str = asset.get_filepath(context)
|
||||
if not Path(asset_file_path_str).exists():
|
||||
self.report({'ERROR'}, f"Asset file does not exist: {asset_file_path_str}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if bpy.data.is_dirty and self.save_current:
|
||||
bpy.ops.wm.save_mainfile()
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=asset_file_path_str)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_build_new_shot(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.build_new_shot"
|
||||
bl_label = "Build New Shot"
|
||||
bl_description = "Build a New Shot file, based on project information found on Kitsu Server"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
_kitsu_context_type = "SHOT"
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
context_core.draw_episode_selector(context, col)
|
||||
context_core.draw_sequence_selector(context, col)
|
||||
context_core.draw_shot_selector(context, col)
|
||||
context_core.draw_task_type_selector(context, col)
|
||||
col.prop(self, "save_file")
|
||||
|
||||
def _get_task_type_for_shot(self, context, shot):
|
||||
for task_type in shot.get_all_task_types():
|
||||
if task_type.id == self.task_type:
|
||||
return task_type
|
||||
|
||||
def _frame_range_invalid(self, context, shot) -> bool:
|
||||
if not (getattr(shot, "data") or getattr(shot, "nb_frames")):
|
||||
return True
|
||||
|
||||
try:
|
||||
shot.data.get("frame_in")
|
||||
except AttributeError:
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
# Get Properties
|
||||
active_project = cache.project_active_get()
|
||||
seq = cache.sequence_active_get()
|
||||
shot = cache.shot_active_get()
|
||||
task_type = cache.task_type_active_get()
|
||||
config.settings_load()
|
||||
|
||||
if seq.id == "" or shot.id == "" or task_type.id == "":
|
||||
self.report(
|
||||
{'ERROR'}, "Please select a sequence, shot and task type to build a shot file"
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self._frame_range_invalid(context, shot):
|
||||
self.report(
|
||||
{'WARNING'}, F"Shot {shot.name} is missing frame range data on Kitsu Server"
|
||||
)
|
||||
|
||||
task_type_short_name = task_type.get_short_name()
|
||||
shot_file_path_str = shot.get_filepath(context, task_type_short_name)
|
||||
|
||||
# Open Template File
|
||||
replace_workspace_with_template(context, task_type.name)
|
||||
|
||||
# Set Up Scene + Naming
|
||||
shot_task_name = shot.get_task_name(task_type.get_short_name())
|
||||
scene = core.set_shot_scene(context, shot_task_name)
|
||||
core.remove_all_data()
|
||||
core.set_resolution_and_fps(active_project, scene)
|
||||
core.set_frame_range(shot, scene)
|
||||
|
||||
# Set Render Settings
|
||||
if task_type_short_name == 'anim': # TODO get anim from a constant instead
|
||||
core.set_render_engine(context.scene, 'BLENDER_WORKBENCH')
|
||||
else:
|
||||
core.set_render_engine(context.scene)
|
||||
|
||||
# Create Output Collection & Link Camera
|
||||
if config.OUTPUT_COL_CREATE.get(task_type_short_name):
|
||||
output_col = core.create_task_type_output_collection(context.scene, shot, task_type)
|
||||
if task_type_short_name == 'anim' or task_type_short_name == 'layout':
|
||||
core.link_camera_rig(context.scene, output_col)
|
||||
|
||||
# Load Assets
|
||||
get_shot_assets(scene=scene, output_collection=output_col, shot=shot)
|
||||
|
||||
# Link External Output Collections
|
||||
core.link_task_type_output_collections(shot, task_type)
|
||||
|
||||
if config.LOAD_EDITORIAL_REF.get(task_type_short_name):
|
||||
edit_export_import_latest(context, shot)
|
||||
|
||||
# Run Hooks
|
||||
hooks_instance = Hooks()
|
||||
hooks_instance.load_hooks(context)
|
||||
hooks_instance.execute_hooks(
|
||||
match_task_type=task_type_short_name,
|
||||
scene=context.scene,
|
||||
shot=shot,
|
||||
prod_path=prefs.project_root_dir_get(context),
|
||||
shot_path=shot_file_path_str,
|
||||
)
|
||||
|
||||
# Set Kitsu Context
|
||||
scene.kitsu.category = "SHOT"
|
||||
scene.kitsu.sequence_active_name = seq.name
|
||||
scene.kitsu.shot_active_name = shot.name
|
||||
scene.kitsu.task_type_active_name = task_type.name
|
||||
|
||||
# Save File
|
||||
if self.save_file:
|
||||
if not save_shot_builder_file(file_path=shot_file_path_str):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Failed to save file at path `{shot_file_path_str}`",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({"INFO"}, f"Successfully Built Shot:`{shot.name}` Task: `{task_type.name}`")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class KITSU_OT_open_shot_file(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.open_shot_file"
|
||||
bl_label = "Open Shot File"
|
||||
bl_description = "Open a Shot File from the current project"
|
||||
|
||||
_kitsu_context_type = "SHOT"
|
||||
|
||||
save_current: bpy.props.BoolProperty( # type: ignore
|
||||
name="Save Current File",
|
||||
description="Automatically save the current file before opening a new one.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
context_core.draw_episode_selector(context, col)
|
||||
context_core.draw_sequence_selector(context, col)
|
||||
context_core.draw_shot_selector(context, col)
|
||||
context_core.draw_task_type_selector(context, col)
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
col.prop(self, "save_current")
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
shot = cache.shot_active_get()
|
||||
task_type = cache.task_type_active_get()
|
||||
|
||||
task_type_short_name = task_type.get_short_name()
|
||||
shot_file_path_str = shot.get_filepath(context, task_type_short_name)
|
||||
if not Path(shot_file_path_str).exists():
|
||||
self.report({'ERROR'}, f"Shot file does not exist: {shot_file_path_str}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if bpy.data.is_dirty and self.save_current:
|
||||
bpy.ops.wm.save_mainfile()
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=shot_file_path_str)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_create_edit_file(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.create_edit_file"
|
||||
bl_label = "Create Edit File"
|
||||
bl_description = "Create a new .blend file for editing using Blender's Video Editing template"
|
||||
|
||||
_edit_entity = None
|
||||
_production_name = None
|
||||
_kitsu_context_type = "EDIT"
|
||||
|
||||
create_kitsu_edit: bpy.props.BoolProperty( # type: ignore
|
||||
name="Create Kitsu Edit if none exists.",
|
||||
description="Automatically create a Kitsu edit for the edit.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
context_core.draw_episode_selector(context, layout)
|
||||
layout.prop(self, "create_kitsu_edit")
|
||||
layout.prop(self, "save_file")
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
active_project = cache.project_active_get()
|
||||
self._edit_entity = cache.edit_default_get(
|
||||
create=self.create_kitsu_edit, episode_id=context.scene.kitsu.episode_active_id
|
||||
)
|
||||
self._edit_entity.set_edit_task()
|
||||
task_type = self._edit_entity.get_task_type()
|
||||
|
||||
if not self._edit_entity:
|
||||
self.report({'ERROR'}, "Failed to create Kitsu edit entity.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
edit_file_path_str = self._edit_entity.get_filepath(context)
|
||||
|
||||
# Create a new file using the Video Editing template
|
||||
replace_workspace_with_template(context, task_type.name)
|
||||
core.set_resolution_and_fps(active_project, scene)
|
||||
|
||||
cache.reset_all_edits_enum_for_active_project()
|
||||
|
||||
scene.kitsu.category = "EDIT"
|
||||
scene.kitsu.edit_active_name = context_core.get_versioned_file_basename(
|
||||
Path(edit_file_path_str).stem
|
||||
)
|
||||
scene.kitsu.task_type_active_name = bkglobals.EDIT_TASK_TYPE
|
||||
|
||||
# Save File
|
||||
if self.save_file:
|
||||
if not save_shot_builder_file(file_path=edit_file_path_str):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Failed to save file at path `{edit_file_path_str}`",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({'INFO'}, f"Created edit file at {edit_file_path_str}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_open_edit_file(KITSU_OT_build_new_file_baseclass):
|
||||
bl_idname = "kitsu.open_edit_file"
|
||||
bl_label = "Open Edit File"
|
||||
bl_description = "Open Latest Edit File from the current project"
|
||||
|
||||
_kitsu_context_type = "EDIT"
|
||||
|
||||
save_current: bpy.props.BoolProperty( # type: ignore
|
||||
name="Save Current File",
|
||||
description="Automatically save the current file before opening a new one.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
global ACTIVE_PROJECT
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
flow = layout.grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=False
|
||||
)
|
||||
col = flow.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
if ACTIVE_PROJECT.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
context_core.draw_episode_selector(context, col)
|
||||
context_core.draw_edit_selector(context, col)
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
col.prop(self, "save_current")
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
edit_entity = cache.edit_default_get(episode_id=context.scene.kitsu.episode_active_id)
|
||||
if not edit_entity:
|
||||
self.report({'ERROR'}, "No edit task found on Kitsu Server.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
edit_file_path_str = edit_entity.get_filepath(context)
|
||||
if not Path(edit_file_path_str).exists():
|
||||
self.report({'ERROR'}, f"Edit file does not exist: {edit_file_path_str}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if bpy.data.is_dirty and self.save_current:
|
||||
bpy.ops.wm.save_mainfile()
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=edit_file_path_str)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = [
|
||||
KITSU_OT_build_new_shot,
|
||||
KITSU_OT_build_new_asset,
|
||||
KITSU_OT_build_config_save_hooks,
|
||||
KITSU_OT_build_config_save_settings,
|
||||
KITSU_OT_build_config_save_templates,
|
||||
KITSU_OT_create_edit_file,
|
||||
KITSU_OT_open_edit_file,
|
||||
KITSU_OT_open_shot_file,
|
||||
KITSU_OT_open_asset_file,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,69 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from . import config
|
||||
|
||||
|
||||
def list_dir_blend_files(p: Path) -> list[Path]:
|
||||
return list(p.glob('*.blend'))
|
||||
|
||||
|
||||
def get_template_for_task_type(task_type_name: str) -> Path:
|
||||
# Find Custom Template in Config Dir if available
|
||||
for file in list_dir_blend_files(config.template_dir_get()):
|
||||
if file.stem.lower() == task_type_name.lower():
|
||||
return file
|
||||
# Fall back to example templates if no custom templates found
|
||||
for file in list_dir_blend_files(config.template_example_dir_get()):
|
||||
if file.stem.lower() == task_type_name.lower():
|
||||
return file
|
||||
return
|
||||
|
||||
|
||||
def replace_workspace_with_template(context: bpy.types.Context, task_type_name: str):
|
||||
if task_type_name is None:
|
||||
return
|
||||
file_path = get_template_for_task_type(task_type_name)
|
||||
remove_prefix = "REMOVE-"
|
||||
if not file_path:
|
||||
print(f"No template found for task type '{task_type_name}'")
|
||||
return
|
||||
|
||||
if not file_path.exists():
|
||||
return
|
||||
|
||||
# Mark Existing Workspaces for Removal
|
||||
for workspace in bpy.data.workspaces:
|
||||
if workspace.name.startswith(remove_prefix):
|
||||
continue
|
||||
workspace.name = remove_prefix + workspace.name
|
||||
|
||||
# Add EXEC_DEFAULT to all bpy,ops calls to ensure they are "blocking" calls
|
||||
file_path_str = file_path.absolute().as_posix()
|
||||
with bpy.data.libraries.load(file_path_str) as (data_from, data_to):
|
||||
for workspace in data_from.workspaces:
|
||||
bpy.ops.wm.append(
|
||||
'EXEC_DEFAULT',
|
||||
filepath=file_path_str,
|
||||
directory=file_path_str + "/" + 'WorkSpace',
|
||||
filename=str(workspace),
|
||||
)
|
||||
|
||||
for lib in bpy.data.libraries:
|
||||
if lib.filepath == file_path_str:
|
||||
bpy.data.libraries.remove(bpy.data.libraries.get(lib.name))
|
||||
break
|
||||
|
||||
workspaces_to_remove = []
|
||||
for workspace in bpy.data.workspaces:
|
||||
if workspace.name.startswith(remove_prefix):
|
||||
workspaces_to_remove.append(workspace)
|
||||
|
||||
# context.window.workspace = workspace
|
||||
for workspace in workspaces_to_remove:
|
||||
with context.temp_override(workspace=workspace):
|
||||
bpy.ops.workspace.delete('EXEC_DEFAULT')
|
||||
return True
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
# Shot Builder Template Files
|
||||
|
||||
# Description
|
||||
Template files are used to implement custom UI when building new shots. Templates are custom .blend files named after the corresponding task type on the Kitsu Server for a given project. Workspaces from the template files are loaded into newly built shot files.
|
||||
## Creating a Template Files
|
||||
1. Save a new .blend file in the directory `your_project_name/svn/pro/config/shot_builder/templates`
|
||||
2. Name the .blend file after a task type in Kitsu (e.g. Animation, Layout, Compositing), name must match exactly the [task type](https://kitsu.cg-wire.com/customization-pipeline/#task-type) name on Kitsu Server.
|
||||
3. Customize Workspaces to create custom UI for the task type, and save the file.
|
||||
|
||||
|
||||
## Limitations
|
||||
Template files are only used for custom UI, other data like scene settings, collections and objects will not be loaded.
|
||||
@@ -1,63 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Header, Menu, Panel
|
||||
|
||||
from bpy.app.translations import (
|
||||
pgettext_iface as iface_,
|
||||
contexts as i18n_contexts,
|
||||
)
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator("kitsu.build_new_shot", text="Shot File")
|
||||
layout.operator("kitsu.build_new_asset", text="Asset File")
|
||||
layout.operator("kitsu.create_edit_file", text="Edit File")
|
||||
|
||||
|
||||
def topbar_kitsu_menu_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.menu("KITSU_MT_project_topbar_menu")
|
||||
|
||||
|
||||
class KITSU_MT_project_topbar_menu(Menu):
|
||||
bl_label = "Project"
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.operator("kitsu.build_new_shot", text="New Shot")
|
||||
layout.operator("kitsu.build_new_asset", text="New Asset")
|
||||
layout.operator("kitsu.create_edit_file", text="New Edit")
|
||||
layout.separator()
|
||||
layout.operator("kitsu.open_shot_file", text="Open Shot")
|
||||
layout.operator("kitsu.open_asset_file", text="Open Asset")
|
||||
layout.operator("kitsu.open_edit_file", text="Open Edit")
|
||||
layout.separator()
|
||||
layout.operator("kitsu.con_detect_context")
|
||||
|
||||
|
||||
classes = [
|
||||
KITSU_MT_project_topbar_menu,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(topbar_kitsu_menu_draw_handler)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(topbar_kitsu_menu_draw_handler)
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,39 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..sqe import opsdata, checkstrip, pull, push, ops, ui, draw
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
|
||||
def reload():
|
||||
global opsdata
|
||||
global checkstrip
|
||||
global pull
|
||||
global push
|
||||
global ops
|
||||
global ui
|
||||
global draw
|
||||
|
||||
opsdata = importlib.reload(opsdata)
|
||||
checkstrip = importlib.reload(checkstrip)
|
||||
pull = importlib.reload(pull)
|
||||
push = importlib.reload(push)
|
||||
ops = importlib.reload(ops)
|
||||
ui = importlib.reload(ui)
|
||||
draw = importlib.reload(draw)
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
ui.register()
|
||||
draw.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
draw.unregister()
|
||||
@@ -1,72 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
import bpy
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
from ..sqe import checkstrip
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def _is_range_in(range1: range, range2: range) -> bool:
|
||||
"""Whether range1 is a subset of range2"""
|
||||
# usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140)
|
||||
# first and last frame can be the same for each strip
|
||||
range2 = range(range2.start + 1, range2.stop - 1)
|
||||
|
||||
if not range1:
|
||||
return True # empty range is subset of anything
|
||||
if not range2:
|
||||
return False # non-empty range can't be subset of empty range
|
||||
if len(range1) > 1 and range1.step % range2.step:
|
||||
return False # must have a single value or integer multiple step
|
||||
return range1.start in range2 or range1[-1] in range2
|
||||
|
||||
|
||||
def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]:
|
||||
"""
|
||||
Scans sequence editor and returns a dictionary. It contains a key for each channel
|
||||
and a list of ranges with the occupied frame ranges as values.
|
||||
"""
|
||||
# {'1': [(101, 213), (300, 320)]}.
|
||||
ranges: Dict[str, List[range]] = {}
|
||||
|
||||
# Populate ranges.
|
||||
for strip in context.scene.sequence_editor.sequences_all:
|
||||
ranges.setdefault(str(strip.channel), [])
|
||||
ranges[str(strip.channel)].append(
|
||||
range(strip.frame_final_start, strip.frame_final_end + 1)
|
||||
)
|
||||
|
||||
# Sort ranges tuple list.
|
||||
for channel in ranges:
|
||||
liste = ranges[channel]
|
||||
ranges[channel] = sorted(liste, key=lambda item: item.start)
|
||||
|
||||
return ranges
|
||||
|
||||
|
||||
def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool:
|
||||
for r in occupied_ranges:
|
||||
# Range(101, 150).
|
||||
if _is_range_in(range_to_check, r):
|
||||
return True
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def get_shot_strips(context: bpy.types.Context) -> List[bpy.types.Strip]:
|
||||
shot_strips = []
|
||||
shot_strips.extend(
|
||||
[
|
||||
strip
|
||||
for strip in context.scene.sequence_editor.sequences_all
|
||||
if checkstrip.is_valid_type(strip, log=False)
|
||||
and checkstrip.is_linked(strip, log=False)
|
||||
]
|
||||
)
|
||||
return shot_strips
|
||||
@@ -1,131 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
|
||||
import gazu
|
||||
from ..types import Sequence, Project, Shot, Cache
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
VALID_STRIP_TYPES = {"MOVIE", "COLOR"}
|
||||
|
||||
|
||||
def is_valid_type(strip: bpy.types.Strip, log: bool = True) -> bool:
|
||||
if not strip.type in VALID_STRIP_TYPES:
|
||||
if log:
|
||||
logger.info("Strip: %s. Invalid type", strip.type)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_initialized(strip: bpy.types.Strip) -> bool:
|
||||
"""Returns True if strip.kitsu.initialized is True else False"""
|
||||
if not strip.kitsu.initialized:
|
||||
logger.info("Strip: %s. Not initialized", strip.name)
|
||||
return False
|
||||
|
||||
logger.info("Strip: %s. Is initialized", strip.name)
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(strip: bpy.types.Strip, log: bool = True) -> bool:
|
||||
"""Returns True if strip.kitsu.linked is True else False"""
|
||||
if not strip.kitsu.linked:
|
||||
if log:
|
||||
logger.info("Strip: %s. Not linked yet", strip.name)
|
||||
return False
|
||||
if log:
|
||||
logger.info("Strip: %s. Is linked to ID: %s", strip.name, strip.kitsu.shot_id)
|
||||
return True
|
||||
|
||||
|
||||
def has_meta(strip: bpy.types.Strip) -> bool:
|
||||
"""Returns True if strip.kitsu.shot_name and strip.kitsu.sequence_name is Truethy else False"""
|
||||
seq = strip.kitsu.sequence_name
|
||||
shot = strip.kitsu.shot_name or strip.kitsu.manual_shot_name
|
||||
|
||||
if not bool(seq and shot):
|
||||
logger.info("Strip: %s. Missing metadata", strip.name)
|
||||
return False
|
||||
|
||||
logger.info("Strip: %s. Has metadata (Sequence: %s, Shot: %s)", strip.name, seq, shot)
|
||||
return True
|
||||
|
||||
|
||||
def shot_exists_by_id(strip: bpy.types.Strip, clear_cache: bool = True) -> Optional[Shot]:
|
||||
"""Returns Shot instance if shot with strip.kitsu.shot_id exists else None"""
|
||||
|
||||
if clear_cache:
|
||||
Cache.clear_all()
|
||||
|
||||
try:
|
||||
shot = Shot.by_id(strip.kitsu.shot_id)
|
||||
except (gazu.exception.RouteNotFoundException, gazu.exception.ServerErrorException):
|
||||
logger.info(
|
||||
"Strip: %s No shot found on server with ID: %s",
|
||||
strip.name,
|
||||
strip.kitsu.shot_id,
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info("Strip: %s Shot %s exists on server (ID: %s)", strip.name, shot.name, shot.id)
|
||||
return shot
|
||||
|
||||
|
||||
def seq_exists_by_id(
|
||||
strip: bpy.types.Strip, project: Project, clear_cache: bool = True
|
||||
) -> Optional[Sequence]:
|
||||
if clear_cache:
|
||||
Cache.clear_all()
|
||||
|
||||
zseq = project.get_sequence(strip.kitsu.sequence_id)
|
||||
|
||||
if not zseq:
|
||||
logger.info(
|
||||
"Strip: %s Sequence %s does not exist on server",
|
||||
strip.name,
|
||||
strip.kitsu.sequence_name,
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
"Strip: %s Sequence %s exists in on server (ID: %s)",
|
||||
strip.name,
|
||||
zseq.name,
|
||||
zseq.id,
|
||||
)
|
||||
return zseq
|
||||
|
||||
|
||||
def shot_exists_by_name(
|
||||
strip: bpy.types.Strip,
|
||||
project: Project,
|
||||
sequence: Sequence,
|
||||
clear_cache: bool = True,
|
||||
) -> Optional[Shot]:
|
||||
"""Returns Shot instance if strip.kitsu.shot_name exists on server, else None"""
|
||||
|
||||
if clear_cache:
|
||||
Cache.clear_all()
|
||||
|
||||
shot = project.get_shot_by_name(sequence, strip.kitsu.manual_shot_name)
|
||||
if not shot:
|
||||
logger.info(
|
||||
"Strip: %s Shot %s does not exist on server",
|
||||
strip.name,
|
||||
strip.kitsu.manual_shot_name,
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info("Strip: %s Shot already existent on server (ID: %s)", strip.name, shot.id)
|
||||
return shot
|
||||
|
||||
|
||||
def contains(strip: bpy.types.Strip, framenr: int) -> bool:
|
||||
"""Returns True if the strip covers the given frame number"""
|
||||
return int(strip.frame_final_start) <= framenr <= int(strip.frame_final_end)
|
||||
@@ -1,126 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import typing
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
# Shaders and batches
|
||||
|
||||
rect_coords = ((0, 0), (1, 0), (1, 1), (0, 1))
|
||||
indices = ((0, 1, 2), (2, 3, 0))
|
||||
# Setup shaders only if Blender runs in the foreground.
|
||||
# If running in the background, no handles are registered, as drawing extra UI
|
||||
# elements does not make sense.
|
||||
# See register() and unregister().
|
||||
if not bpy.app.background:
|
||||
ucolor_2d_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
ucolor_2d_rect_batch = batch_for_shader(
|
||||
ucolor_2d_shader, "TRIS", {"pos": rect_coords}, indices=indices
|
||||
)
|
||||
|
||||
|
||||
Float2 = typing.Tuple[float, float]
|
||||
Float3 = typing.Tuple[float, float, float]
|
||||
Float4 = typing.Tuple[float, float, float, float]
|
||||
|
||||
|
||||
def draw_line(position: Float2, size: Float2, color: Float4):
|
||||
with gpu.matrix.push_pop():
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
gpu.matrix.translate(position)
|
||||
gpu.matrix.scale(size)
|
||||
|
||||
ucolor_2d_shader.uniform_float("color", color)
|
||||
ucolor_2d_rect_batch.draw(ucolor_2d_shader)
|
||||
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def get_strip_rectf(strip) -> Float4:
|
||||
# Get x and y in terms of the grid's frames and channels.
|
||||
x1 = strip.frame_final_start
|
||||
x2 = strip.frame_final_end
|
||||
# Seems to be a 5 % offset from channel top start of strip.
|
||||
y1 = strip.channel + 0.05
|
||||
y2 = strip.channel - 0.05 + 1
|
||||
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
def draw_line_in_strip(strip_coords: Float4, height_factor: float, color: Float4):
|
||||
# Unpack strip coordinates.
|
||||
s_x1, channel, s_x2, _ = strip_coords
|
||||
|
||||
# Get the line's measures, as a percentage of channel height.
|
||||
# Note that the strip's height is 10% smaller than the channel (centered 5% top and bottom).
|
||||
# Note 2: offset the height slightly (0.005) to make room for the selection outline (channel coords).
|
||||
line_height_in_channel = (0.9 - 0.005 * 2) * height_factor + 0.005
|
||||
line_thickness = 0.04
|
||||
|
||||
# Offset the width slightly to make room for the selection outline (virtual grid horizontal coords).
|
||||
width_offset = 0.2
|
||||
width = (s_x2 - s_x1) - width_offset * 2
|
||||
|
||||
pos = (s_x1 + width_offset, channel + line_height_in_channel)
|
||||
scale = (width, line_thickness)
|
||||
draw_line(pos, scale, color)
|
||||
|
||||
|
||||
def draw_callback_px():
|
||||
context = bpy.context
|
||||
sqe = context.scene.sequence_editor
|
||||
if not sqe:
|
||||
return
|
||||
strips = sqe.sequences_all
|
||||
|
||||
for strip in strips:
|
||||
# Get corners of the strip rectangle in terms of the grid's frames and channels (virtual, not px).
|
||||
strip_coords = get_strip_rectf(strip)
|
||||
|
||||
if strip.kitsu.initialized or strip.kitsu.linked:
|
||||
try:
|
||||
color = tuple(
|
||||
context.scene.kitsu.sequence_colors[strip.kitsu.sequence_id].color
|
||||
)
|
||||
except KeyError:
|
||||
color = (1, 1, 1)
|
||||
|
||||
alpha = 0.75 if strip.kitsu.linked else 0.25
|
||||
|
||||
line_color = color + (alpha,)
|
||||
draw_line_in_strip(strip_coords, 0.0, line_color)
|
||||
|
||||
if strip.kitsu.media_outdated:
|
||||
line_color = (1.0, 0.05, 0.145, 0.75)
|
||||
draw_line_in_strip(strip_coords, 0.9, line_color)
|
||||
|
||||
|
||||
draw_handles = []
|
||||
|
||||
|
||||
def register():
|
||||
if bpy.app.background:
|
||||
# Do not register anything if Blender runs in the background (no UI needed).
|
||||
return
|
||||
draw_handles.append(
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_add(
|
||||
draw_callback_px, (), "WINDOW", "POST_VIEW"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
if bpy.app.background:
|
||||
return
|
||||
for handle in reversed(draw_handles):
|
||||
try:
|
||||
bpy.types.SpaceSequenceEditor.draw_handler_remove(handle, "WINDOW")
|
||||
except ValueError:
|
||||
# Not sure why, but sometimes the handler seems to already be removed...??
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,265 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
from . import pull
|
||||
import bpy
|
||||
from .. import bkglobals, prefs
|
||||
from ..logger import LoggerFactory
|
||||
from ..types import Sequence, Task, TaskStatus, Shot, TaskType
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
_sqe_shot_enum_list: List[Tuple[str, str, str]] = []
|
||||
_sqe_not_linked: List[Tuple[str, str, str]] = []
|
||||
_sqe_duplicates: List[Tuple[str, str, str]] = []
|
||||
_sqe_multi_project: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def sqe_get_not_linked(self, context):
|
||||
return _sqe_not_linked
|
||||
|
||||
|
||||
def sqe_get_duplicates(self, context):
|
||||
return _sqe_duplicates
|
||||
|
||||
|
||||
def sqe_get_multi_project(self, context):
|
||||
return _sqe_multi_project
|
||||
|
||||
|
||||
def sqe_update_not_linked(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""get all strips that are initialized but not linked yet"""
|
||||
enum_list = []
|
||||
|
||||
if context.selected_sequences:
|
||||
strips = context.selected_sequences
|
||||
else:
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
for strip in strips:
|
||||
if strip.kitsu.initialized and not strip.kitsu.linked:
|
||||
enum_list.append((strip.name, strip.name, ""))
|
||||
|
||||
return enum_list
|
||||
|
||||
|
||||
def sqe_update_duplicates(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""get all strips that are initialized but not linked yet"""
|
||||
enum_list = []
|
||||
data_dict = {}
|
||||
if context.selected_sequences:
|
||||
strips = context.selected_sequences
|
||||
else:
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
# Create data dict that holds all shots ids and the corresponding strips that are linked to it.
|
||||
for i in range(len(strips)):
|
||||
if strips[i].kitsu.linked:
|
||||
# Get shot_id, shot_name, create entry in data_dict if id not existent.
|
||||
shot_id = strips[i].kitsu.shot_id
|
||||
shot_name = strips[i].kitsu.shot_name
|
||||
if shot_id not in data_dict:
|
||||
data_dict[shot_id] = {"name": shot_name, "strips": []}
|
||||
|
||||
# Append i to strips list.
|
||||
if strips[i] not in set(data_dict[shot_id]["strips"]):
|
||||
data_dict[shot_id]["strips"].append(strips[i])
|
||||
|
||||
# Comparet to all other strip.
|
||||
for j in range(i + 1, len(strips)):
|
||||
if shot_id == strips[j].kitsu.shot_id:
|
||||
data_dict[shot_id]["strips"].append(strips[j])
|
||||
|
||||
# Convert in data strucutre for enum property.
|
||||
for shot_id, data in data_dict.items():
|
||||
if len(data["strips"]) > 1:
|
||||
enum_list.append(("", data["name"], shot_id))
|
||||
for strip in data["strips"]:
|
||||
enum_list.append((strip.name, strip.name, ""))
|
||||
|
||||
return enum_list
|
||||
|
||||
|
||||
def sqe_update_multi_project(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""get all strips that are initialized but not linked yet"""
|
||||
enum_list: List[Tuple[str, str, str]] = []
|
||||
data_dict: Dict[str, Any] = {}
|
||||
|
||||
if context.selected_sequences:
|
||||
strips = context.selected_sequences
|
||||
else:
|
||||
strips = context.scene.sequence_editor.sequences_all
|
||||
|
||||
# Create data dict that holds project names as key and values the corresponding sequence strips.
|
||||
for strip in strips:
|
||||
if strip.kitsu.linked:
|
||||
project = strip.kitsu.project_name
|
||||
if project not in data_dict:
|
||||
data_dict[project] = []
|
||||
|
||||
# Append i to strips list.
|
||||
if strip not in set(data_dict[project]):
|
||||
data_dict[project].append(strip)
|
||||
|
||||
# Convert in data strucutre for enum property.
|
||||
for project, strips in data_dict.items():
|
||||
enum_list.append(("", project, ""))
|
||||
for strip in strips:
|
||||
enum_list.append((strip.name, strip.name, ""))
|
||||
|
||||
return enum_list
|
||||
|
||||
|
||||
def resolve_pattern(pattern: str, var_lookup_table: Dict[str, str]) -> str:
|
||||
matches = re.findall(r"\<(\w+)\>", pattern)
|
||||
matches = list(set(matches))
|
||||
# If no variable detected just return value.
|
||||
if len(matches) == 0:
|
||||
return pattern
|
||||
else:
|
||||
result = pattern
|
||||
for to_replace in matches:
|
||||
if to_replace in var_lookup_table:
|
||||
to_insert = var_lookup_table[to_replace]
|
||||
result = result.replace("<{}>".format(to_replace), to_insert)
|
||||
else:
|
||||
logger.warning(
|
||||
"Failed to resolve variable: %s not defined!", to_replace
|
||||
)
|
||||
return ""
|
||||
return result
|
||||
|
||||
|
||||
def get_shots_enum_for_link_shot_op(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _sqe_shot_enum_list
|
||||
|
||||
if not self.sequence_enum:
|
||||
return []
|
||||
|
||||
zseq_active = Sequence.by_id(self.sequence_enum)
|
||||
|
||||
_sqe_shot_enum_list.clear()
|
||||
_sqe_shot_enum_list.extend(
|
||||
[(s.id, s.name, s.description or "") for s in zseq_active.get_all_shots()]
|
||||
)
|
||||
return _sqe_shot_enum_list
|
||||
|
||||
|
||||
def upload_preview(
|
||||
context: bpy.types.Context, filepath: Path, task_type: TaskType, comment: str = ""
|
||||
) -> None:
|
||||
# Get shot by id which is in filename of thumbnail.
|
||||
shot_id = filepath.name.split(bkglobals.SPACE_REPLACER)[0]
|
||||
shot = Shot.by_id(shot_id)
|
||||
|
||||
# Find task from task type for that shot, ca be None of no task was added for that task type.
|
||||
task = Task.by_name(shot, task_type)
|
||||
|
||||
if not task:
|
||||
# Turns out a entity on the server can have 0 tasks even tough task types exist
|
||||
# you have to create a task first before being able to upload a thumbnail.
|
||||
task_status = TaskStatus.by_short_name("wip")
|
||||
task = Task.new_task(shot, task_type, task_status=task_status)
|
||||
else:
|
||||
task_status = TaskStatus.by_id(task.task_status_id)
|
||||
|
||||
# Create a comment, e.G 'Update thumbnail'.
|
||||
comment_obj = task.add_comment(task_status, comment=comment)
|
||||
|
||||
# Add_preview_to_comment.
|
||||
task.add_preview_to_comment(comment_obj, filepath.as_posix())
|
||||
|
||||
logger.info(f"Uploaded preview for shot: {shot.name} under: {task_type.name}")
|
||||
|
||||
|
||||
def init_start_frame_offset(strip: bpy.types.Strip) -> None:
|
||||
# Frame start offset.
|
||||
offset_start = strip.frame_final_start - strip.frame_start
|
||||
# Cast offset start to int, since after Blender 3.3 strip values are floats
|
||||
strip.kitsu.frame_start_offset = int(offset_start)
|
||||
|
||||
|
||||
def append_sequence_color(
|
||||
context: bpy.types.Context, seq: Sequence
|
||||
) -> Optional[Tuple[str, str, str]]:
|
||||
"""
|
||||
Extend scene.kitsu.sequence_colors property with seq.data['color'] value if it exists.
|
||||
"""
|
||||
# Pull sequencee color property.
|
||||
|
||||
if not seq.data:
|
||||
logger.info("%s failed to load sequence color. Missing 'data' key")
|
||||
return None
|
||||
if not "color" in seq.data:
|
||||
logger.info("%s failed to load sequence color. Missing data['color'] key")
|
||||
return None
|
||||
|
||||
try:
|
||||
item = context.scene.kitsu.sequence_colors[seq.id]
|
||||
except:
|
||||
item = context.scene.kitsu.sequence_colors.add()
|
||||
item.name = seq.id
|
||||
logger.info(
|
||||
"Added %s to scene.kitsu.seqeuence_colors",
|
||||
seq.name,
|
||||
)
|
||||
finally:
|
||||
item.color = tuple(seq.data["color"])
|
||||
|
||||
return tuple(seq.data["color"])
|
||||
|
||||
|
||||
def push_sequence_color(context: bpy.types.Context, sequence: Sequence) -> None:
|
||||
# Updates sequence color and logs.
|
||||
try:
|
||||
item = context.scene.kitsu.sequence_colors[sequence.id]
|
||||
except KeyError:
|
||||
logger.info(
|
||||
"%s failed to push sequence color. Does not exists in 'context.scene.kitsu.sequence_colors'",
|
||||
sequence.name,
|
||||
)
|
||||
else:
|
||||
sequence.update_data({"color": list(item.color)})
|
||||
logger.info("%s pushed sequence color", sequence.name)
|
||||
|
||||
|
||||
def create_metadata_strip(
|
||||
scene: bpy.types.Scene, name: str, channel, frame_start: int, frame_end: int
|
||||
) -> bpy.types.MovieStrip:
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
strip = scene.sequence_editor.sequences.new_movie(
|
||||
name,
|
||||
addon_prefs.metadatastrip_file,
|
||||
channel,
|
||||
frame_start,
|
||||
)
|
||||
|
||||
strip.frame_final_end = frame_end
|
||||
|
||||
# Set blend alpha.
|
||||
strip.blend_alpha = 0
|
||||
|
||||
init_start_frame_offset(strip)
|
||||
|
||||
return strip
|
||||
|
||||
|
||||
def link_metadata_strip(
|
||||
context, shot: Shot, seq: Sequence, strip: bpy.types.MovieStrip
|
||||
) -> bpy.types.MovieStrip:
|
||||
# Pull shot meta.
|
||||
pull.shot_meta(strip, shot)
|
||||
|
||||
# Rename strip.
|
||||
strip.name = shot.name
|
||||
|
||||
# Pull sequence color.
|
||||
append_sequence_color(context, seq)
|
||||
return strip
|
||||
@@ -1,42 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import bkglobals
|
||||
from ..types import Cache, Sequence, Project, Shot
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def shot_meta(strip: bpy.types.Strip, shot: Shot, clear_cache: bool = True) -> None:
|
||||
if clear_cache:
|
||||
# Clear cache before pulling.
|
||||
Cache.clear_all()
|
||||
|
||||
# Update sequence props.
|
||||
seq = Sequence.by_id(shot.parent_id)
|
||||
strip.kitsu.sequence_id = seq.id
|
||||
strip.kitsu.sequence_name = seq.name
|
||||
|
||||
# Update shot props.
|
||||
strip.kitsu.shot_id = shot.id
|
||||
strip.kitsu.shot_name = shot.name
|
||||
strip.kitsu.shot_description = shot.description if shot.description else ""
|
||||
|
||||
# Update project props.
|
||||
project = Project.by_id(shot.project_id)
|
||||
strip.kitsu.project_id = project.id
|
||||
strip.kitsu.project_name = project.name
|
||||
|
||||
# Update meta props.
|
||||
strip.kitsu.initialized = True
|
||||
strip.kitsu.linked = True
|
||||
|
||||
# Update strip name.
|
||||
strip.name = shot.name
|
||||
|
||||
# Log.
|
||||
logger.info("Pulled meta from shot: %s to strip: %s", shot.name, strip.name)
|
||||
@@ -1,94 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import bkglobals
|
||||
from ..types import Sequence, Project, Shot
|
||||
from ..logger import LoggerFactory
|
||||
import gazu
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def shot_meta(strip: bpy.types.Strip, shot: Shot) -> None:
|
||||
# Update shot info.
|
||||
|
||||
shot.name = strip.kitsu.shot_name
|
||||
shot.description = strip.kitsu.shot_description
|
||||
shot.data["frame_in"] = strip.frame_final_start
|
||||
shot.data["frame_out"] = strip.frame_final_end
|
||||
shot.data["3d_start"] = strip.kitsu_3d_start
|
||||
shot.nb_frames = strip.frame_final_duration
|
||||
shot.data["fps"] = bkglobals.FPS
|
||||
|
||||
# If user changed the sequence the shot belongs to
|
||||
# (can only be done by operator not by hand).
|
||||
if strip.kitsu.sequence_id != shot.sequence_id:
|
||||
sequence = Sequence.by_id(strip.kitsu.sequence_id)
|
||||
shot.sequence_id = sequence.id
|
||||
shot.parent_id = sequence.id
|
||||
shot.sequence_name = sequence.name
|
||||
|
||||
# Update on server.
|
||||
shot.update()
|
||||
logger.info("Pushed meta to shot: %s from strip: %s", shot.name, strip.name)
|
||||
|
||||
|
||||
def new_shot(
|
||||
strip: bpy.types.Strip, sequence: Sequence, project: Project, add_tasks=False
|
||||
) -> Shot:
|
||||
frame_range = (strip.frame_final_start, strip.frame_final_end)
|
||||
shot = project.create_shot(
|
||||
sequence,
|
||||
strip.kitsu.shot_name,
|
||||
nb_frames=strip.frame_final_duration,
|
||||
frame_in=frame_range[0],
|
||||
frame_out=frame_range[1],
|
||||
data={"fps": bkglobals.FPS, "3d_start": bkglobals.FRAME_START},
|
||||
)
|
||||
|
||||
if add_tasks:
|
||||
create_intial_tasks(shot, project)
|
||||
|
||||
# Update description, no option to pass that on create.
|
||||
if strip.kitsu.shot_description:
|
||||
shot.description = strip.kitsu.shot_description
|
||||
shot.update()
|
||||
|
||||
# Set project name locally, will be available on next pull.
|
||||
shot.project_name = project.name
|
||||
logger.info("Pushed create shot: %s for project: %s", shot.name, project.name)
|
||||
return shot
|
||||
|
||||
|
||||
def new_sequence(strip: bpy.types.Strip, project: Project) -> Sequence:
|
||||
sequence = project.create_sequence(
|
||||
strip.kitsu.sequence_name,
|
||||
strip.kitsu.episode_id
|
||||
)
|
||||
logger.info(
|
||||
"Pushed create sequence: %s for project: %s", sequence.name, project.name
|
||||
)
|
||||
return sequence
|
||||
|
||||
|
||||
def delete_shot(strip: bpy.types.Strip, shot: Shot) -> str:
|
||||
result = shot.remove()
|
||||
logger.info(
|
||||
"Pushed delete shot: %s for project: %s",
|
||||
shot.name,
|
||||
shot.project_name or "Unknown",
|
||||
)
|
||||
strip.kitsu.clear()
|
||||
return result
|
||||
|
||||
|
||||
def create_intial_tasks(shot: Shot, project: Project):
|
||||
shot_entity = gazu.shot.get_shot(shot.id)
|
||||
for task_type in gazu.task.all_task_types_for_project(project.id):
|
||||
if task_type["for_entity"] == "Shot":
|
||||
gazu.task.new_task(shot_entity, task_type)
|
||||
@@ -1,770 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import cache, prefs, ui, bkglobals
|
||||
from ..sqe import checkstrip
|
||||
from ..context import core as context_core
|
||||
from ..logger import LoggerFactory
|
||||
from ..sqe.ops import (
|
||||
KITSU_OT_sqe_push_new_sequence,
|
||||
KITSU_OT_sqe_push_new_shot,
|
||||
KITSU_OT_sqe_push_shot_meta,
|
||||
KITSU_OT_sqe_uninit_strip,
|
||||
KITSU_OT_sqe_unlink_shot,
|
||||
KITSU_OT_sqe_init_strip,
|
||||
KITSU_OT_sqe_link_shot,
|
||||
KITSU_OT_sqe_link_sequence,
|
||||
KITSU_OT_sqe_set_thumbnail_task_type,
|
||||
KITSU_OT_sqe_set_sqe_render_task_type,
|
||||
KITSU_OT_sqe_push_render_still,
|
||||
KITSU_OT_sqe_push_render,
|
||||
KITSU_OT_sqe_push_del_shot,
|
||||
KITSU_OT_sqe_pull_shot_meta,
|
||||
KITSU_OT_sqe_multi_edit_strip,
|
||||
KITSU_OT_sqe_debug_duplicates,
|
||||
KITSU_OT_sqe_debug_not_linked,
|
||||
KITSU_OT_sqe_debug_multi_project,
|
||||
KITSU_OT_sqe_pull_edit,
|
||||
KITSU_OT_sqe_init_strip_start_frame,
|
||||
KITSU_OT_sqe_create_metadata_strip,
|
||||
KITSU_OT_sqe_fix_metadata_strips,
|
||||
KITSU_OT_sqe_add_sequence_color,
|
||||
KITSU_OT_sqe_scan_for_media_updates,
|
||||
KITSU_OT_sqe_change_strip_source,
|
||||
KITSU_OT_sqe_clear_update_indicators,
|
||||
KITSU_OT_sqe_import_image_sequence,
|
||||
KITSU_OT_sqe_import_playblast,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
def get_selshots_noun(nr_of_shots: int, prefix: str = "Active") -> str:
|
||||
if not nr_of_shots:
|
||||
noun = "All"
|
||||
elif nr_of_shots == 1:
|
||||
noun = f"{prefix} Shot"
|
||||
else:
|
||||
noun = "%i Shots" % nr_of_shots
|
||||
return noun
|
||||
|
||||
|
||||
class KITSU_MT_sqe_advanced_delete(bpy.types.Menu):
|
||||
bl_label = "Advanced Delete"
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
selshots = context.selected_sequences
|
||||
strips_to_unlink = [s for s in selshots if s.kitsu.linked]
|
||||
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
KITSU_OT_sqe_push_del_shot.bl_idname,
|
||||
text=f"Unlink and Delete {len(strips_to_unlink)} Shots",
|
||||
icon="CANCEL",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
|
||||
"""
|
||||
Panel in sequence editor that shows all kinds of tools related to Kitsu and sequence strips
|
||||
"""
|
||||
|
||||
# TODO: Because each draw function was previously a seperate Panel there might be a lot of
|
||||
# code duplication now, needs to be refactored at some point
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Shot Tools"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 20
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not context_core.is_edit_context():
|
||||
return False
|
||||
sqe = context.scene.sequence_editor
|
||||
return bool(prefs.session_auth(context) or (sqe and sqe.sequences_all))
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
active_project = cache.project_active_get()
|
||||
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
|
||||
if not cache.episode_active_get():
|
||||
self.layout.label(text="Please Set Active Episode", icon="ERROR")
|
||||
return
|
||||
if self.poll_error(context):
|
||||
self.draw_error(context)
|
||||
|
||||
if self.poll_setup(context):
|
||||
self.draw_setup(context)
|
||||
|
||||
if self.poll_metadata(context):
|
||||
self.draw_metadata(context)
|
||||
|
||||
if self.poll_offline_metadata(context):
|
||||
self.draw_offline_metadata(context)
|
||||
|
||||
if self.poll_multi_edit(context):
|
||||
self.draw_multi_edit(context)
|
||||
|
||||
if self.poll_push(context):
|
||||
self.draw_push(context)
|
||||
|
||||
if self.poll_pull(context):
|
||||
self.draw_pull(context)
|
||||
self.draw_media(context)
|
||||
|
||||
if self.poll_debug(context):
|
||||
self.draw_debug(context)
|
||||
|
||||
@classmethod
|
||||
def poll_error(cls, context: bpy.types.Context) -> bool:
|
||||
project_active = cache.project_active_get()
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
if not prefs.session_auth(context):
|
||||
return False
|
||||
|
||||
return bool(not project_active or not addon_prefs.is_project_root_valid)
|
||||
|
||||
def draw_error(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
project_active = cache.project_active_get()
|
||||
box = ui.draw_error_box(layout)
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
if not project_active:
|
||||
ui.draw_error_active_project_unset(box)
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
ui.draw_error_invalid_project_root_dir(box)
|
||||
|
||||
@classmethod
|
||||
def poll_setup(cls, context: bpy.types.Context) -> bool:
|
||||
return bool(context.selected_sequences)
|
||||
|
||||
def draw_setup(self, context: bpy.types.Context) -> None:
|
||||
"""
|
||||
Panel in SQE that shows operators to setup shots. That includes initialization,
|
||||
uninitizialization, linking and unlinking.
|
||||
"""
|
||||
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
selshots = context.selected_sequences
|
||||
nr_of_shots = len(selshots)
|
||||
noun = get_selshots_noun(nr_of_shots)
|
||||
project_active = cache.project_active_get()
|
||||
|
||||
strips_to_init = []
|
||||
strips_to_uninit = []
|
||||
strips_to_unlink = []
|
||||
|
||||
for s in selshots:
|
||||
if s.type not in checkstrip.VALID_STRIP_TYPES:
|
||||
continue
|
||||
if not s.kitsu.initialized:
|
||||
strips_to_init.append(s)
|
||||
elif s.kitsu.linked:
|
||||
strips_to_unlink.append(s)
|
||||
elif s.kitsu.initialized:
|
||||
strips_to_uninit.append(s)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Setup Shots", icon="TOOL_SETTINGS")
|
||||
|
||||
# Production.
|
||||
if prefs.session_auth(context):
|
||||
box.row().label(text=f"Production: {project_active.name}")
|
||||
|
||||
# Single Selection.
|
||||
if nr_of_shots == 1:
|
||||
row = box.row(align=True)
|
||||
|
||||
# Initialize.
|
||||
if strip.type not in checkstrip.VALID_STRIP_TYPES:
|
||||
row.label(text=f"Only sequence strips of types: {checkstrip.VALID_STRIP_TYPES }")
|
||||
return
|
||||
|
||||
if not strip.kitsu.initialized:
|
||||
# Init active.
|
||||
row.operator(KITSU_OT_sqe_init_strip.bl_idname, text=f"Init {noun}", icon="ADD")
|
||||
# Link active.
|
||||
row.operator(
|
||||
KITSU_OT_sqe_link_shot.bl_idname,
|
||||
text=f"Link {noun}",
|
||||
icon="LINKED",
|
||||
)
|
||||
# Create metadata strip from uninitialized strip.
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_sqe_create_metadata_strip.bl_idname,
|
||||
text=f"Create Metadata Strip from {noun}",
|
||||
)
|
||||
|
||||
# Unlink.
|
||||
elif strip.kitsu.linked:
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_sqe_unlink_shot.bl_idname,
|
||||
text=f"Unlink {noun}",
|
||||
icon="UNLINKED",
|
||||
)
|
||||
row.menu("KITSU_MT_sqe_advanced_delete", icon="DOWNARROW_HLT", text="")
|
||||
|
||||
# Uninitialize.
|
||||
else:
|
||||
row = box.row(align=True)
|
||||
# Unlink active.
|
||||
row.operator(
|
||||
KITSU_OT_sqe_uninit_strip.bl_idname,
|
||||
text=f"Uninitialize {noun}",
|
||||
icon="REMOVE",
|
||||
)
|
||||
|
||||
# Multiple Selection.
|
||||
elif nr_of_shots > 1:
|
||||
row = box.row(align=True)
|
||||
|
||||
# Init.
|
||||
if strips_to_init:
|
||||
row.operator(
|
||||
KITSU_OT_sqe_init_strip.bl_idname,
|
||||
text=f"Init {len(strips_to_init)} Shots",
|
||||
icon="ADD",
|
||||
)
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_sqe_create_metadata_strip.bl_idname,
|
||||
text=f"Create {len(strips_to_init)} Metadata Strips",
|
||||
)
|
||||
|
||||
# Make row.
|
||||
if strips_to_uninit or strips_to_unlink:
|
||||
row = box.row(align=True)
|
||||
|
||||
# Uninitialize.
|
||||
if strips_to_uninit:
|
||||
row.operator(
|
||||
KITSU_OT_sqe_uninit_strip.bl_idname,
|
||||
text=f"Uninitialize {len(strips_to_uninit)} Shots",
|
||||
icon="REMOVE",
|
||||
)
|
||||
|
||||
# Unlink all.
|
||||
if strips_to_unlink:
|
||||
row.operator(
|
||||
KITSU_OT_sqe_unlink_shot.bl_idname,
|
||||
text=f"Unlink {len(strips_to_unlink)} Shots",
|
||||
icon="UNLINKED",
|
||||
)
|
||||
row.menu("KITSU_MT_sqe_advanced_delete", icon="DOWNARROW_HLT", text="")
|
||||
|
||||
@classmethod
|
||||
def poll_metadata(cls, context: bpy.types.Context) -> bool:
|
||||
nr_of_shots = len(context.selected_sequences)
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
if nr_of_shots == 1:
|
||||
return strip.kitsu.initialized
|
||||
return False
|
||||
|
||||
def draw_metadata(self, context: bpy.types.Context) -> None:
|
||||
"""
|
||||
Panel in sequence editor that shows .kitsu properties of active strip. (shot, sequence)
|
||||
"""
|
||||
split_factor = 0.2
|
||||
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Metadata", icon="ALIGN_LEFT")
|
||||
|
||||
col = box.column(align=True)
|
||||
|
||||
# Sequence.
|
||||
split = col.split(factor=split_factor, align=True)
|
||||
split.label(text="Sequence")
|
||||
|
||||
if not strip.kitsu.sequence_id:
|
||||
sub_row = split.row(align=True)
|
||||
sub_row.prop(strip.kitsu, "sequence_name", text="")
|
||||
sub_row.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
|
||||
|
||||
else:
|
||||
# Lots of splitting because color prop is too big by default
|
||||
sub_split = split.split(factor=0.8, align=True)
|
||||
sub_split.prop(strip.kitsu, "sequence_name", text="") # TODO Use new dropdown here too
|
||||
|
||||
sub_sub_split = sub_split.split(factor=0.4, align=True)
|
||||
sub_sub_split.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
|
||||
|
||||
try:
|
||||
sequence_color_item = context.scene.kitsu.sequence_colors[strip.kitsu.sequence_id]
|
||||
except KeyError:
|
||||
sub_sub_split.operator(
|
||||
KITSU_OT_sqe_add_sequence_color.bl_idname, text="", icon="COLOR"
|
||||
)
|
||||
|
||||
else:
|
||||
sub_sub_split.prop(sequence_color_item, "color", text="")
|
||||
|
||||
# Shot.
|
||||
split = col.split(factor=split_factor, align=True)
|
||||
split.label(text="Shot")
|
||||
if not strip.kitsu.shot_id:
|
||||
placeholder = strip.kitsu.shot_name or ''
|
||||
split.prop(strip.kitsu, "manual_shot_name", text="", placeholder=placeholder)
|
||||
else:
|
||||
split.prop(strip.kitsu, "shot_name", text="")
|
||||
|
||||
# Description.
|
||||
split = col.split(factor=split_factor, align=True)
|
||||
split.label(text="Description")
|
||||
split.prop(strip.kitsu, "shot_description_display", text="")
|
||||
split.enabled = False if not strip.kitsu.initialized else True
|
||||
|
||||
# Frame range.
|
||||
split = col.split(factor=split_factor)
|
||||
split.label(text="Frame Range")
|
||||
row = split.row(align=False)
|
||||
row.prop(strip, "kitsu_3d_start", text="In")
|
||||
row.prop(strip, "kitsu_frame_end", text="Out")
|
||||
row.prop(strip, "kitsu_frame_duration", text="Duration")
|
||||
row.operator(KITSU_OT_sqe_init_strip_start_frame.bl_idname, text="", icon="FILE_REFRESH")
|
||||
|
||||
"""
|
||||
split = col.split(factor=split_factor)
|
||||
split.label(text="Offsets")
|
||||
row = split.row(align=False)
|
||||
row.prop(strip.kitsu, "frame_start_offset", text="In")
|
||||
"""
|
||||
|
||||
def poll_offline_metadata(cls, context: bpy.types.Context) -> bool:
|
||||
offline_metadata_strips = [
|
||||
strip
|
||||
for strip in context.scene.sequence_editor.sequences
|
||||
if strip.kitsu.shot_id != '' and not Path(strip.filepath).is_file()
|
||||
]
|
||||
|
||||
return len(offline_metadata_strips) > 0
|
||||
|
||||
def draw_offline_metadata(self, context: bpy.types.Context) -> None:
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Fix Metadata Strips", icon="ERROR")
|
||||
offline_metadata_strips = [
|
||||
strip
|
||||
for strip in context.selected_sequences
|
||||
if strip.kitsu.shot_id != '' and not Path(strip.filepath).is_file()
|
||||
]
|
||||
if len(offline_metadata_strips) == 0:
|
||||
text = "Fix All Missing Media"
|
||||
else:
|
||||
text = f"Fix {len(offline_metadata_strips)} Missing Media"
|
||||
box.operator(KITSU_OT_sqe_fix_metadata_strips.bl_idname, text=text)
|
||||
|
||||
@classmethod
|
||||
def poll_multi_edit(cls, context: bpy.types.Context) -> bool:
|
||||
if not prefs.session_auth(context):
|
||||
return False
|
||||
sel_shots = context.selected_sequences
|
||||
nr_of_shots = len(sel_shots)
|
||||
unvalid = [s for s in sel_shots if s.kitsu.linked or not s.kitsu.initialized]
|
||||
return bool(not unvalid and nr_of_shots > 1)
|
||||
|
||||
def draw_multi_edit(self, context: bpy.types.Context) -> None:
|
||||
"""
|
||||
Panel in sequence editor that can edit properties of multiple strips at one.
|
||||
Mostly used to quickly initialize lots of shots with an increasing counter.
|
||||
"""
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
|
||||
nr_of_shots = len(context.selected_sequences)
|
||||
noun = get_selshots_noun(nr_of_shots)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Multi Edit Metadata", icon="PROPERTIES")
|
||||
|
||||
# Sequence
|
||||
col = box.column()
|
||||
sub_row = col.row(align=True)
|
||||
sub_row.prop(context.window_manager, "selected_sequence_name", text="Sequence")
|
||||
sub_row.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
|
||||
|
||||
# Counter.
|
||||
row = box.row()
|
||||
row.prop(context.window_manager, "shot_counter_start", text="Shot Counter Start")
|
||||
row.prop(context.window_manager, "show_advanced", text="", icon="TOOL_SETTINGS")
|
||||
|
||||
if context.window_manager.show_advanced:
|
||||
# Counter.
|
||||
box.row().prop(addon_prefs, "shot_counter_digits", text="Shot Counter Digits")
|
||||
box.row().prop(addon_prefs, "shot_counter_increment", text="Shot Counter Increment")
|
||||
|
||||
# Variables.
|
||||
row = box.row(align=True)
|
||||
row.prop(
|
||||
context.window_manager,
|
||||
"var_use_custom_seq",
|
||||
text="Custom Sequence Variable",
|
||||
)
|
||||
if context.window_manager.var_use_custom_seq:
|
||||
row.prop(context.window_manager, "var_sequence_custom", text="")
|
||||
|
||||
# Project.
|
||||
row = box.row(align=True)
|
||||
row.prop(
|
||||
context.window_manager,
|
||||
"var_use_custom_project",
|
||||
text="Custom Project Variable",
|
||||
)
|
||||
if context.window_manager.var_use_custom_project:
|
||||
row.prop(context.window_manager, "var_project_custom", text="")
|
||||
|
||||
# Shot pattern.
|
||||
box.row().prop(addon_prefs, "shot_pattern", text="Shot Pattern")
|
||||
|
||||
# Preview.
|
||||
row = box.row()
|
||||
row.prop(context.window_manager, "shot_preview", text="Preview")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_sqe_multi_edit_strip.bl_idname,
|
||||
text=f"Set Metadata for {noun}",
|
||||
icon="ALIGN_LEFT",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll_push(cls, context: bpy.types.Context) -> bool:
|
||||
# If only one strip is selected and it is not init then hide panel.
|
||||
if not prefs.session_auth(context):
|
||||
return False
|
||||
|
||||
selshots = context.selected_sequences
|
||||
if not selshots:
|
||||
selshots = context.scene.sequence_editor.sequences_all
|
||||
|
||||
strips_to_meta = []
|
||||
strips_to_tb = []
|
||||
strips_to_submit = []
|
||||
|
||||
for s in selshots:
|
||||
if s.kitsu.linked:
|
||||
strips_to_tb.append(s)
|
||||
strips_to_meta.append(s)
|
||||
|
||||
elif s.kitsu.initialized and (s.kitsu.manual_shot_name != "" or s.kitsu.shot_name):
|
||||
strips_to_submit.append(s)
|
||||
|
||||
return bool(strips_to_meta or strips_to_tb or strips_to_submit)
|
||||
|
||||
def draw_push(self, context: bpy.types.Context) -> None:
|
||||
"""
|
||||
Panel that shows operator to sync sequence editor metadata with backend.
|
||||
"""
|
||||
nr_of_shots = len(context.selected_sequences)
|
||||
layout = self.layout
|
||||
strip = context.scene.sequence_editor.active_strip
|
||||
|
||||
selshots = context.selected_sequences
|
||||
if not selshots:
|
||||
selshots = context.scene.sequence_editor.sequences_all
|
||||
|
||||
strips_to_meta = []
|
||||
strips_to_tb = []
|
||||
strips_to_submit = []
|
||||
strips_to_delete = []
|
||||
|
||||
for s in selshots:
|
||||
if s.kitsu.linked:
|
||||
strips_to_tb.append(s)
|
||||
strips_to_meta.append(s)
|
||||
strips_to_delete.append(s)
|
||||
|
||||
elif s.kitsu.initialized:
|
||||
if s.kitsu.shot_name and s.kitsu.sequence_name:
|
||||
strips_to_submit.append(s)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Push", icon="EXPORT")
|
||||
# Special case if one shot is selected and it is init but not linked
|
||||
# shows the operator but it is not enabled until user types in required metadata.
|
||||
if nr_of_shots == 1 and not strip.kitsu.linked:
|
||||
# New operator.
|
||||
row = box.row()
|
||||
col = row.column(align=True)
|
||||
col.operator(
|
||||
KITSU_OT_sqe_push_new_shot.bl_idname,
|
||||
text="Submit New Shot",
|
||||
icon="ADD",
|
||||
)
|
||||
return
|
||||
|
||||
# Either way no selection one selection but linked or multiple.
|
||||
|
||||
# Metadata operator.
|
||||
row = box.row()
|
||||
if strips_to_meta:
|
||||
col = row.column(align=True)
|
||||
noun = get_selshots_noun(len(strips_to_meta), prefix=f"{len(strips_to_meta)}")
|
||||
col.operator(
|
||||
KITSU_OT_sqe_push_shot_meta.bl_idname,
|
||||
text=f"Metadata {noun}",
|
||||
icon="ALIGN_LEFT",
|
||||
)
|
||||
|
||||
# Thumbnail and seqeunce renderoperator.
|
||||
if strips_to_tb:
|
||||
# Upload thumbnail op.
|
||||
noun = get_selshots_noun(len(strips_to_tb), prefix=f"{len(strips_to_meta)}")
|
||||
split = col.split(factor=0.7, align=True)
|
||||
split.operator(
|
||||
KITSU_OT_sqe_push_render_still.bl_idname,
|
||||
text=f"Render Still {noun}",
|
||||
icon="IMAGE_DATA",
|
||||
)
|
||||
# Select task types op.
|
||||
noun = context.scene.kitsu.task_type_thumbnail_name or "Select Task Type"
|
||||
split.operator(
|
||||
KITSU_OT_sqe_set_thumbnail_task_type.bl_idname,
|
||||
text=noun,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
|
||||
# Sqe render op.
|
||||
noun = get_selshots_noun(len(strips_to_tb), prefix=f"{len(strips_to_meta)}")
|
||||
split = col.split(factor=0.7, align=True)
|
||||
split.operator(
|
||||
KITSU_OT_sqe_push_render.bl_idname,
|
||||
text=f"Render Movie {noun}",
|
||||
icon="IMAGE_DATA",
|
||||
)
|
||||
# Select task types op.
|
||||
noun = context.scene.kitsu.task_type_sqe_render_name or "Select Task Type"
|
||||
split.operator(
|
||||
KITSU_OT_sqe_set_sqe_render_task_type.bl_idname,
|
||||
text=noun,
|
||||
icon="DOWNARROW_HLT",
|
||||
)
|
||||
|
||||
# Submit operator.
|
||||
if nr_of_shots > 0:
|
||||
if strips_to_submit:
|
||||
noun = get_selshots_noun(len(strips_to_submit), prefix=f"{len(strips_to_submit)}")
|
||||
row = box.row()
|
||||
col = row.column(align=True)
|
||||
col.operator(
|
||||
KITSU_OT_sqe_push_new_shot.bl_idname,
|
||||
text=f"Submit {noun}",
|
||||
icon="ADD",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll_pull(cls, context: bpy.types.Context) -> bool:
|
||||
if not prefs.session_auth(context):
|
||||
return False
|
||||
|
||||
selshots = context.selected_sequences
|
||||
all_shots = context.scene.sequence_editor.sequences_all
|
||||
|
||||
if not selshots: # Pull entire edit.
|
||||
return True
|
||||
|
||||
strips_to_meta_sel = [s for s in selshots if s.kitsu.linked]
|
||||
strips_to_meta_all = [s for s in all_shots if s.kitsu.linked]
|
||||
|
||||
if not selshots:
|
||||
return bool(strips_to_meta_all)
|
||||
return bool(strips_to_meta_sel)
|
||||
|
||||
def draw_pull(self, context: bpy.types.Context) -> None:
|
||||
"""
|
||||
Panel that shows operator to sync sequence editor metadata with backend.
|
||||
"""
|
||||
|
||||
selshots = context.selected_sequences
|
||||
if not selshots:
|
||||
selshots = context.scene.sequence_editor.sequences_all
|
||||
|
||||
strips_to_meta = []
|
||||
|
||||
for s in selshots:
|
||||
if s.kitsu.linked:
|
||||
strips_to_meta.append(s)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Pull", icon="IMPORT")
|
||||
|
||||
layout = self.layout
|
||||
if strips_to_meta:
|
||||
noun = get_selshots_noun(len(strips_to_meta), prefix=f"{len(strips_to_meta)}")
|
||||
row = box.row()
|
||||
row.operator(
|
||||
KITSU_OT_sqe_pull_shot_meta.bl_idname,
|
||||
text=f"Metadata {noun}",
|
||||
icon="ALIGN_LEFT",
|
||||
)
|
||||
|
||||
if not context.selected_sequences:
|
||||
row = box.row()
|
||||
row.operator(
|
||||
KITSU_OT_sqe_pull_edit.bl_idname,
|
||||
text=f"Pull entire Edit",
|
||||
icon="FILE_MOVIE",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll_debug(cls, context: bpy.types.Context) -> bool:
|
||||
return prefs.addon_prefs_get(context).enable_debug
|
||||
|
||||
def draw_debug(self, context: bpy.types.Context) -> None:
|
||||
nr_of_shots = len(context.selected_sequences)
|
||||
noun = get_selshots_noun(nr_of_shots)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Debug", icon="MODIFIER_ON")
|
||||
|
||||
row = box.row()
|
||||
row.operator(
|
||||
KITSU_OT_sqe_debug_duplicates.bl_idname,
|
||||
text=f"Duplicates {noun}",
|
||||
icon="MODIFIER_ON",
|
||||
)
|
||||
row = box.row()
|
||||
row.operator(
|
||||
KITSU_OT_sqe_debug_not_linked.bl_idname,
|
||||
text=f"Not Linked {noun}",
|
||||
icon="MODIFIER_ON",
|
||||
)
|
||||
row = box.row()
|
||||
row.operator(
|
||||
KITSU_OT_sqe_debug_multi_project.bl_idname,
|
||||
text=f"Multi Projects {noun}",
|
||||
icon="MODIFIER_ON",
|
||||
)
|
||||
|
||||
def draw_media(self, context: bpy.types.Context) -> None:
|
||||
sel_metadata_strips = [strip for strip in context.selected_sequences if strip.kitsu.linked]
|
||||
|
||||
noun = get_selshots_noun(len(sel_metadata_strips), prefix=f"{len(sel_metadata_strips)}")
|
||||
playblast = "Playblast" if len(sel_metadata_strips) <= 1 else "Playblasts"
|
||||
|
||||
sequence = "Sequence" if len(sel_metadata_strips) <= 1 else "Sequences"
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Media", icon="RENDER_ANIMATION")
|
||||
box.operator(
|
||||
KITSU_OT_sqe_import_playblast.bl_idname,
|
||||
text=f"Import {noun} {playblast}",
|
||||
icon="FILE_MOVIE",
|
||||
)
|
||||
box.operator(
|
||||
KITSU_OT_sqe_import_image_sequence.bl_idname,
|
||||
text=f"Import {noun} Image {sequence}",
|
||||
icon="RENDER_RESULT",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_PT_sqe_general_tools(bpy.types.Panel):
|
||||
"""
|
||||
Panel in sequence editor that shows tools that don't relate directly to Kitsu
|
||||
"""
|
||||
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "General Tools"
|
||||
bl_space_type = "SEQUENCE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 30
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not context_core.is_edit_context():
|
||||
return False
|
||||
selshots = context.selected_sequences
|
||||
|
||||
sqe = context.scene.sequence_editor
|
||||
if not sqe:
|
||||
return False
|
||||
|
||||
if not selshots:
|
||||
selshots = context.scene.sequence_editor.sequences_all
|
||||
movie_strips = [s for s in selshots if s.type == "MOVIE"]
|
||||
return bool(movie_strips)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
active_strip = context.scene.sequence_editor.active_strip
|
||||
selshots = context.selected_sequences
|
||||
if not selshots:
|
||||
selshots = context.scene.sequence_editor.sequences_all
|
||||
|
||||
strips_to_update_media = []
|
||||
|
||||
for s in selshots:
|
||||
if s.type == "MOVIE":
|
||||
strips_to_update_media.append(s)
|
||||
|
||||
# Create box.
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="General", icon="MODIFIER")
|
||||
|
||||
# Scan for outdated media and reset operator.
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
KITSU_OT_sqe_scan_for_media_updates.bl_idname,
|
||||
text=f"Check media update for {len(strips_to_update_media)} {'strip' if len(strips_to_update_media) == 1 else 'strips'}",
|
||||
)
|
||||
row.operator(KITSU_OT_sqe_clear_update_indicators.bl_idname, text="", icon="X")
|
||||
|
||||
# Up down source operator. Check for to strips to accommodate linked strips
|
||||
if len(selshots) <= 2 and active_strip and active_strip.type == "MOVIE":
|
||||
row = box.row(align=True)
|
||||
row.prop(active_strip, "filepath_display", text="")
|
||||
row.operator(
|
||||
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="TRIA_UP"
|
||||
).direction = "UP"
|
||||
row.operator(
|
||||
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="TRIA_DOWN"
|
||||
).direction = "DOWN"
|
||||
row.operator(
|
||||
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="FILE_PARENT"
|
||||
).go_latest = True
|
||||
|
||||
|
||||
# ---------REGISTER ----------.
|
||||
|
||||
classes = [
|
||||
KITSU_MT_sqe_advanced_delete,
|
||||
KITSU_PT_sqe_shot_tools,
|
||||
KITSU_PT_sqe_general_tools,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,27 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
from ..tasks 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()
|
||||
@@ -1,60 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
from .. import tasks
|
||||
|
||||
import bpy
|
||||
import gazu
|
||||
from .. import cache, prefs, util
|
||||
|
||||
from ..tasks import opsdata
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
|
||||
class KITSU_OT_tasks_user_laod(bpy.types.Operator):
|
||||
"""
|
||||
Gets all tasks that the current logged in user is assgined to
|
||||
"""
|
||||
|
||||
bl_idname = "kitsu.tasks_user_laod"
|
||||
bl_label = "Tasks Load"
|
||||
bl_property = "enum_prop"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return prefs.session_auth(context)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
tasks_coll_prop = addon_prefs.tasks
|
||||
active_user = cache.user_active_get()
|
||||
|
||||
# load tasks this also updates the collection property
|
||||
cache.load_user_all_tasks(context)
|
||||
|
||||
util.ui_redraw()
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Fetched {len(tasks_coll_prop.items())} tasks for {active_user.full_name}",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ---------REGISTER ----------
|
||||
|
||||
classes = [KITSU_OT_tasks_user_laod]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,12 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..logger import LoggerFactory
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
@@ -1,90 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from .. import prefs, cache
|
||||
from .ops import KITSU_OT_tasks_user_laod
|
||||
|
||||
# from ..tasks.ops import KITSU_OT_session_end, KITSU_OT_session_start
|
||||
|
||||
|
||||
class KITSU_PT_tasks_user(bpy.types.Panel):
|
||||
bl_category = "Kitsu"
|
||||
bl_label = "Tasks"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_order = 45
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return prefs.session_auth(context)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
tasks_coll_prop = addon_prefs.tasks
|
||||
active_user = cache.user_active_get()
|
||||
split_factor = 0.225
|
||||
split_factor_small = 0.95
|
||||
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text=active_user.full_name, icon="CHECKBOX_HLT")
|
||||
|
||||
# Detect Context
|
||||
row.operator(
|
||||
KITSU_OT_tasks_user_laod.bl_idname,
|
||||
icon="FILE_REFRESH",
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
# uilist
|
||||
row = box.row(align=True)
|
||||
row.template_list(
|
||||
"KITSU_UL_tasks_user",
|
||||
"",
|
||||
addon_prefs,
|
||||
"tasks",
|
||||
context.window_manager.kitsu,
|
||||
"tasks_index",
|
||||
rows=5,
|
||||
type="DEFAULT",
|
||||
)
|
||||
|
||||
|
||||
class KITSU_UL_tasks_user(bpy.types.UIList):
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon, active_data, active_propname, index
|
||||
):
|
||||
task_id = item.id
|
||||
entity_id = item.entity_id
|
||||
entity_name = item.entity_name
|
||||
task_type_id = item.task_type_id
|
||||
task_type_name = item.task_type_name
|
||||
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
layout.label(text=f"{entity_name} {task_type_name}")
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text=f"{entity_name} {task_type_name}")
|
||||
|
||||
|
||||
# ---------REGISTER ----------
|
||||
|
||||
classes = [KITSU_UL_tasks_user, KITSU_PT_tasks_user]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from . import prefs
|
||||
|
||||
|
||||
def draw_error_box(layout: bpy.types.UILayout) -> bpy.types.UILayout:
|
||||
box = layout.box()
|
||||
box.label(text="Error", icon="ERROR")
|
||||
return box
|
||||
|
||||
|
||||
def draw_error_active_project_unset(box: bpy.types.UILayout) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text="No Active Project")
|
||||
row.operator(
|
||||
"preferences.addon_show", text="Open Addon Preferences"
|
||||
).module = __package__
|
||||
|
||||
|
||||
def draw_error_invalid_playblast_root_dir(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Invalid Playblast Root Directory")
|
||||
row.operator(
|
||||
"preferences.addon_show", text="Open Addon Preferences"
|
||||
).module = __package__
|
||||
|
||||
|
||||
def draw_error_invalid_edit_export_root_dir(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Invalid Edit Export Directory")
|
||||
row.operator("preferences.addon_show", text="Open Addon Preferences").module = __package__
|
||||
|
||||
|
||||
def draw_error_frame_range_outdated(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.alert = True
|
||||
row.label(text="Frame Range Outdated")
|
||||
row.operator("kitsu.pull_frame_range", icon="TRIA_DOWN")
|
||||
|
||||
|
||||
def draw_error_invalid_render_preset_dir(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Invalid Render Preset Directory")
|
||||
row.operator(
|
||||
"preferences.addon_show", text="Open Addon Preferences"
|
||||
).module = __package__
|
||||
|
||||
|
||||
def draw_error_invalid_project_root_dir(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Invalid Project Root Directory")
|
||||
row.operator(
|
||||
"preferences.addon_show", text="Open Addon Preferences"
|
||||
).module = __package__
|
||||
|
||||
|
||||
def draw_error_config_dir_not_exists(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Config Directory does not exist: {addon_prefs.config_dir}")
|
||||
|
||||
|
||||
def draw_error_no_active_camera(
|
||||
box: bpy.types.UILayout,
|
||||
) -> bpy.types.UILayout:
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"No active camera")
|
||||
row.prop(bpy.context.scene, "camera", text="")
|
||||
@@ -1,42 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
|
||||
from . import bkglobals
|
||||
|
||||
|
||||
def ui_redraw() -> None:
|
||||
"""
|
||||
Forces blender to redraw the UI.
|
||||
"""
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def get_version(str_value: str, format: type = str) -> Union[str, int, None]:
|
||||
match = re.search(bkglobals.VERSION_PATTERN, str_value)
|
||||
if match:
|
||||
version = match.group()
|
||||
if format == str:
|
||||
return version
|
||||
if format == int:
|
||||
return int(version.replace("v", ""))
|
||||
return None
|
||||
|
||||
|
||||
def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences:
|
||||
# NOTE: This was moved out of prefs.py to resolve a circular dependency with cache.py.
|
||||
if not context:
|
||||
context = bpy.context
|
||||
base_package = __package__
|
||||
if base_package.startswith('bl_ext'):
|
||||
# 4.2
|
||||
return context.preferences.addons[base_package].preferences
|
||||
else:
|
||||
return context.preferences.addons[base_package.split(".")[0]].preferences
|
||||
@@ -1,135 +0,0 @@
|
||||
"""External dependencies loader."""
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import logging
|
||||
from types import ModuleType
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
_my_dir = Path(__file__).parent
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
|
||||
"""Loads modules from a wheel file 'module_name*.whl'.
|
||||
|
||||
Loads `module_name`, and if submodules are given, loads
|
||||
`module_name.submodule` for each of the submodules. This allows loading all
|
||||
required modules from the same wheel in one session, ensuring that
|
||||
inter-submodule references are correct.
|
||||
|
||||
Returns the loaded modules, so [module, submodule, submodule, ...].
|
||||
"""
|
||||
|
||||
fname_prefix = _fname_prefix_from_module_name(module_name)
|
||||
wheel = _wheel_filename(fname_prefix)
|
||||
|
||||
loaded_modules: list[ModuleType] = []
|
||||
to_load = [module_name] + [f"{module_name}.{submodule}" for submodule in submodules]
|
||||
|
||||
# Load the module from the wheel file. Keep a backup of sys.path so that it
|
||||
# can be restored later. This should ensure that future import statements
|
||||
# cannot find this wheel file, increasing the separation of dependencies of
|
||||
# this add-on from other add-ons.
|
||||
with _sys_path_mod_backup(wheel):
|
||||
for modname in to_load:
|
||||
try:
|
||||
module = importlib.import_module(modname)
|
||||
except ImportError as ex:
|
||||
raise ImportError(
|
||||
"Unable to load %r from %s: %s" % (modname, wheel, ex)
|
||||
) from None
|
||||
assert isinstance(module, ModuleType)
|
||||
loaded_modules.append(module)
|
||||
_log.info("Loaded %s from %s", modname, module.__file__)
|
||||
|
||||
assert len(loaded_modules) == len(
|
||||
to_load
|
||||
), f"expecting to load {len(to_load)} modules, but only have {len(loaded_modules)}: {loaded_modules}"
|
||||
return loaded_modules
|
||||
|
||||
|
||||
def load_wheel_global(module_name: str, fname_prefix: str = "") -> ModuleType:
|
||||
"""Loads a wheel from 'fname_prefix*.whl', unless the named module can be imported.
|
||||
|
||||
This allows us to use system-installed packages before falling back to the shipped wheels.
|
||||
This is useful for development, less so for deployment.
|
||||
|
||||
If `fname_prefix` is the empty string, it will use the first package from `module_name`.
|
||||
In other words, `module_name="pkg.subpkg"` will result in `fname_prefix="pkg"`.
|
||||
"""
|
||||
|
||||
if not fname_prefix:
|
||||
fname_prefix = _fname_prefix_from_module_name(module_name)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as ex:
|
||||
_log.debug("Unable to import %s directly, will try wheel: %s", module_name, ex)
|
||||
else:
|
||||
_log.debug(
|
||||
"Was able to load %s from %s, no need to load wheel %s",
|
||||
module_name,
|
||||
module.__file__,
|
||||
fname_prefix,
|
||||
)
|
||||
return module
|
||||
|
||||
wheel = _wheel_filename(fname_prefix)
|
||||
|
||||
wheel_filepath = str(wheel)
|
||||
if wheel_filepath not in sys.path:
|
||||
sys.path.insert(0, wheel_filepath)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as ex:
|
||||
raise ImportError(
|
||||
"Unable to load %r from %s: %s" % (module_name, wheel, ex)
|
||||
) from None
|
||||
|
||||
_log.debug("Globally loaded %s from %s", module_name, module.__file__)
|
||||
return module
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _sys_path_mod_backup(wheel_file: Path) -> Iterator[None]:
|
||||
"""Temporarily inserts a wheel onto sys.path.
|
||||
|
||||
When the context exits, it restores sys.path and sys.modules, so that
|
||||
anything that was imported within the context remains unimportable by other
|
||||
modules.
|
||||
"""
|
||||
old_syspath = sys.path[:]
|
||||
old_sysmod = sys.modules.copy()
|
||||
|
||||
try:
|
||||
sys.path.insert(0, str(wheel_file))
|
||||
yield
|
||||
finally:
|
||||
# Restore without assigning a new list instance. That way references
|
||||
# held by other code will stay valid.
|
||||
sys.path[:] = old_syspath
|
||||
sys.modules.clear()
|
||||
sys.modules.update(old_sysmod)
|
||||
|
||||
|
||||
def _wheel_filename(fname_prefix: str) -> Path:
|
||||
path_pattern = "%s*.whl" % fname_prefix
|
||||
wheels: list[Path] = list(_my_dir.glob(path_pattern))
|
||||
if not wheels:
|
||||
raise RuntimeError("Unable to find wheel at %r" % path_pattern)
|
||||
|
||||
# If there are multiple wheels that match, load the last-modified one.
|
||||
# Alphabetical sorting isn't going to cut it since BAT 1.10 was released.
|
||||
def modtime(filepath: Path) -> float:
|
||||
return filepath.stat().st_mtime
|
||||
|
||||
wheels.sort(key=modtime)
|
||||
return wheels[-1]
|
||||
|
||||
|
||||
def _fname_prefix_from_module_name(module_name: str) -> str:
|
||||
return module_name.split(".", 1)[0]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,4 +0,0 @@
|
||||
# Blender SVN
|
||||
blender-svn is a Blender add-on to interact with the Subversion version control system from within Blender.
|
||||
|
||||
You can find the documentation [here](https://studio.blender.org/tools/addons/blender_svn).
|
||||
@@ -1,76 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
import bpy
|
||||
|
||||
from . import (
|
||||
props,
|
||||
repository,
|
||||
operators,
|
||||
threaded,
|
||||
ui,
|
||||
prefs,
|
||||
svn_info,
|
||||
)
|
||||
|
||||
bl_info = {
|
||||
"name": "Blender SVN",
|
||||
"author": "Demeter Dzadik, Paul Golter",
|
||||
"description": "An SVN (Subversion) interface built directly into Blender",
|
||||
"blender": (3, 1, 0),
|
||||
"version": (1, 0, 3),
|
||||
"location": "View3D",
|
||||
"warning": "",
|
||||
"doc_url": "",
|
||||
"tracker_url": "https://projects.blender.org/studio/blender-studio-tools/src/branch/main/scripts-blender/addons/blender_svn",
|
||||
"category": "Generic",
|
||||
}
|
||||
|
||||
|
||||
modules = [
|
||||
props,
|
||||
operators,
|
||||
repository,
|
||||
threaded,
|
||||
ui,
|
||||
prefs,
|
||||
svn_info,
|
||||
]
|
||||
|
||||
|
||||
def register_unregister_modules(modules, register: bool):
|
||||
"""Recursively register or unregister modules by looking for either
|
||||
un/register() functions or lists named `registry` which should be a list of
|
||||
registerable classes.
|
||||
"""
|
||||
register_func = bpy.utils.register_class if register else bpy.utils.unregister_class
|
||||
|
||||
for m in modules:
|
||||
if register:
|
||||
importlib.reload(m)
|
||||
if hasattr(m, 'registry'):
|
||||
for c in m.registry:
|
||||
try:
|
||||
register_func(c)
|
||||
except Exception as e:
|
||||
un = 'un' if not register else ''
|
||||
print(f"Warning: Failed to {un}register class: {c.__name__}")
|
||||
print(e)
|
||||
|
||||
if hasattr(m, 'modules'):
|
||||
register_unregister_modules(m.modules, register)
|
||||
|
||||
if register and hasattr(m, 'register'):
|
||||
m.register()
|
||||
elif hasattr(m, 'unregister'):
|
||||
m.unregister()
|
||||
|
||||
|
||||
def register():
|
||||
register_unregister_modules(modules, True)
|
||||
|
||||
|
||||
def unregister():
|
||||
register_unregister_modules(modules, False)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user