Blender Kitsu: Add Operator to Push Frame Start #98

Merged
Nick Alberelli merged 6 commits from feature/3d-offset-kitsu into main 2023-06-27 15:35:00 +02:00
8 changed files with 195 additions and 116 deletions

View File

@ -161,7 +161,7 @@ In the `Push` panel you will find all the operators that push data to Kitsu. <br
>**Metadata**: Pushes metadata of shot: sequence, shot name, frame range, sequence_color
>>**Note**: Global edit frame range will be saved in `"frame_in"` `"frame_out"` kitsu shot attribute <br/>
The actual shot frame range (starting at 101) will be saved in `["data"]["3d_in"] and `["data"]["3d_out"] kitsu shot attribute <br/>
The actual shot frame range (starting at 101) will be saved in `["data"]["3d_start"]` kitsu shot attribute <br/>
>**Thumbnails**: Renders a thumbnail of the selected shots (will be saved to the `Thumbnail Directory` -> see addon preferences) and uploads it to Kitsu. Thumbnails are linked to a task in Kitsu. So you can select the Task Type for which you want to upload the thumbnail with the `Set Thumbnail Task Type` operator. <br/>
If you select multiple metastrips it will always use the middle frame to create the thumbnail. If you have only one selected it will use the frame which is under the cursor (it curser is inside shot range). <br/>
@ -243,7 +243,8 @@ The animation tools will show up when you selected a `Task Type` with the name `
![image info](/media/addons/blender_kitsu/context_animation_tools.jpg)
>**Create Playblast**: Will create a openGL viewport render of the viewport from which the operator was executed and uploads it to Kitsu. The `+` button increments the version of the playblast. If you would override an older version you will see a warning before the filepath. The `directory` button will open a file browser in the playblast directory. The playblast will be uploaded to the `Animation` Task Type of the active shot that was set in the `Context Browser`. The web browser will be opened after the playblast and should point to the respective shot on Kitsu. <br/>
**Update Frame Range**: Will pull the frame range of the active shot from Kitsu and apply it to the scene. It will use the `['data']['3d_in']` and `['data']['3d_out']` attribute of the Kitsu shot. <br/>
**Push Frame Start**: Will Push the current scene's frame start to Kitsu. This will set the `['data']['3d_start]` attribute of the Kitsu shot.
**Pull Frame Range**: Will pull the frame range of the active shot from Kitsu and apply it to the scene. It will use read `['data']['3d_start]` attribute of the Kitsu shot. <br/>
**Update Output Collection**: Blender Studio Pipeline specific operator. <br/>
**Duplicate Collection**: Blender Studio Pipeline specific operator. <br/>
**Check Action Names**: Blender Studio Pipeline specific operator. <br/>

View File

@ -59,11 +59,11 @@ class KITSU_PT_vi3d_anim_tools(bpy.types.Panel):
box = layout.box()
box.label(text="Scene", icon="SCENE_DATA")
# Pull frame range.
row = box.row(align=True)
row.operator(
col = box.column(align=True)
col.operator("kitsu.push_frame_range", icon="TRIA_UP")
col.operator(
"kitsu.pull_frame_range",
icon="FILE_REFRESH",
icon="TRIA_DOWN",
)
# Update output collection.

View File

@ -25,6 +25,8 @@ from typing import Dict, List, Set, Optional, Tuple, Any
import bpy
from bpy.app.handlers import persistent
from blender_kitsu import bkglobals
from blender_kitsu import (
cache,
util,
@ -340,9 +342,36 @@ class KITSU_OT_playblast_set_version(bpy.types.Operator):
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."
@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: {context.scene.frame_start}", icon="ERROR")
def execute(self, context: bpy.types.Context) -> Set[str]:
shot = cache.shot_active_pull_update()
shot.data["3d_start"] = context.scene.frame_start
shot.update()
self.report({"INFO"}, f"Updated frame range offset {context.scene.frame_start}")
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
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 = "Update Frame Range"
bl_label = "Pull Frame Range"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Pulls frame range of active shot from the server "
@ -356,15 +385,15 @@ class KITSU_OT_pull_frame_range(bpy.types.Operator):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_shot = cache.shot_active_pull_update()
if "3d_in" not in active_shot.data or "3d_out" not in active_shot.data:
if "3d_start" not in active_shot.data:
self.report(
{"ERROR"},
f"Failed to pull frame range. Shot {active_shot.name} missing '3d_in', '3d_out' attribute on server",
f"Failed to pull frame range. Shot {active_shot.name} missing '3d_start'.",
)
return {"CANCELLED"}
frame_in = int(active_shot.data["3d_in"])
frame_out = int(active_shot.data["3d_out"])
frame_in = int(active_shot.data["3d_start"])
frame_out = int(active_shot.data["3d_start"]) + int(active_shot.nb_frames) - 1
# Check if current frame range matches the one for active shot.
if (
@ -428,16 +457,14 @@ def load_post_handler_check_frame_range(dummy: Any) -> None:
# Pull update for shot.
cache.shot_active_pull_update()
if "3d_in" not in active_shot.data or "3d_out" not in active_shot.data:
if "3d_start" not in active_shot.data:
logger.warning(
"Failed to check frame range. Shot %s missing '3d_in', '3d_out' attribute on server",
"Failed to check frame range. Shot %s missing '3d_start' attribute on server",
active_shot.name,
)
return
frame_in = int(active_shot.data["3d_in"])
frame_out = int(active_shot.data["3d_out"])
frame_in = int(active_shot.data["3d_start"])
frame_out = int(active_shot.data["3d_start"]) + int(active_shot.nb_frames) - 1
if (
frame_in == bpy.context.scene.frame_start
and frame_out == bpy.context.scene.frame_end
@ -478,6 +505,7 @@ classes = [
KITSU_OT_playblast_set_version,
KITSU_OT_playblast_increment_playblast_version,
KITSU_OT_pull_frame_range,
KITSU_OT_push_frame_range,
]

View File

@ -25,8 +25,8 @@ from blender_kitsu.shot_builder.task_type import TaskType
from blender_kitsu.shot_builder.render_settings import RenderSettings
from blender_kitsu.shot_builder.connectors.connector import Connector
import requests
from blender_kitsu import cache
from blender_kitsu.gazu.asset import all_assets_for_shot
from blender_kitsu import cache
from blender_kitsu.gazu.asset import all_assets_for_shot
from blender_kitsu.gazu.shot import all_shots_for_project, all_sequences_for_project
import typing
@ -43,16 +43,19 @@ class KitsuPreferences(bpy.types.PropertyGroup):
backend: bpy.props.StringProperty( # type: ignore
name="Server URL",
description="Kitsu server address",
default="https://kitsu.blender.cloud/api")
default="https://kitsu.blender.cloud/api",
)
username: bpy.props.StringProperty( # type: ignore
name="Username",
description="Username to connect to Kitsu",)
description="Username to connect to Kitsu",
)
password: bpy.props.StringProperty( # type: ignore
name="Password",
description="Password to connect to Kitsu",
subtype='PASSWORD',)
subtype='PASSWORD',
)
def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.label(text="Kitsu")
@ -63,10 +66,11 @@ class KitsuPreferences(bpy.types.PropertyGroup):
def _validate(self):
if not (self.backend and self.username and self.password):
raise KitsuException(
"Kitsu connector has not been configured in the add-on preferences")
"Kitsu connector has not been configured in the add-on preferences"
)
class KitsuDataContainer():
class KitsuDataContainer:
def __init__(self, data: typing.Dict[str, typing.Optional[str]]):
self._data = data
@ -109,7 +113,17 @@ class KitsuSequenceRef(ShotRef):
class KitsuShotRef(ShotRef):
def __init__(self, kitsu_id: str, name: str, code: str, frame_start: int, frames: int, frame_end: int, frames_per_second: float, sequence: KitsuSequenceRef):
def __init__(
self,
kitsu_id: str,
name: str,
code: str,
frame_start: int,
frames: int,
frame_end: int,
frames_per_second: float,
sequence: KitsuSequenceRef,
):
super().__init__(name=name, code=code)
self.kitsu_id = kitsu_id
self.frame_start = frame_start
@ -137,8 +151,7 @@ class KitsuConnector(Connector):
def __get_production_data(self) -> KitsuProject:
production = cache.project_active_get()
project = KitsuProject(typing.cast(
typing.Dict[str, typing.Any], production))
project = KitsuProject(typing.cast(typing.Dict[str, typing.Any], production))
return project
def get_name(self) -> str:
@ -147,8 +160,9 @@ class KitsuConnector(Connector):
def get_task_types(self) -> typing.List[TaskType]:
project = cache.project_active_get()
task_types = project.task_types
task_types = project.task_types
import pprint
pprint.pprint(task_types)
return []
@ -156,56 +170,80 @@ class KitsuConnector(Connector):
project = cache.project_active_get()
kitsu_sequences = all_sequences_for_project(project.id)
sequence_lookup = {sequence_data['id']: KitsuSequenceRef(
kitsu_id=sequence_data['id'],
name=sequence_data['name'],
code=sequence_data['code'],
) for sequence_data in kitsu_sequences}
sequence_lookup = {
sequence_data['id']: KitsuSequenceRef(
kitsu_id=sequence_data['id'],
name=sequence_data['name'],
code=sequence_data['code'],
)
for sequence_data in kitsu_sequences
}
kitsu_shots = all_shots_for_project(project.id)
shots: typing.List[ShotRef] = []
for shot_data in kitsu_shots:
#Initialize default values
# Initialize default values
frame_start = vars.DEFAULT_FRAME_START
frame_end = 0
# shot_data['data'] can be None
if shot_data['data']:
# If 3d_in key not found use default start frame.
frame_start = int(shot_data['data'].get('3d_in', vars.DEFAULT_FRAME_START))
frame_end = int(shot_data['data'].get('3d_out', 0))
# If 3d_start key not found use default start frame.
frame_start = int(
shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START)
)
frame_end = (
int(shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START))
+ shot_data['nb_frames']
- 1
)
# If 3d_in and 3d_out available use that to calculate frames.
# If 3d_start and 3d_out available use that to calculate frames.
# If not try shot_data['nb_frames'] or 0 -> invalid.
frames = int((frame_end - frame_start + 1) if frame_end else shot_data['nb_frames'] or 0)
frames = int(
(frame_end - frame_start + 1)
if frame_end
else shot_data['nb_frames'] or 0
)
if frames < 0:
logger.error("%s duration is negative: %i. Check frame range information on Kitsu", shot_data['name'], frames)
logger.error(
"%s duration is negative: %i. Check frame range information on Kitsu",
shot_data['name'],
frames,
)
frames = 0
shots.append(KitsuShotRef(
kitsu_id=shot_data['id'],
name=shot_data['name'],
code=shot_data['code'],
frame_start=frame_start,
frames=frames,
frame_end = frame_end,
frames_per_second=24.0,
sequence=sequence_lookup[shot_data['parent_id']],
))
shots.append(
KitsuShotRef(
kitsu_id=shot_data['id'],
name=shot_data['name'],
code=shot_data['code'],
frame_start=frame_start,
frames=frames,
frame_end=frame_end,
frames_per_second=24.0,
sequence=sequence_lookup[shot_data['parent_id']],
)
)
return shots
def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]:
kitsu_assets = all_assets_for_shot(shot.kitsu_id)
return [AssetRef(name=asset_data['name'], code=asset_data['code'])
for asset_data in kitsu_assets]
kitsu_assets = all_assets_for_shot(shot.kitsu_id)
return [
AssetRef(name=asset_data['name'], code=asset_data['code'])
for asset_data in kitsu_assets
]
def get_render_settings(self, shot: Shot) -> RenderSettings:
"""
Retrieve the render settings for the given shot.
"""
project = cache.project_active_get()
return RenderSettings(width=int(project.resolution.split('x')[0]), height=int(project.resolution.split('x')[1]), frames_per_second=project.fps)
return RenderSettings(
width=int(project.resolution.split('x')[0]),
height=int(project.resolution.split('x')[1]),
frames_per_second=project.fps,
)

View File

@ -5,49 +5,53 @@ from typing import Set
from blender_kitsu import prefs
from blender_kitsu import cache
def editorial_export_get_latest(context:bpy.types.Context, shot) -> list[bpy.types.Sequence]: #TODO add info to shot
"""Loads latest export from editorial department"""
addon_prefs = prefs.addon_prefs_get(context)
strip_channel = 1
latest_file = editorial_export_check_latest(context)
if not latest_file:
return None
# Check if Kitsu server returned empty shot
if shot.get("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="FIT",
def editorial_export_get_latest(
context: bpy.types.Context, shot
) -> list[bpy.types.Sequence]: # TODO add info to shot
"""Loads latest export from editorial department"""
addon_prefs = prefs.addon_prefs_get(context)
strip_channel = 1
latest_file = editorial_export_check_latest(context)
if not latest_file:
return None
# Check if Kitsu server returned empty shot
if shot.get("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="FIT",
)
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["data"].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
)
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_in = shot["data"].get("3d_in")
frame_3d_offset = frame_3d_in - 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
return new_strips
def editorial_export_check_latest(context: bpy.types.Context):
@ -59,7 +63,10 @@ def editorial_export_check_latest(context: bpy.types.Context):
files_list = [
f
for f in edit_export_path.iterdir()
if f.is_file() and editorial_export_is_valid_edit_name(addon_prefs.edit_export_file_pattern, f.name)
if f.is_file()
and editorial_export_is_valid_edit_name(
addon_prefs.edit_export_file_pattern, f.name
)
]
if len(files_list) >= 1:
files_list = sorted(files_list, reverse=True)
@ -67,7 +74,7 @@ def editorial_export_check_latest(context: bpy.types.Context):
return None
def editorial_export_is_valid_edit_name(file_pattern:str, filename: str) -> bool:
def editorial_export_is_valid_edit_name(file_pattern: str, filename: str) -> bool:
"""Verify file name matches file pattern set in preferences"""
match = re.search(file_pattern, filename)
if match:

View File

@ -3,27 +3,29 @@ from typing import Set
from blender_kitsu.shot_builder.editorial.core import editorial_export_get_latest
from blender_kitsu import cache, gazu
class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator):
bl_idname = "asset_setup.load_latest_editorial"
bl_label = "Load Editorial Export"
bl_description = (
"Loads latest edit from shot_preview_folder "
"Shifts edit so current shot starts at 3d_in metadata shot key from Kitsu"
"Shifts edit so current shot starts at 3d_start metadata shot key from Kitsu"
)
def execute(self, context: bpy.types.Context) -> Set[str]:
def execute(self, context: bpy.types.Context) -> Set[str]:
cache_shot = cache.shot_active_get()
shot = gazu.shot.get_shot(cache_shot.id) #TODO INEFFICENT TO LOAD SHOT TWICE
shot = gazu.shot.get_shot(cache_shot.id) # TODO INEFFICENT TO LOAD SHOT TWICE
strips = editorial_export_get_latest(context, shot)
if strips is None:
self.report(
{"ERROR"}, f"No valid editorial export in editorial export path."
)
return {"CANCELLED"}
self.report({"INFO"}, f"Loaded latest edit: {strips[0].name}")
return {"FINISHED"}
classes = [
ANIM_SETUP_OT_load_latest_editorial,
]
@ -33,6 +35,7 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.utils.unregister_class(cls)

View File

@ -1866,22 +1866,22 @@ class KITSU_OT_sqe_pull_edit(bpy.types.Operator):
def _apply_strip_slip_from_shot(
self, context: bpy.types.Context, strip: bpy.types.Sequence, shot: Shot
) -> None:
if "3d_in" not in shot.data:
if "3d_start" not in shot.data:
logger.warning(
"%s no update to frame_start_offset. '3d_in' key not in shot.data",
"%s no update to frame_start_offset. '3d_start' key not in shot.data",
shot.name,
)
return
if not shot.data["3d_in"]:
if not shot.data["3d_start"]:
logger.warning(
"%s no update to frame_start_offset. '3d_in' key invalid value: %i",
"%s no update to frame_start_offset. '3d_start' key invalid value: %i",
shot.name,
shot.data["3d_in"],
shot.data["3d_start"],
)
return
# get offset
offset = strip.kitsu_frame_start - int(shot.data["3d_in"])
offset = strip.kitsu_frame_start - int(shot.data["3d_start"])
# Deselect everything.
if context.selected_sequences:

View File

@ -30,14 +30,19 @@ logger = LoggerFactory.getLogger()
def shot_meta(strip: bpy.types.Sequence, shot: Shot) -> None:
# Update shot info.
# Only set 3d_start if none is found
try:
kitsu_3d_start = shot.data["3d_start"]
except:
kitsu_3d_start = 101 # TODO REPLACE WITH BAKED GLOBAL VALUE
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_in"] = strip.kitsu_frame_start
shot.data["3d_out"] = strip.kitsu_frame_end
shot.data["3d_start"] = kitsu_3d_start
shot.nb_frames = strip.frame_final_duration
shot.data["fps"] = bkglobals.FPS
@ -59,7 +64,6 @@ def new_shot(
sequence: Sequence,
project: Project,
) -> Shot:
frame_range = (strip.frame_final_start, strip.frame_final_end)
shot = project.create_shot(
sequence,
@ -69,8 +73,6 @@ def new_shot(
frame_out=frame_range[1],
data={
"fps": bkglobals.FPS,
"3d_in": strip.kitsu_frame_start,
"3d_out": strip.kitsu_frame_end,
},
)
# Update description, no option to pass that on create.