mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Anim: Optional wrapping for timeline navigation
Add the option to make various timeline navitagion operators wrap the scene/playback range. The wrapping occurs when: - jumping 1 frame - jumping a delta of frames - jumping between keyframes - jumping between markers The wrapping ignores frames/markers outside the playback range. If the Preview Range is active the wrapping will limit to that range instead. This is managed via a playback setting on the Scene. Pull Request: https://projects.blender.org/blender/blender/pulls/161367
This commit is contained in:
parent
728c8624b3
commit
675d94bd4b
5 changed files with 159 additions and 9 deletions
|
|
@ -91,7 +91,7 @@ def playback_controls(layout, context):
|
|||
row.operator("screen.time_jump", text="", icon='FRAME_NEXT').backward = False
|
||||
row.operator("screen.frame_jump", text="", icon='FF').end = True
|
||||
|
||||
row.popover(panel="TIME_PT_jump", text="")
|
||||
row.popover(panel="TIME_PT_playback_options", text="")
|
||||
|
||||
if tool_settings:
|
||||
row = layout.row(align=True)
|
||||
|
|
@ -348,11 +348,11 @@ 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"
|
||||
class TIME_PT_playback_options(TimelinePanelButtons, Panel):
|
||||
bl_label = "Playback Options"
|
||||
bl_options = {'HIDE_HEADER'}
|
||||
bl_region_type = 'HEADER'
|
||||
bl_ui_units_x = 10
|
||||
bl_ui_units_x = 14
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
|
@ -363,6 +363,8 @@ class TIME_PT_jump(TimelinePanelButtons, Panel):
|
|||
is_sequencer = st.type == 'SEQUENCE_EDITOR' and st.view_type == 'SEQUENCER'
|
||||
scene = context.scene if not is_sequencer else context.sequencer_scene
|
||||
|
||||
layout.prop(scene, "wrap_timeline_navigation", text="Wrap Timeline Navigation")
|
||||
layout.separator()
|
||||
layout.prop(scene, "time_jump_unit", expand=True, text="Jump Unit")
|
||||
layout.prop(scene, "time_jump_delta", text="Delta")
|
||||
|
||||
|
|
@ -375,7 +377,7 @@ classes = (
|
|||
TIME_PT_keyframing,
|
||||
TIME_PT_keyframing_settings,
|
||||
TIME_PT_auto_keyframing,
|
||||
TIME_PT_jump,
|
||||
TIME_PT_playback_options,
|
||||
TIME_PT_playhead_snapping,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,12 @@ struct ScenePlaybackRange {
|
|||
/** The start frame is always less or equal the end frame. */
|
||||
int start_frame;
|
||||
int end_frame;
|
||||
|
||||
/** Check whether a given frame falls within this range (inclusive). */
|
||||
bool contains(const int frame) const
|
||||
{
|
||||
return frame >= start_frame && frame <= end_frame;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <optional>
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
|
|
@ -3860,6 +3861,26 @@ void ED_areas_do_frame_follow(bContext *C, bool center_view)
|
|||
}
|
||||
}
|
||||
|
||||
/** Wrap a frame value within the playback range using modulo arithmetic.
|
||||
*
|
||||
* If the frame is already within the range, it is returned unchanged. Otherwise,
|
||||
* the frame wraps around (e.g., going past the end jumps to the start).
|
||||
*/
|
||||
static int wrap_frame_in_range(const int frame, const ScenePlaybackRange &range)
|
||||
{
|
||||
if (range.contains(frame)) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
const int range_size = range.end_frame - range.start_frame + 1;
|
||||
const int frames_since_start = frame - range.start_frame;
|
||||
int offset = frames_since_start % range_size;
|
||||
if (offset < 0) {
|
||||
offset += range_size;
|
||||
}
|
||||
return range.start_frame + offset;
|
||||
}
|
||||
|
||||
/* function to be called outside UI context, or for redo */
|
||||
static wmOperatorStatus frame_offset_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
|
|
@ -3877,7 +3898,15 @@ static wmOperatorStatus frame_offset_exec(bContext *C, wmOperator *op)
|
|||
delta += 1;
|
||||
}
|
||||
scene->r.cfra += delta;
|
||||
FRAMENUMBER_MIN_CLAMP(scene->r.cfra);
|
||||
|
||||
const bool wrap_timeline_navigation = scene->r.flag & SCER_WRAP_TIMELINE_NAVIGATION;
|
||||
if (wrap_timeline_navigation) {
|
||||
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(scene);
|
||||
scene->r.cfra = wrap_frame_in_range(scene->r.cfra, playback_range);
|
||||
}
|
||||
else {
|
||||
FRAMENUMBER_MIN_CLAMP(scene->r.cfra);
|
||||
}
|
||||
scene->r.subframe = 0.0f;
|
||||
|
||||
ED_areas_do_frame_follow(C, false);
|
||||
|
|
@ -4024,7 +4053,14 @@ static wmOperatorStatus frame_jump_delta_exec(bContext *C, wmOperator *op)
|
|||
scene->r.subframe -= subframe_offset;
|
||||
}
|
||||
|
||||
FRAMENUMBER_MIN_CLAMP(scene->r.cfra);
|
||||
const bool wrap_timeline_navigation = scene->r.flag & SCER_WRAP_TIMELINE_NAVIGATION;
|
||||
if (wrap_timeline_navigation) {
|
||||
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(scene);
|
||||
scene->r.cfra = wrap_frame_in_range(scene->r.cfra, playback_range);
|
||||
}
|
||||
else {
|
||||
FRAMENUMBER_MIN_CLAMP(scene->r.cfra);
|
||||
}
|
||||
|
||||
ED_areas_do_frame_follow(C, true);
|
||||
ed::vse::sync_active_scene_and_time_with_scene_strip(*C);
|
||||
|
|
@ -4132,6 +4168,20 @@ static void keylist_fallback_for_keyframe_jump(bContext &C, Scene *scene, AnimKe
|
|||
}
|
||||
}
|
||||
|
||||
template<typename KeyColumnIterator>
|
||||
static bool scene_to_first_key_column_in_range(Scene *scene,
|
||||
const ScenePlaybackRange playback_range,
|
||||
KeyColumnIterator key_columns)
|
||||
{
|
||||
for (const ActKeyColumn &ak_wrap : key_columns) {
|
||||
if (playback_range.contains(ak_wrap.cfra)) {
|
||||
BKE_scene_frame_set(scene, ak_wrap.cfra);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* function to be called outside UI context, or for redo */
|
||||
static wmOperatorStatus keyframe_jump_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
|
|
@ -4166,12 +4216,28 @@ static wmOperatorStatus keyframe_jump_exec(bContext *C, wmOperator *op)
|
|||
ED_keylist_prepare_for_direct_access(keylist);
|
||||
|
||||
const float cfra = BKE_scene_frame_get(scene);
|
||||
/* find matching keyframe in the right direction */
|
||||
|
||||
const bool wrap_timeline_navigation = scene->r.flag & SCER_WRAP_TIMELINE_NAVIGATION;
|
||||
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(scene);
|
||||
|
||||
/* Find matching keyframe in the right direction. */
|
||||
const ActKeyColumn *ak;
|
||||
|
||||
if (next) {
|
||||
ak = ED_keylist_find_next(keylist, cfra);
|
||||
while ((ak != nullptr) && (done == false)) {
|
||||
if (wrap_timeline_navigation) {
|
||||
/* Ignore keyframes before playback_range. */
|
||||
if (ak->cfra < playback_range.start_frame) {
|
||||
ak = ak->next;
|
||||
continue;
|
||||
}
|
||||
/* No more keyframes within playback_range. */
|
||||
if (ak->cfra > playback_range.end_frame) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfra < ak->cfra) {
|
||||
BKE_scene_frame_set(scene, ak->cfra);
|
||||
done = true;
|
||||
|
|
@ -4180,11 +4246,29 @@ static wmOperatorStatus keyframe_jump_exec(bContext *C, wmOperator *op)
|
|||
ak = ak->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrap around to the beginning of the frame range. */
|
||||
if (!done && wrap_timeline_navigation) {
|
||||
done = scene_to_first_key_column_in_range(
|
||||
scene, playback_range, *ED_keylist_listbase(keylist));
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
ak = ED_keylist_find_prev(keylist, cfra);
|
||||
while ((ak != nullptr) && (done == false)) {
|
||||
if (wrap_timeline_navigation) {
|
||||
/* Ignore keyframes after playback_range. */
|
||||
if (ak->cfra > playback_range.end_frame) {
|
||||
ak = ak->prev;
|
||||
continue;
|
||||
}
|
||||
/* No more keyframes within playback_range. */
|
||||
if (ak->cfra < playback_range.start_frame) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfra > ak->cfra) {
|
||||
BKE_scene_frame_set(scene, ak->cfra);
|
||||
done = true;
|
||||
|
|
@ -4193,6 +4277,12 @@ static wmOperatorStatus keyframe_jump_exec(bContext *C, wmOperator *op)
|
|||
ak = ak->prev;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrap around to the end of the frame range. */
|
||||
if (!done && wrap_timeline_navigation) {
|
||||
done = scene_to_first_key_column_in_range(
|
||||
scene, playback_range, ED_keylist_listbase(keylist)->items_reversed());
|
||||
}
|
||||
}
|
||||
|
||||
/* free temp stuff */
|
||||
|
|
@ -4242,6 +4332,18 @@ static void SCREEN_OT_keyframe_jump(wmOperatorType *ot)
|
|||
/** \name Jump to Marker Operator
|
||||
* \{ */
|
||||
|
||||
template<typename MarkerIterator>
|
||||
static std::optional<int> get_first_marker_in_range(const ScenePlaybackRange playback_range,
|
||||
MarkerIterator markers)
|
||||
{
|
||||
for (const TimeMarker &marker : markers) {
|
||||
if (playback_range.contains(marker.frame)) {
|
||||
return marker.frame;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/* function to be called outside UI context, or for redo */
|
||||
static wmOperatorStatus marker_jump_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
|
|
@ -4250,12 +4352,21 @@ static wmOperatorStatus marker_jump_exec(bContext *C, wmOperator *op)
|
|||
if (!scene) {
|
||||
return OPERATOR_CANCELLED;
|
||||
}
|
||||
|
||||
const bool wrap_timeline_navigation = scene->r.flag & SCER_WRAP_TIMELINE_NAVIGATION;
|
||||
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(scene);
|
||||
|
||||
int closest = scene->r.cfra;
|
||||
const bool next = RNA_boolean_get(op->ptr, "next");
|
||||
bool found = false;
|
||||
|
||||
/* find matching marker in the right direction */
|
||||
/* Find matching marker in the right direction. */
|
||||
for (TimeMarker &marker : scene->markers) {
|
||||
/* Ignore markers outside of playback_range. */
|
||||
if (wrap_timeline_navigation && !playback_range.contains(marker.frame)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
if ((marker.frame > scene->r.cfra) && (!found || closest > marker.frame)) {
|
||||
closest = marker.frame;
|
||||
|
|
@ -4270,6 +4381,26 @@ static wmOperatorStatus marker_jump_exec(bContext *C, wmOperator *op)
|
|||
}
|
||||
}
|
||||
|
||||
/* Wrap around playback range and try to look for markers again. */
|
||||
if (!found && wrap_timeline_navigation) {
|
||||
if (next) {
|
||||
if (const std::optional<int> frame = get_first_marker_in_range(playback_range,
|
||||
scene->markers))
|
||||
{
|
||||
closest = *frame;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (const std::optional<int> frame = get_first_marker_in_range(
|
||||
playback_range, scene->markers.items_reversed()))
|
||||
{
|
||||
closest = *frame;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* any success? */
|
||||
if (!found) {
|
||||
BKE_report(op->reports, RPT_INFO, "No more markers to jump to in this direction");
|
||||
|
|
|
|||
|
|
@ -714,6 +714,8 @@ enum eRender_Flag : short {
|
|||
SCER_ALLOW_PREROLL = 1 << 2,
|
||||
/** Show/use sub-frames (for checking motion blur). */
|
||||
SCER_SHOW_SUBFRAME = 1 << 3,
|
||||
/** Wrap the playhead within the playback range while navigating timeline. */
|
||||
SCER_WRAP_TIMELINE_NAVIGATION = 1 << 4,
|
||||
};
|
||||
ENUM_OPERATORS(eRender_Flag)
|
||||
|
||||
|
|
|
|||
|
|
@ -9115,6 +9115,15 @@ void RNA_def_scene(BlenderRNA *brna)
|
|||
prop, "Only Show Selected", "Only include channels relating to selected objects and data");
|
||||
RNA_def_property_update(prop, NC_SCENE | ND_FRAME, nullptr);
|
||||
|
||||
prop = RNA_def_property(srna, "wrap_timeline_navigation", PROP_BOOLEAN, PROP_NONE);
|
||||
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
|
||||
RNA_def_property_boolean_sdna(prop, nullptr, "r.flag", SCER_WRAP_TIMELINE_NAVIGATION);
|
||||
RNA_def_property_ui_text(
|
||||
prop,
|
||||
"Wrap Timeline",
|
||||
"Wrap the playhead around the playback range when navigating the timeline");
|
||||
RNA_def_property_update(prop, NC_SCENE | ND_FRAME, nullptr);
|
||||
|
||||
/* Stamp */
|
||||
prop = RNA_def_property(srna, "use_stamp_note", PROP_STRING, PROP_NONE);
|
||||
RNA_def_property_string_sdna(prop, nullptr, "r.stamp_udata");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue