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
7 changed files with 156 additions and 122 deletions
Showing only changes of commit 43ac8cd7b0 - Show all commits

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,7 @@ 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/>
**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_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

@ -345,32 +345,24 @@ class KITSU_OT_playblast_set_version(bpy.types.Operator):
class KITSU_OT_push_frame_range(bpy.types.Operator):
bl_idname = "kitsu.push_frame_range"
bl_label = "Push Frame Range Offset"
bl_label = "Push Frame Start"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Adjusts the start frame to set it outside of the global value set for productionPush metadata via 3d_offset."
_shot = None
kitsu_offset: bpy.props.IntProperty(name="Frame Range Offset", default=0)
bl_description = "Adjusts the start frame of animation file."
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
col = layout.column(align=True)
col.label(text="Choose an offset value for this animation file's frame range.")
col.label(
text="This will affect the results of 'Update Frame Range'", icon="ERROR"
)
col.prop(self, "kitsu_offset")
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_offset"] = self.kitsu_offset
shot.data["3d_start"] = context.scene.frame_start
shot.update()
self.report({"INFO"}, f"Updated frame range offset {shot.data['3d_offset']}")
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]:
self.kitsu_offset = context.scene.frame_start - bkglobals.FRAME_START
return context.window_manager.invoke_props_dialog(self, width=500)
@ -390,15 +382,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"]) + int(active_shot.data["3d_offset"])
frame_out = int(active_shot.data["3d_out"]) + int(active_shot.data["3d_offset"])
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 (
@ -462,14 +454,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"]) + int(active_shot.data["3d_offset"])
frame_out = int(active_shot.data["3d_out"]) + int(active_shot.data["3d_offset"])
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

View File

@ -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:
@ -149,6 +162,7 @@ class KitsuConnector(Connector):
project = cache.project_active_get()
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(
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}
)
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(
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,
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]
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,7 +5,10 @@ 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
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
@ -39,17 +42,18 @@ def editorial_export_get_latest(context:bpy.types.Context, shot) -> list[bpy.typ
# 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
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
strip.frame_start = (
-frame_in + (strip_frame_start * 2) + frame_3d_offset + edit_export_offset
)
return new_strips
def editorial_export_check_latest(context: bpy.types.Context):
"""Find latest export in editorial export directory"""
addon_prefs = prefs.addon_prefs_get(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,17 +3,18 @@ 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]:
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(
@ -24,6 +25,7 @@ class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator):
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)

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,11 @@ logger = LoggerFactory.getLogger()
def shot_meta(strip: bpy.types.Sequence, 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_in"] = strip.kitsu_frame_start
shot.data["3d_out"] = strip.kitsu_frame_end
shot.nb_frames = strip.frame_final_duration
shot.data["fps"] = bkglobals.FPS
@ -59,7 +56,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 +65,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.