From f145e1f7e21620c120ec36e1ef9ac3fe92a098ed Mon Sep 17 00:00:00 2001 From: Nika Kutsniashvili Date: Tue, 7 Oct 2025 13:43:20 +0200 Subject: [PATCH] Anim: Time jump operator Adds a new operator that jumps time by a given number of frames or seconds, forward or backward. Surprisingly, it was lacking in Blender, and prompted many users (including me) to create extensions. This PR adds two properties: `time_jump_unit` for choosing whether to jump by frames or seconds and `time_jump_delta` property that defines by how many frames or seconds the operator should jump, as well as an actual operator that changes the current frame (`screen.time_jump`). `time_jump_delta` is a float that gives users the ability to jump by half a second, for example, or by subframes. Default is set to 1 second, which translates to as many frames jump as frame rate / frame base. The operator is intentionally not bound by frame range, and can go in negative frames as well. This is very important because it's extremely common to set frame range to the current workload, but wish to see animation beyond that. Operators are added in the new footer for animation editors alongside with pop-up menu where properties can be changed. Shortcuts are also added: Ctrl+Left/Right Arrow, which was surprisingly free in Blender. Now timeline controls are: - **Right Arrow**: Next Frame - **Ctrl + Right Arrow**: Jump Forward (by default also Next Second) - **Shift + Right Arrow**: Jump to End Pull Request: https://projects.blender.org/blender/blender/pulls/140677 --- .../keyconfig/keymap_data/blender_default.py | 4 ++ scripts/startup/bl_ui/space_time.py | 24 +++++++ .../blender/blenkernel/BKE_blender_version.h | 2 +- .../blenloader/intern/versioning_500.cc | 7 +++ source/blender/editors/screen/screen_ops.cc | 63 +++++++++++++++++++ source/blender/makesdna/DNA_scene_defaults.h | 2 + source/blender/makesdna/DNA_scene_types.h | 11 +++- source/blender/makesrna/intern/rna_scene.cc | 22 +++++++ 8 files changed, 133 insertions(+), 2 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index d5da13edc19..e23e52eba61 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -3705,6 +3705,10 @@ def km_frames(params): {"properties": [("end", True)]}), ("screen.frame_jump", {"type": 'LEFT_ARROW', "value": 'PRESS', "shift": True, "repeat": True}, {"properties": [("end", False)]}), + ("screen.time_jump", {"type": 'RIGHT_ARROW', "value": 'PRESS', "ctrl": True, "repeat": True}, + {"properties": [("backward", False)]}), + ("screen.time_jump", {"type": 'LEFT_ARROW', "value": 'PRESS', "ctrl": True, "repeat": True}, + {"properties": [("backward", True)]}), ("screen.keyframe_jump", {"type": 'UP_ARROW', "value": 'PRESS', "repeat": True}, {"properties": [("next", False)]}), ("screen.keyframe_jump", {"type": 'DOWN_ARROW', "value": 'PRESS', "repeat": True}, diff --git a/scripts/startup/bl_ui/space_time.py b/scripts/startup/bl_ui/space_time.py index 9748dd1784f..dfbbb228f0b 100644 --- a/scripts/startup/bl_ui/space_time.py +++ b/scripts/startup/bl_ui/space_time.py @@ -97,6 +97,12 @@ def playback_controls(layout, context): row.operator("screen.keyframe_jump", text="", icon='NEXT_KEYFRAME').next = True row.operator("screen.frame_jump", text="", icon='FF').end = True + # Time jump + row = layout.row(align=True) + row.operator("screen.time_jump", text="", icon='FRAME_PREV').backward = True + row.operator("screen.time_jump", text="", icon='FRAME_NEXT').backward = False + row.popover(panel="TIME_PT_jump", text="") + if tool_settings: row = layout.row(align=True) row.prop(tool_settings, "use_snap_playhead", text="") @@ -289,12 +295,30 @@ class TIME_PT_auto_keyframing(TimelinePanelButtons, Panel): col.prop(tool_settings, "use_record_with_nla", text="Layered Recording") +class TIME_PT_jump(TimelinePanelButtons, Panel): + bl_label = "Time Jump" + bl_options = {'HIDE_HEADER'} + bl_region_type = 'HEADER' + bl_ui_units_x = 10 + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + scene = context.scene + + layout.prop(scene, "time_jump_unit", expand=True, text="Jump Unit") + layout.prop(scene, "time_jump_delta", text="Delta") + + ################################### classes = ( TIME_PT_playback, TIME_PT_keyframing_settings, TIME_PT_auto_keyframing, + TIME_PT_jump, TIME_PT_playhead_snapping, ) diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 242a98ff31c..c803d12d89f 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -27,7 +27,7 @@ /* Blender file format version. */ #define BLENDER_FILE_VERSION BLENDER_VERSION -#define BLENDER_FILE_SUBVERSION 101 +#define BLENDER_FILE_SUBVERSION 102 /* Minimum Blender version that supports reading file written with the current * version. Older Blender versions will test this and cancel loading the file, showing a warning to diff --git a/source/blender/blenloader/intern/versioning_500.cc b/source/blender/blenloader/intern/versioning_500.cc index aa2ca673ff9..a59c7e32505 100644 --- a/source/blender/blenloader/intern/versioning_500.cc +++ b/source/blender/blenloader/intern/versioning_500.cc @@ -3819,6 +3819,13 @@ void blo_do_versions_500(FileData *fd, Library * /*lib*/, Main *bmain) } } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 102)) { + LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) { + scene->r.time_jump_delta = 1.0f; + scene->r.time_jump_unit = 1; + } + } + /** * Always bump subversion in BKE_blender_version.h when adding versioning * code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check. diff --git a/source/blender/editors/screen/screen_ops.cc b/source/blender/editors/screen/screen_ops.cc index fd13a21e1c1..2fd8cc7a89c 100644 --- a/source/blender/editors/screen/screen_ops.cc +++ b/source/blender/editors/screen/screen_ops.cc @@ -3365,6 +3365,68 @@ static void SCREEN_OT_frame_jump(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Time Jump Operator + * \{ */ + +/* function to be called outside UI context, or for redo */ +static wmOperatorStatus frame_jump_delta_exec(bContext *C, wmOperator *op) +{ + Scene *scene = CTX_data_scene(C); + const bool backward = RNA_boolean_get(op->ptr, "backward"); + + float delta = scene->r.time_jump_delta; + + if (scene->r.time_jump_unit == SCE_TIME_JUMP_SECOND) { + delta *= scene->r.frs_sec / scene->r.frs_sec_base; + } + + int step = (int)delta; + float fraction = delta - step; + if (backward) { + scene->r.cfra -= step; + scene->r.subframe -= fraction; + } + else { + scene->r.cfra += step; + scene->r.subframe += fraction; + } + + /* Check if subframe has a non-fractional component, and roll that into cfra. */ + if (scene->r.subframe < 0.0f || scene->r.subframe >= 1.0f) { + const float subframe_offset = floorf(scene->r.subframe); + const int frame_offset = (int)subframe_offset; + scene->r.cfra += frame_offset; + scene->r.subframe -= subframe_offset; + } + + ED_areas_do_frame_follow(C, true); + + DEG_id_tag_update(&scene->id, ID_RECALC_FRAME_CHANGE); + + WM_event_add_notifier(C, NC_SCENE | ND_FRAME, scene); + + return OPERATOR_FINISHED; +} + +static void SCREEN_OT_time_jump(wmOperatorType *ot) +{ + ot->name = "Jump Time by Delta"; + ot->description = "Jump forward/backward by a given number of frames or seconds"; + ot->idname = "SCREEN_OT_time_jump"; + + ot->exec = frame_jump_delta_exec; + + ot->poll = operator_screenactive_norender; + ot->flag = OPTYPE_UNDO_GROUPED; + ot->undo_group = "Frame Change"; + + /* rna */ + RNA_def_boolean(ot->srna, "backward", false, "Backwards", "Jump backwards in time"); +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Jump to Key-Frame Operator * \{ */ @@ -6884,6 +6946,7 @@ void ED_operatortypes_screen() /* Frame changes. */ WM_operatortype_append(SCREEN_OT_frame_offset); WM_operatortype_append(SCREEN_OT_frame_jump); + WM_operatortype_append(SCREEN_OT_time_jump); WM_operatortype_append(SCREEN_OT_keyframe_jump); WM_operatortype_append(SCREEN_OT_marker_jump); diff --git a/source/blender/makesdna/DNA_scene_defaults.h b/source/blender/makesdna/DNA_scene_defaults.h index b3cf4367e02..a75a8179017 100644 --- a/source/blender/makesdna/DNA_scene_defaults.h +++ b/source/blender/makesdna/DNA_scene_defaults.h @@ -65,6 +65,8 @@ .sfra = 1, \ .efra = 250, \ .frame_step = 1, \ + .time_jump_delta = 1.0, \ + .time_jump_unit = 1, \ .xsch = 1920, \ .ysch = 1080, \ .xasp = 1, \ diff --git a/source/blender/makesdna/DNA_scene_types.h b/source/blender/makesdna/DNA_scene_types.h index ef99159ae7b..c42297d522f 100644 --- a/source/blender/makesdna/DNA_scene_types.h +++ b/source/blender/makesdna/DNA_scene_types.h @@ -971,7 +971,10 @@ typedef struct RenderData { int compositor_denoise_preview_quality; /* eCompositorDenoiseQaulity */ int compositor_denoise_final_quality; /* eCompositorDenoiseQaulity */ - char _pad6[4]; + /** Frames to jump manually. */ + float time_jump_delta; + int time_jump_unit; + char _pad10[4]; } RenderData; /** #RenderData::quality_flag */ @@ -1020,6 +1023,12 @@ typedef enum eCompositorDenoiseQaulity { SCE_COMPOSITOR_DENOISE_FAST = 2, } eCompositorDenoiseQaulity; +/** #RenderData::time_jump_unit */ +enum { + SCE_TIME_JUMP_FRAME = 0, + SCE_TIME_JUMP_SECOND = 1, +}; + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/makesrna/intern/rna_scene.cc b/source/blender/makesrna/intern/rna_scene.cc index c0e432aaefd..85ddc7f4f93 100644 --- a/source/blender/makesrna/intern/rna_scene.cc +++ b/source/blender/makesrna/intern/rna_scene.cc @@ -8651,6 +8651,12 @@ void RNA_def_scene(BlenderRNA *brna) {0, nullptr, 0, nullptr, nullptr}, }; + static const EnumPropertyItem time_jump_unit_items[] = { + {SCE_TIME_JUMP_FRAME, "FRAME", 0, "Frame", "Jump by frames"}, + {SCE_TIME_JUMP_SECOND, "SECOND", 0, "Second", "Jump by seconds"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + /* Struct definition */ srna = RNA_def_struct(brna, "Scene", "ID"); RNA_def_struct_ui_text(srna, @@ -8755,6 +8761,22 @@ void RNA_def_scene(BlenderRNA *brna) "Number of frames to skip forward while rendering/playing back each frame"); RNA_def_property_update(prop, NC_SCENE | ND_FRAME, nullptr); + prop = RNA_def_property(srna, "time_jump_unit", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, nullptr, "r.time_jump_unit"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_enum_items(prop, time_jump_unit_items); + RNA_def_property_ui_text( + prop, "Time Jump Unit", "Which unit to use for time jumps in the timeline"); + RNA_def_property_update(prop, NC_SCENE | ND_FRAME_RANGE, nullptr); + + prop = RNA_def_property(srna, "time_jump_delta", PROP_FLOAT, PROP_TIME); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_float_sdna(prop, nullptr, "r.time_jump_delta"); + RNA_def_property_range(prop, 0.1f, FLT_MAX); + RNA_def_property_ui_text( + prop, "Time Jump Delta", "Number of frames or seconds to jump forward or backward"); + RNA_def_property_update(prop, NC_SCENE | ND_FRAME_RANGE, nullptr); + prop = RNA_def_property(srna, "frame_current_final", PROP_FLOAT, PROP_TIME); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE | PROP_EDITABLE); RNA_def_property_range(prop, MINAFRAME, MAXFRAME);