From 53578d33aeaa1b9ac0cf0481b6d3537fa274d56d Mon Sep 17 00:00:00 2001 From: Ramon Klauck Date: Wed, 25 Jun 2025 02:56:55 +0200 Subject: [PATCH] VSE: Keyframing in Preview This PR makes it easier to add keyframes for strips in preview. This works same way as in 3D viewport, using keying sets. Pressing I key adds keyframe to default keying set, pressing K dhows menu with available keying sets. For VSE, location, rotation and scale properties are available for now. Other existing keying sets are not valid for VSE. Deleting keyframes and potentially adding more keying sets will be handled in separate PR. Pull Request: https://projects.blender.org/blender/blender/pulls/140107 --- scripts/modules/keyingsets_utils.py | 62 ++++++++++++++++++- .../keyconfig/keymap_data/blender_default.py | 5 ++ scripts/startup/bl_ui/space_sequencer.py | 14 +++++ scripts/startup/keyingsets_builtins.py | 14 ++--- .../blender/editors/animation/keyframing.cc | 34 ++++++++++ 5 files changed, 120 insertions(+), 9 deletions(-) diff --git a/scripts/modules/keyingsets_utils.py b/scripts/modules/keyingsets_utils.py index c4724bbf4e9..3669c76a0f9 100644 --- a/scripts/modules/keyingsets_utils.py +++ b/scripts/modules/keyingsets_utils.py @@ -44,6 +44,9 @@ def path_add_property(path, prop): # selected objects (active object must be in object mode) def RKS_POLL_selected_objects(_ksi, context): + if context.area.type == 'SEQUENCE_EDITOR': + return False + ob = context.active_object if ob: return ob.mode == 'OBJECT' @@ -53,6 +56,9 @@ def RKS_POLL_selected_objects(_ksi, context): # selected bones def RKS_POLL_selected_bones(_ksi, context): + if context.area.type == 'SEQUENCE_EDITOR': + return False + # we must be in Pose Mode, and there must be some bones selected ob = context.active_object if ob and ob.mode == 'POSE': @@ -62,9 +68,27 @@ def RKS_POLL_selected_bones(_ksi, context): # nothing selected return False +# selected vse strip + + +def RKS_POLL_selected_strip(_ksi, context): + if context.active_strip or context.selected_strips: + return True + + # nothing selected + return False + + +# selected bones, objects or strips +def RKS_POLL_selected_items(ksi, context): + return (RKS_POLL_selected_bones(ksi, context) or + RKS_POLL_selected_objects(ksi, context) or + RKS_POLL_selected_strip(ksi, context)) # selected bones or objects -def RKS_POLL_selected_items(ksi, context): + + +def RKS_POLL_selected_bones_or_objects(ksi, context): return (RKS_POLL_selected_bones(ksi, context) or RKS_POLL_selected_objects(ksi, context)) @@ -74,11 +98,17 @@ def RKS_POLL_selected_items(ksi, context): # All selected objects or pose bones, depending on which we've got. def RKS_ITER_selected_item(ksi, context, ks): + if context.area.type == 'SEQUENCE_EDITOR': + if context.selected_strips: + for strip in context.selected_strips: + ksi.generate(context, ks, strip) + return + ob = context.active_object if ob and ob.mode == 'POSE': for bone in context.selected_pose_bones: ksi.generate(context, ks, bone) - else: + elif context.selected_objects: for ob in context.selected_objects: ksi.generate(context, ks, ob) @@ -165,6 +195,17 @@ def RKS_GEN_location(_ksi, _context, ks, data): # get id-block and path info id_block, base_path, grouping = get_transform_generators_base_info(data) + if isinstance(data, bpy.types.Strip): + path_x = path_add_property(base_path, "transform.offset_x") + path_y = path_add_property(base_path, "transform.offset_y") + if grouping: + ks.paths.add(id_block, path_x, group_method='NAMED', group_name=grouping) + ks.paths.add(id_block, path_y, group_method='NAMED', group_name=grouping) + else: + ks.paths.add(id_block, path_x) + ks.paths.add(id_block, path_y) + return + # add the property name to the base path path = path_add_property(base_path, "location") @@ -181,6 +222,13 @@ def RKS_GEN_rotation(_ksi, _context, ks, data): id_block, base_path, grouping = get_transform_generators_base_info(data) # add the property name to the base path + if isinstance(data, bpy.types.Strip): + path = path_add_property(base_path, "transform.rotation") + if grouping: + ks.paths.add(id_block, path, group_method='NAMED', group_name=grouping) + else: + ks.paths.add(id_block, path) + return # rotation mode affects the property used if data.rotation_mode == 'QUATERNION': path = path_add_property(base_path, "rotation_quaternion") @@ -201,6 +249,16 @@ def RKS_GEN_scaling(_ksi, _context, ks, data): # get id-block and path info id_block, base_path, grouping = get_transform_generators_base_info(data) + if isinstance(data, bpy.types.Strip): + path_x = path_add_property(base_path, "transform.scale_x") + path_y = path_add_property(base_path, "transform.scale_y") + if grouping: + ks.paths.add(id_block, path_x, group_method='NAMED', group_name=grouping) + ks.paths.add(id_block, path_y, group_method='NAMED', group_name=grouping) + else: + ks.paths.add(id_block, path_x) + ks.paths.add(id_block, path_y) + return # add the property name to the base path path = path_add_property(base_path, "scale") diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index f25914a8569..64025affcdc 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -3234,6 +3234,11 @@ def km_sequencer_preview(params): ("sequencer.delete", {"type": 'X', "value": 'PRESS'}, None), ("sequencer.delete", {"type": 'DEL', "value": 'PRESS'}, None), + # Animation + ("anim.keyframe_insert", {"type": 'I', "value": 'PRESS'}, None), + ("anim.keying_set_active_set", {"type": 'K', "value": 'PRESS', "shift": True}, None), + ("anim.keyframe_insert_menu", {"type": 'K', "value": 'PRESS'}, {"properties": [("always_prompt", True)]}), + *_template_items_context_menu("SEQUENCER_MT_preview_context_menu", params.context_menu_event), ]) diff --git a/scripts/startup/bl_ui/space_sequencer.py b/scripts/startup/bl_ui/space_sequencer.py index be2b67b44b6..3c37b9f186e 100644 --- a/scripts/startup/bl_ui/space_sequencer.py +++ b/scripts/startup/bl_ui/space_sequencer.py @@ -975,6 +975,17 @@ class SEQUENCER_MT_strip_show_hide(Menu): layout.operator("sequencer.mute", text="Hide Unselected").unselected = True +class SEQUENCER_MT_strip_animation(Menu): + bl_label = "Animation" + + def draw(self, _context): + layout = self.layout + + layout.operator("anim.keyframe_insert", text="Insert Keyframe") + layout.operator("anim.keyframe_insert_menu", text="Insert Keyframe with Keying Set").always_prompt = True + layout.operator("anim.keying_set_active_set", text="Change Keying Set...") + + class SEQUENCER_MT_strip_input(Menu): bl_label = "Inputs" @@ -1131,6 +1142,8 @@ class SEQUENCER_MT_strip(Menu): layout.separator() layout.operator("sequencer.preview_duplicate_move", text="Duplicate") layout.separator() + layout.menu("SEQUENCER_MT_strip_animation") + layout.separator() layout.menu("SEQUENCER_MT_strip_show_hide") layout.separator() if strip and strip.type == 'TEXT': @@ -3177,6 +3190,7 @@ classes = ( SEQUENCER_MT_strip_retiming, SEQUENCER_MT_strip_text, SEQUENCER_MT_strip_show_hide, + SEQUENCER_MT_strip_animation, SEQUENCER_MT_strip_input, SEQUENCER_MT_strip_lock_mute, SEQUENCER_MT_image, diff --git a/scripts/startup/keyingsets_builtins.py b/scripts/startup/keyingsets_builtins.py index 3e5b662cc8e..77c1937eede 100644 --- a/scripts/startup/keyingsets_builtins.py +++ b/scripts/startup/keyingsets_builtins.py @@ -207,7 +207,7 @@ class BUILTIN_KSI_VisualLoc(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -225,7 +225,7 @@ class BUILTIN_KSI_VisualRot(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -243,7 +243,7 @@ class BUILTIN_KSI_VisualScaling(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -261,7 +261,7 @@ class BUILTIN_KSI_VisualLocRot(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -283,7 +283,7 @@ class BUILTIN_KSI_VisualLocScale(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -305,7 +305,7 @@ class BUILTIN_KSI_VisualLocRotScale(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item @@ -329,7 +329,7 @@ class BUILTIN_KSI_VisualRotScale(KeyingSetInfo): bl_options = {'INSERTKEY_VISUAL'} # poll - use predefined callback for selected bones/objects - poll = keyingsets_utils.RKS_POLL_selected_items + poll = keyingsets_utils.RKS_POLL_selected_bones_or_objects # iterator - use callback for selected bones/objects iterator = keyingsets_utils.RKS_ITER_selected_item diff --git a/source/blender/editors/animation/keyframing.cc b/source/blender/editors/animation/keyframing.cc index fa7f5428ac3..5bdb021c50b 100644 --- a/source/blender/editors/animation/keyframing.cc +++ b/source/blender/editors/animation/keyframing.cc @@ -10,6 +10,8 @@ #include +#include "DNA_sequence_types.h" +#include "ED_sequencer.hh" #include "MEM_guardedalloc.h" #include "BLI_string.h" @@ -207,6 +209,7 @@ static wmOperatorStatus insert_key_with_keyingset(bContext *C, wmOperator *op, K if (num_channels > 0) { /* send notifiers that keyframes have been changed */ WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr); + WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, scene); } if (confirm) { @@ -231,6 +234,25 @@ static blender::Vector construct_rna_paths(PointerRNA *ptr) eRotationModes rotation_mode; blender::Vector paths; + if (ptr->type == &RNA_Strip || RNA_struct_is_a(ptr->type, &RNA_Strip)) { + eKeyInsertChannels insert_channel_flags = eKeyInsertChannels(U.key_insert_channels); + if (insert_channel_flags & USER_ANIM_KEY_CHANNEL_LOCATION) { + paths.append({"transform.offset_x"}); + paths.append({"transform.offset_y"}); + } + if (insert_channel_flags & USER_ANIM_KEY_CHANNEL_ROTATION) { + paths.append({"transform.rotation"}); + } + if (insert_channel_flags & USER_ANIM_KEY_CHANNEL_SCALE) { + paths.append({"transform.scale_x"}); + paths.append({"transform.scale_y"}); + } + if (insert_channel_flags & USER_ANIM_KEY_CHANNEL_CUSTOM_PROPERTIES) { + paths.extend(blender::animrig::get_keyable_id_property_paths(*ptr)); + } + return paths; + } + if (ptr->type == &RNA_PoseBone) { bPoseChannel *pchan = static_cast(ptr->data); rotation_mode = eRotationModes(pchan->rotmode); @@ -284,6 +306,17 @@ static blender::Vector construct_rna_paths(PointerRNA *ptr) static bool get_selection(bContext *C, blender::Vector *r_selection) { const eContextObjectMode context_mode = CTX_data_mode_enum(C); + ScrArea *area = CTX_wm_area(C); + + if (area && area->spacetype == SPACE_SEQ) { + blender::VectorSet strips = blender::ed::vse::selected_strips_from_context(C); + for (Strip *strip : strips) { + PointerRNA ptr; + ptr = RNA_pointer_create_discrete(&CTX_data_scene(C)->id, &RNA_Strip, strip); + r_selection->append(ptr); + } + return true; + } switch (context_mode) { case CTX_MODE_OBJECT: { @@ -366,6 +399,7 @@ static wmOperatorStatus insert_key(bContext *C, wmOperator *op) } WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr); + WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, scene); return OPERATOR_FINISHED; }