diff --git a/scripts/startup/bl_ui/space_dopesheet.py b/scripts/startup/bl_ui/space_dopesheet.py index ae4ef9036a6..dfd24377649 100644 --- a/scripts/startup/bl_ui/space_dopesheet.py +++ b/scripts/startup/bl_ui/space_dopesheet.py @@ -16,6 +16,8 @@ from bl_ui.properties_data_grease_pencil import ( GreasePencil_LayerAdjustmentsPanel, GreasePencil_LayerDisplayPanel, ) +from bl_ui.space_toolsystem_common import PlayheadSnappingPanel + from rna_prop_ui import PropertyPanel @@ -216,6 +218,10 @@ class DOPESHEET_HT_header(Header): DOPESHEET_HT_editor_buttons.draw_header(context, layout) +class DOPESHEET_PT_playhead_snapping(PlayheadSnappingPanel, Panel): + bl_space_type = 'DOPESHEET_EDITOR' + + # Header for "normal" dopesheet editor modes (e.g. Dope Sheet, Action, Shape Keys, etc.) class DOPESHEET_HT_editor_buttons: @@ -277,6 +283,8 @@ class DOPESHEET_HT_editor_buttons: text="", ) + layout.popover(panel="DOPESHEET_PT_playhead_snapping") + row = layout.row(align=True) row.prop(tool_settings, "use_proportional_action", text="", icon_only=True) sub = row.row(align=True) @@ -1004,6 +1012,7 @@ classes = ( DOPESHEET_PT_grease_pencil_layer_adjustments, DOPESHEET_PT_grease_pencil_layer_relations, DOPESHEET_PT_grease_pencil_layer_display, + DOPESHEET_PT_playhead_snapping, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/space_graph.py b/scripts/startup/bl_ui/space_graph.py index 4b1d0c0bd1e..2627ca47e70 100644 --- a/scripts/startup/bl_ui/space_graph.py +++ b/scripts/startup/bl_ui/space_graph.py @@ -9,6 +9,12 @@ from bl_ui.space_dopesheet import ( dopesheet_filter, ) +from bl_ui.space_toolsystem_common import PlayheadSnappingPanel + + +class GRAPH_PT_playhead_snapping(PlayheadSnappingPanel, Panel): + bl_space_type = 'GRAPH_EDITOR' + class GRAPH_HT_header(Header): bl_space_type = 'GRAPH_EDITOR' @@ -65,6 +71,7 @@ class GRAPH_HT_header(Header): panel="GRAPH_PT_snapping", text="", ) + layout.popover(panel="GRAPH_PT_playhead_snapping") row = layout.row(align=True) row.prop(tool_settings, "use_proportional_fcurve", text="", icon_only=True) @@ -561,6 +568,7 @@ classes = ( GRAPH_PT_filters, GRAPH_PT_snapping, GRAPH_PT_driver_snapping, + GRAPH_PT_playhead_snapping, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/space_nla.py b/scripts/startup/bl_ui/space_nla.py index 76794175918..863ffb9aa75 100644 --- a/scripts/startup/bl_ui/space_nla.py +++ b/scripts/startup/bl_ui/space_nla.py @@ -10,6 +10,12 @@ from bl_ui.space_dopesheet import ( dopesheet_filter, ) +from bl_ui.space_toolsystem_common import PlayheadSnappingPanel + + +class NLA_PT_playhead_snapping(PlayheadSnappingPanel, Panel): + bl_space_type = 'NLA_EDITOR' + class NLA_HT_header(Header): bl_space_type = 'NLA_EDITOR' @@ -39,6 +45,7 @@ class NLA_HT_header(Header): panel="NLA_PT_snapping", text="", ) + layout.popover(panel="NLA_PT_playhead_snapping") class NLA_PT_snapping(Panel): @@ -408,6 +415,7 @@ classes = ( NLA_PT_filters, NLA_PT_action, NLA_PT_snapping, + NLA_PT_playhead_snapping, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/space_sequencer.py b/scripts/startup/bl_ui/space_sequencer.py index 51d3f12f728..c1142994a90 100644 --- a/scripts/startup/bl_ui/space_sequencer.py +++ b/scripts/startup/bl_ui/space_sequencer.py @@ -18,6 +18,7 @@ from bl_ui.properties_grease_pencil_common import ( ) from bl_ui.space_toolsystem_common import ( ToolActivePanelHelper, + PlayheadSnappingPanel, ) from rna_prop_ui import PropertyPanel @@ -185,6 +186,7 @@ class SEQUENCER_HT_header(Header): row.prop(tool_settings, "use_snap_sequencer", text="") sub = row.row(align=True) sub.popover(panel="SEQUENCER_PT_snapping") + layout.popover(panel="SEQUENCER_PT_playhead_snapping") layout.separator_spacer() if st.view_type in {'PREVIEW', 'SEQUENCER_PREVIEW'}: @@ -3028,6 +3030,10 @@ class SEQUENCER_PT_custom_props(SequencerButtonsPanel, PropertyPanel, Panel): bl_category = "Strip" +class SEQUENCER_PT_playhead_snapping(PlayheadSnappingPanel, Panel): + bl_space_type = 'SEQUENCE_EDITOR' + + class SEQUENCER_PT_snapping(Panel): bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'HEADER' @@ -3093,9 +3099,6 @@ class SEQUENCER_PT_sequencer_snapping(Panel): col.prop(sequencer_tool_settings, "snap_ignore_muted", text="Muted Strips") col.prop(sequencer_tool_settings, "snap_ignore_sound", text="Sound Strips") - col = layout.column(heading="Current Frame", align=True) - col.prop(sequencer_tool_settings, "use_snap_current_frame_to_strips", text="Snap to Strips") - classes = ( SEQUENCER_MT_change, @@ -3192,6 +3195,7 @@ classes = ( SEQUENCER_PT_snapping, SEQUENCER_PT_preview_snapping, SEQUENCER_PT_sequencer_snapping, + SEQUENCER_PT_playhead_snapping, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/space_toolsystem_common.py b/scripts/startup/bl_ui/space_toolsystem_common.py index 033ce31cb5c..c77e8e40f60 100644 --- a/scripts/startup/bl_ui/space_toolsystem_common.py +++ b/scripts/startup/bl_ui/space_toolsystem_common.py @@ -1239,6 +1239,32 @@ def _keymap_from_item(context, item): return None +class PlayheadSnappingPanel: + bl_region_type = 'HEADER' + bl_label = "Playhead" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + tool_settings = context.tool_settings + layout = self.layout + col = layout.column() + + col.prop(tool_settings, "use_snap_playhead") + col.prop(tool_settings, "playhead_snap_distance") + col.separator() + col.label(text="Snap Target") + col.prop(tool_settings, "snap_playhead_element", expand=True) + col.separator() + + if 'FRAME' in tool_settings.snap_playhead_element: + col.prop(tool_settings, "snap_playhead_frame_step") + if 'SECOND' in tool_settings.snap_playhead_element: + col.prop(tool_settings, "snap_playhead_second_step") + + classes = ( WM_MT_toolsystem_submenu, ) diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 49d8e5c3de9..978c1be896d 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 75 +#define BLENDER_FILE_SUBVERSION 76 /* 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_450.cc b/source/blender/blenloader/intern/versioning_450.cc index f9981a110be..4ab0d203d4a 100644 --- a/source/blender/blenloader/intern/versioning_450.cc +++ b/source/blender/blenloader/intern/versioning_450.cc @@ -4410,6 +4410,16 @@ void do_versions_after_linking_450(FileData * /*fd*/, Main *bmain) FOREACH_NODETREE_END; } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 405, 76)) { + ToolSettings toolsettings_default = *DNA_struct_default_get(ToolSettings); + LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) { + scene->toolsettings->snap_playhead_mode = toolsettings_default.snap_playhead_mode; + scene->toolsettings->snap_step_frames = toolsettings_default.snap_step_frames; + scene->toolsettings->snap_step_seconds = toolsettings_default.snap_step_seconds; + scene->toolsettings->playhead_snap_distance = toolsettings_default.playhead_snap_distance; + } + } + /** * 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/animation/CMakeLists.txt b/source/blender/editors/animation/CMakeLists.txt index def887f300d..1b4b622668f 100644 --- a/source/blender/editors/animation/CMakeLists.txt +++ b/source/blender/editors/animation/CMakeLists.txt @@ -5,6 +5,7 @@ set(INC ../asset ../include + ../space_graph ../../asset_system ../../makesrna ../../../../extern/fmtlib/include diff --git a/source/blender/editors/animation/anim_ops.cc b/source/blender/editors/animation/anim_ops.cc index 2e78cb1de81..bcab368ba6f 100644 --- a/source/blender/editors/animation/anim_ops.cc +++ b/source/blender/editors/animation/anim_ops.cc @@ -27,6 +27,7 @@ #include "BLT_translation.hh" +#include "UI_resources.hh" #include "UI_view2d.hh" #include "RNA_access.hh" @@ -36,14 +37,18 @@ #include "WM_types.hh" #include "ED_anim_api.hh" +#include "ED_keyframes_keylist.hh" +#include "ED_markers.hh" #include "ED_screen.hh" #include "ED_sequencer.hh" +#include "ED_space_graph.hh" #include "ED_time_scrub_ui.hh" #include "DEG_depsgraph.hh" #include "DEG_depsgraph_build.hh" #include "SEQ_iterator.hh" +#include "SEQ_retiming.hh" #include "SEQ_sequencer.hh" #include "SEQ_time.hh" @@ -56,6 +61,34 @@ /** \name Frame Change Operator * \{ */ +/* Persistent data to re-use during frame change modal operations. */ +class FrameChangeModalData { + /* Used for keyframe snapping. Is populated when needed and re-used so it doesn't have to be + * created on every modal call. */ + public: + AnimKeylist *keylist; + + FrameChangeModalData() + { + keylist = nullptr; + } + + ~FrameChangeModalData() + { + if (keylist) { + ED_keylist_free(keylist); + } + } +}; + +/* Point the playhead can snap to. */ +struct SnapTarget { + float pos; + /* If true, only snap if close to the point in screenspace. If false snap to this point + * regardless of screen space distance. */ + bool use_snap_treshold; +}; + /* Check if the operator can be run from the current context */ static bool change_frame_poll(bContext *C) { @@ -94,18 +127,146 @@ static bool change_frame_poll(bContext *C) return false; } -static int seq_snap_threshold_get_frame_distance(bContext *C) +/* Returns the playhead snap threshold in frames. Of course that depends on the zoom level of the + * editor. */ +static float get_snap_threshold(const ToolSettings *tool_settings, const ARegion *region) { - const int snap_distance = blender::seq::tool_settings_snap_distance_get(CTX_data_scene(C)); - const ARegion *region = CTX_wm_region(C); - return round_fl_to_int(UI_view2d_region_to_view_x(®ion->v2d, snap_distance) - - UI_view2d_region_to_view_x(®ion->v2d, 0)); + const int snap_threshold = tool_settings->playhead_snap_distance; + return UI_view2d_region_to_view_x(®ion->v2d, snap_threshold) - + UI_view2d_region_to_view_x(®ion->v2d, 0); } -static void seq_frame_snap_update_best(const int position, - const int timeline_frame, - int *r_best_frame, - int *r_best_distance) +static void ensure_change_frame_keylist(bContext *C, FrameChangeModalData &op_data) +{ + /* Only populate data once. */ + if (op_data.keylist != nullptr) { + return; + } + + ScrArea *area = CTX_wm_area(C); + + if (area->spacetype == SPACE_SEQ) { + /* Special case for the sequencer since it has retiming keys, but those have no bAnimListElem + * representation. Need to manually add entries to keylist. */ + op_data.keylist = ED_keylist_create(); + Scene *scene = CTX_data_scene(C); + + ListBase *seqbase = blender::seq::active_seqbase_get(blender::seq::editing_get(scene)); + LISTBASE_FOREACH (Strip *, strip, seqbase) { + sequencer_strip_to_keylist(*strip, *op_data.keylist, *scene); + } + ED_keylist_prepare_for_direct_access(op_data.keylist); + return; + } + + bAnimContext ac; + if (!ANIM_animdata_get_context(C, &ac)) { + BLI_assert_unreachable(); + return; + } + + ListBase anim_data = {nullptr, nullptr}; + + switch (area->spacetype) { + case SPACE_ACTION: { + const eAnimFilter_Flags filter = ANIMFILTER_DATA_VISIBLE; + ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype); + break; + } + + case SPACE_GRAPH: + anim_data = blender::ed::graph::get_editable_fcurves(ac); + break; + + default: + BLI_assert_unreachable(); + break; + } + + op_data.keylist = ED_keylist_create(); + + LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { + switch (ale->datatype) { + case ALE_FCURVE: { + FCurve *fcurve = static_cast(ale->data); + fcurve_to_keylist(ale->adt, fcurve, op_data.keylist, 0, {-FLT_MAX, FLT_MAX}, true); + break; + } + + case ALE_GPFRAME: { + gpl_to_keylist(nullptr, static_cast(ale->data), op_data.keylist); + break; + } + + case ALE_GREASE_PENCIL_CEL: { + grease_pencil_cels_to_keylist( + ale->adt, static_cast(ale->data), op_data.keylist, 0); + break; + } + + default: + break; + } + } + ANIM_animdata_freelist(&anim_data); + + ED_keylist_prepare_for_direct_access(op_data.keylist); +} + +static void append_keyframe_snap_target(bContext *C, + FrameChangeModalData &op_data, + const float timeline_frame, + blender::Vector &r_targets) +{ + ensure_change_frame_keylist(C, op_data); + const ActKeyColumn *closest_column = ED_keylist_find_closest(op_data.keylist, timeline_frame); + if (!closest_column) { + return; + } + r_targets.append({closest_column->cfra, true}); +} + +static void append_marker_snap_target(Scene *scene, + const float timeline_frame, + blender::Vector &r_targets) +{ + if (BLI_listbase_is_empty(&scene->markers)) { + /* This check needs to be here because `ED_markers_find_nearest_marker_time` returns the + * current frame if there are no markers. */ + return; + } + const float nearest_marker = ED_markers_find_nearest_marker_time(&scene->markers, + timeline_frame); + r_targets.append({nearest_marker, true}); +} + +static void append_second_snap_target(Scene *scene, + const float timeline_frame, + const int step, + blender::Vector &r_targets) +{ + const int start_frame = scene->r.sfra; + const float snap_frame = BKE_scene_frame_snap_by_seconds( + scene, step, timeline_frame - start_frame) + + start_frame; + r_targets.append({snap_frame, false}); +} + +static void append_frame_snap_target(const Scene *scene, + const float timeline_frame, + const int step, + blender::Vector &r_targets) +{ + const int start_frame = scene->r.sfra; + const float snap_frame = (round((timeline_frame - start_frame) / float(step)) * step) + + start_frame; + r_targets.append({snap_frame, false}); +} + +static void seq_frame_snap_update_best(const float position, + const float timeline_frame, + float *r_best_frame, + float *r_best_distance) { if (abs(position - timeline_frame) < *r_best_distance) { *r_best_distance = abs(position - timeline_frame); @@ -113,14 +274,15 @@ static void seq_frame_snap_update_best(const int position, } } -static int seq_frame_apply_snap(bContext *C, Scene *scene, const int timeline_frame) +static void append_sequencer_strip_snap_target(blender::Span strips, + const Scene *scene, + const float timeline_frame, + blender::Vector &r_targets) { + float best_frame = FLT_MAX; + float best_distance = FLT_MAX; - ListBase *seqbase = blender::seq::active_seqbase_get(blender::seq::editing_get(scene)); - - int best_frame = 0; - int best_distance = MAXFRAME; - for (Strip *strip : blender::seq::query_all_strips(seqbase)) { + for (Strip *strip : strips) { seq_frame_snap_update_best(blender::seq::time_left_handle_frame_get(scene, strip), timeline_frame, &best_frame, @@ -131,11 +293,230 @@ static int seq_frame_apply_snap(bContext *C, Scene *scene, const int timeline_fr &best_distance); } - if (best_distance < seq_snap_threshold_get_frame_distance(C)) { - return best_frame; + /* best_frame will be FLT_MAX if no target was found. */ + if (best_distance != FLT_MAX) { + r_targets.append({best_frame, true}); + } +} + +static void append_nla_strip_snap_target(bContext *C, + const float timeline_frame, + blender::Vector &r_targets) +{ + + bAnimContext ac; + if (!ANIM_animdata_get_context(C, &ac)) { + BLI_assert_unreachable(); } - return timeline_frame; + ListBase anim_data = {nullptr, nullptr}; + eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | + ANIMFILTER_LIST_CHANNELS | ANIMFILTER_FCURVESONLY); + ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype)); + + float best_frame = FLT_MAX; + float best_distance = FLT_MAX; + + LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { + if (ale->type != ANIMTYPE_NLATRACK) { + continue; + } + NlaTrack *track = static_cast(ale->data); + LISTBASE_FOREACH (NlaStrip *, strip, &track->strips) { + if (abs(strip->start - timeline_frame) < best_distance) { + best_distance = abs(strip->start - timeline_frame); + best_frame = strip->start; + } + if (abs(strip->end - timeline_frame) < best_distance) { + best_distance = abs(strip->end - timeline_frame); + best_frame = strip->end; + } + } + } + ANIM_animdata_freelist(&anim_data); + + /* If no strip was found, best_frame will be FLT_MAX. */ + if (best_frame != FLT_MAX) { + r_targets.append({best_frame, true}); + } +} + +/* ---- */ + +static blender::Vector seq_get_snap_targets(bContext *C, + FrameChangeModalData &op_data, + const float timeline_frame) +{ + Scene *scene = CTX_data_scene(C); + ToolSettings *tool_settings = scene->toolsettings; + + blender::Vector targets; + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_STRIPS) { + ListBase *seqbase = blender::seq::active_seqbase_get(blender::seq::editing_get(scene)); + append_sequencer_strip_snap_target( + blender::seq::query_all_strips(seqbase), scene, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_MARKERS) { + append_marker_snap_target(scene, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_KEYS) { + append_keyframe_snap_target(C, op_data, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_SECOND) { + append_second_snap_target(scene, timeline_frame, tool_settings->snap_step_seconds, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_FRAME) { + append_frame_snap_target(scene, timeline_frame, tool_settings->snap_step_frames, targets); + } + + return targets; +} + +static blender::Vector nla_get_snap_targets(bContext *C, const float timeline_frame) +{ + Scene *scene = CTX_data_scene(C); + ToolSettings *tool_settings = scene->toolsettings; + + blender::Vector targets; + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_STRIPS) { + append_nla_strip_snap_target(C, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_MARKERS) { + append_marker_snap_target(scene, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_SECOND) { + append_second_snap_target(scene, timeline_frame, tool_settings->snap_step_seconds, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_FRAME) { + append_frame_snap_target(scene, timeline_frame, tool_settings->snap_step_frames, targets); + } + + return targets; +} + +static blender::Vector action_get_snap_targets(bContext *C, + FrameChangeModalData &op_data, + const float timeline_frame) +{ + Scene *scene = CTX_data_scene(C); + ToolSettings *tool_settings = scene->toolsettings; + + blender::Vector targets; + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_MARKERS) { + append_marker_snap_target(scene, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_KEYS) { + append_keyframe_snap_target(C, op_data, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_SECOND) { + append_second_snap_target(scene, timeline_frame, tool_settings->snap_step_seconds, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_FRAME) { + append_frame_snap_target(scene, timeline_frame, tool_settings->snap_step_frames, targets); + } + + return targets; +} + +static blender::Vector graph_get_snap_targets(bContext *C, + FrameChangeModalData &op_data, + const float timeline_frame) +{ + Scene *scene = CTX_data_scene(C); + ToolSettings *tool_settings = scene->toolsettings; + + blender::Vector targets; + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_MARKERS) { + append_marker_snap_target(scene, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_KEYS) { + append_keyframe_snap_target(C, op_data, timeline_frame, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_SECOND) { + append_second_snap_target(scene, timeline_frame, tool_settings->snap_step_seconds, targets); + } + + if (tool_settings->snap_playhead_mode & SCE_SNAP_TO_FRAME) { + append_frame_snap_target(scene, timeline_frame, tool_settings->snap_step_frames, targets); + } + + return targets; +} + +/* ---- */ + +/* Returns a frame that is snapped to the closest point of interest defined by the area. If no + * point of interest is nearby, the frame is returned unmodified. */ +static float apply_frame_snap(bContext *C, FrameChangeModalData &op_data, const float frame) +{ + ScrArea *area = CTX_wm_area(C); + + blender::Vector targets; + Scene *scene = CTX_data_scene(C); + switch (area->spacetype) { + case SPACE_SEQ: + targets = seq_get_snap_targets(C, op_data, frame); + break; + case SPACE_ACTION: + targets = action_get_snap_targets(C, op_data, frame); + break; + case SPACE_GRAPH: + targets = graph_get_snap_targets(C, op_data, frame); + break; + case SPACE_NLA: + targets = nla_get_snap_targets(C, frame); + break; + + default: + break; + } + + float snap_frame = FLT_MAX; + + /* Find closest frame of all targets. */ + for (const SnapTarget &target : targets) { + if (abs(target.pos - frame) < abs(snap_frame - frame)) { + snap_frame = target.pos; + } + } + + const ARegion *region = CTX_wm_region(C); + if (abs(snap_frame - frame) < get_snap_threshold(scene->toolsettings, region)) { + return snap_frame; + } + + snap_frame = FLT_MAX; + /* No frame is close enough to the snap threshold. Hard snap to targets without a threshold. */ + for (const SnapTarget &target : targets) { + if (target.use_snap_treshold) { + continue; + } + if (abs(target.pos - frame) < abs(snap_frame - frame)) { + snap_frame = target.pos; + } + } + + if (snap_frame != FLT_MAX) { + return snap_frame; + } + + return frame; } /* Set the new frame number */ @@ -149,12 +530,8 @@ static void change_frame_apply(bContext *C, wmOperator *op, const bool always_up const float old_subframe = scene->r.subframe; if (do_snap) { - if (CTX_wm_space_seq(C) && blender::seq::editing_get(scene) != nullptr) { - frame = seq_frame_apply_snap(C, scene, frame); - } - else { - frame = BKE_scene_frame_snap_by_seconds(scene, 1.0, frame); - } + FrameChangeModalData *op_data = static_cast(op->customdata); + frame = apply_frame_snap(C, *op_data, frame); } /* set the new frame number */ @@ -224,16 +601,21 @@ static void change_frame_seq_preview_end(SpaceSeq *sseq) } } -static bool use_sequencer_snapping(bContext *C) +static bool use_playhead_snapping(bContext *C) { - if (!CTX_wm_space_seq(C)) { - return false; + Scene *scene = CTX_data_scene(C); + ScrArea *area = CTX_wm_area(C); + + if (area->spacetype == SPACE_GRAPH) { + SpaceGraph *graph_editor = static_cast(area->spacedata.first); + /* Snapping is disabled for driver mode. Need to evaluate if it makes sense there and what form + * it should take. */ + if (graph_editor->mode == SIPO_MODE_DRIVERS) { + return false; + } } - Scene *scene = CTX_data_scene(C); - short snap_flag = blender::seq::tool_settings_snap_flag_get(scene); - return (scene->toolsettings->snap_flag_seq & SCE_SNAP) && - (snap_flag & SEQ_SNAP_CURRENT_FRAME_TO_STRIPS); + return scene->toolsettings->snap_flag_playhead & SCE_SNAP; } static bool sequencer_skip_for_handle_tweak(const bContext *C, const wmEvent *event) @@ -262,6 +644,8 @@ static bool sequencer_skip_for_handle_tweak(const bContext *C, const wmEvent *ev static wmOperatorStatus change_frame_invoke(bContext *C, wmOperator *op, const wmEvent *event) { bScreen *screen = CTX_wm_screen(C); + FrameChangeModalData *op_data = MEM_new(__func__); + op->customdata = op_data; /* This check is done in case scrubbing and strip tweaking in the sequencer are bound to the same * event (e.g. RCS keymap where both are activated on left mouse press). Tweaking should take @@ -276,7 +660,7 @@ static wmOperatorStatus change_frame_invoke(bContext *C, wmOperator *op, const w */ RNA_float_set(op->ptr, "frame", frame_from_event(C, event)); - if (use_sequencer_snapping(C)) { + if (use_playhead_snapping(C)) { RNA_boolean_set(op->ptr, "snap", true); } @@ -356,7 +740,7 @@ static wmOperatorStatus change_frame_modal(bContext *C, wmOperator *op, const wm case EVT_LEFTCTRLKEY: case EVT_RIGHTCTRLKEY: /* Use Ctrl key to invert snapping in sequencer. */ - if (use_sequencer_snapping(C)) { + if (use_playhead_snapping(C)) { if (event->val == KM_RELEASE) { RNA_boolean_set(op->ptr, "snap", true); } @@ -378,10 +762,18 @@ static wmOperatorStatus change_frame_modal(bContext *C, wmOperator *op, const wm } } + WorkspaceStatus status(C); + status.item(IFACE_("Toggle Snapping"), ICON_EVENT_CTRL); + if (ret != OPERATOR_RUNNING_MODAL) { + ED_workspace_status_text(C, nullptr); bScreen *screen = CTX_wm_screen(C); screen->scrubbing = false; + FrameChangeModalData *op_data = static_cast(op->customdata); + MEM_delete(op_data); + op->customdata = nullptr; + if (RNA_boolean_get(op->ptr, "seq_solo_preview")) { SpaceSeq *sseq = CTX_wm_space_seq(C); if (sseq != nullptr) { diff --git a/source/blender/editors/animation/keyframes_keylist.cc b/source/blender/editors/animation/keyframes_keylist.cc index efe283e43ae..9677a182032 100644 --- a/source/blender/editors/animation/keyframes_keylist.cc +++ b/source/blender/editors/animation/keyframes_keylist.cc @@ -28,6 +28,7 @@ #include "DNA_mask_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" +#include "DNA_sequence_types.h" #include "BKE_fcurve.hh" #include "BKE_grease_pencil.hh" @@ -35,6 +36,8 @@ #include "ED_anim_api.hh" #include "ED_keyframes_keylist.hh" +#include "SEQ_retiming.hh" + #include "ANIM_action.hh" using namespace blender; @@ -264,6 +267,39 @@ const ActKeyColumn *ED_keylist_find_prev(const AnimKeylist *keylist, const float return prev_column; } +const ActKeyColumn *ED_keylist_find_closest(const AnimKeylist *keylist, const float cfra) +{ + BLI_assert_msg(keylist->is_runtime_initialized, + "ED_keylist_prepare_for_direct_access needs to be called before searching."); + if (ED_keylist_is_empty(keylist)) { + return nullptr; + } + if (cfra <= keylist->runtime.key_columns.first().cfra) { + return &keylist->runtime.key_columns.first(); + } + if (cfra >= keylist->runtime.key_columns.last().cfra) { + keylist->runtime.key_columns.last(); + } + const ActKeyColumn *prev = ED_keylist_find_prev(keylist, cfra); + BLI_assert_msg(prev != nullptr, + "This should exist since we checked for cfra bounds just before"); + /* This could be a nullptr though. */ + const ActKeyColumn *next = prev->next; + + if (!next) { + return prev; + } + + const float prev_delta = cfra - prev->cfra; + const float next_delta = next->cfra - cfra; + /* `prev_delta` and `next_delta` can both be 0 if the given `cfra` is exactly at a key column. */ + + if (prev_delta <= next_delta) { + return prev; + } + return next; +} + const ActKeyColumn *ED_keylist_find_any_between(const AnimKeylist *keylist, const Bounds frame_range) { @@ -589,6 +625,50 @@ static void nupdate_ak_gpframe(ActKeyColumn *ak, void *data) /* ......... */ +/* Extra struct to pass the timeline frame since the retiming key doesn't contain that and we would + * need the scene to get it. */ +struct SeqAllocateData { + const SeqRetimingKey *key; + float timeline_frame; +}; + +/* New node callback used for building ActKeyColumns from Sequencer keys. */ +static ActKeyColumn *nalloc_ak_seqframe(void *data) +{ + ActKeyColumn *ak = MEM_callocN("ActKeyColumnGPF"); + const SeqAllocateData *allocate_data = (SeqAllocateData *)data; + const SeqRetimingKey *timing_key = allocate_data->key; + + /* store settings based on state of BezTriple */ + ak->cfra = allocate_data->timeline_frame; + ak->sel = (timing_key->flag & SEQ_KEY_SELECTED) ? SELECT : 0; + ak->key_type = eBezTriple_KeyframeType::BEZT_KEYTYPE_KEYFRAME; + + /* Count keyframes in this column. */ + ak->totkey = 1; + /* Set as visible block. */ + ak->totblock = 1; + ak->block.sel = ak->sel; + ak->block.flag = 0; + + return ak; +} + +/* Node updater callback used for building ActKeyColumns from Sequencer keys. */ +static void nupdate_ak_seqframe(ActKeyColumn *ak, void *data) +{ + SeqAllocateData *allocate_data = static_cast(data); + + /* Set selection status and 'touched' status. */ + if (allocate_data->key->flag & SEQ_KEY_SELECTED) { + ak->sel = SELECT; + } + + ak->totkey++; +} + +/* ......... */ + /* New node callback used for building ActKeyColumns from GPencil frames */ static ActKeyColumn *nalloc_ak_masklayshape(void *data) { @@ -1417,3 +1497,18 @@ void mask_to_keylist(bDopeSheet * /*ads*/, MaskLayer *masklay, AnimKeylist *keyl update_keyblocks(keylist, nullptr, 0); } + +void sequencer_strip_to_keylist(const Strip &strip, AnimKeylist &keylist, Scene &scene) +{ + if (!blender::seq::retiming_is_active(&strip)) { + return; + } + keylist_reset_last_accessed(&keylist); + for (const SeqRetimingKey &retime_key : blender::seq::retiming_keys_get(&strip)) { + const float cfra = blender::seq::retiming_key_timeline_frame_get(&scene, &strip, &retime_key); + SeqAllocateData allocate_data = {&retime_key, cfra}; + keylist_add_or_update_column( + &keylist, cfra, nalloc_ak_seqframe, nupdate_ak_seqframe, &allocate_data); + } + update_keyblocks(&keylist, nullptr, 0); +} diff --git a/source/blender/editors/animation/keyframes_keylist_test.cc b/source/blender/editors/animation/keyframes_keylist_test.cc index 0f4a468bd5d..e6b7b03b0d9 100644 --- a/source/blender/editors/animation/keyframes_keylist_test.cc +++ b/source/blender/editors/animation/keyframes_keylist_test.cc @@ -154,6 +154,40 @@ TEST(keylist, find_exact) ED_keylist_free(keylist); } +TEST(keylist, find_closest) +{ + AnimKeylist *keylist = create_test_keylist(); + + { + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, -1); + EXPECT_EQ(closest->cfra, 10.0); + } + + { + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 10); + EXPECT_EQ(closest->cfra, 10.0); + } + + { + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 14.999); + EXPECT_EQ(closest->cfra, 10.0); + } + { + /* When the distance between key columns is equal, the previous column is chosen */ + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 15); + EXPECT_EQ(closest->cfra, 10.0); + } + { + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 15.001); + EXPECT_EQ(closest->cfra, 20.0); + } + { + const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 30.001); + EXPECT_EQ(closest->cfra, 30.0); + } + ED_keylist_free(keylist); +} + class KeylistSummaryTest : public testing::Test { public: Main *bmain; diff --git a/source/blender/editors/include/ED_keyframes_keylist.hh b/source/blender/editors/include/ED_keyframes_keylist.hh index d4e96282242..825ca2852a8 100644 --- a/source/blender/editors/include/ED_keyframes_keylist.hh +++ b/source/blender/editors/include/ED_keyframes_keylist.hh @@ -25,6 +25,7 @@ struct ListBase; struct MaskLayer; struct Object; struct Scene; +struct Strip; struct bAction; struct bActionGroup; struct bAnimContext; @@ -138,6 +139,7 @@ void ED_keylist_prepare_for_direct_access(AnimKeylist *keylist); const ActKeyColumn *ED_keylist_find_exact(const AnimKeylist *keylist, float cfra); const ActKeyColumn *ED_keylist_find_next(const AnimKeylist *keylist, float cfra); const ActKeyColumn *ED_keylist_find_prev(const AnimKeylist *keylist, float cfra); +const ActKeyColumn *ED_keylist_find_closest(const AnimKeylist *keylist, float cfra); const ActKeyColumn *ED_keylist_find_any_between(const AnimKeylist *keylist, const blender::Bounds frame_range); bool ED_keylist_is_empty(const AnimKeylist *keylist); @@ -280,6 +282,9 @@ void gpl_to_keylist(bDopeSheet *ads, bGPDlayer *gpl, AnimKeylist *keylist); /* Mask */ void mask_to_keylist(bDopeSheet *ads, MaskLayer *masklay, AnimKeylist *keylist); +/* Sequencer strip data. */ +void sequencer_strip_to_keylist(const Strip &strip, AnimKeylist &keylist, Scene &scene); + /* ActKeyColumn API ---------------- */ /** Checks if #ActKeyColumn has any block data. */ diff --git a/source/blender/makesdna/DNA_scene_defaults.h b/source/blender/makesdna/DNA_scene_defaults.h index 9f9e2340c32..9af4b1506c8 100644 --- a/source/blender/makesdna/DNA_scene_defaults.h +++ b/source/blender/makesdna/DNA_scene_defaults.h @@ -379,8 +379,13 @@ .snap_node_mode = SCE_SNAP_TO_GRID, \ .snap_uv_mode = SCE_SNAP_TO_INCREMENT, \ .snap_anim_mode = SCE_SNAP_TO_FRAME, \ + .snap_playhead_mode = SCE_SNAP_TO_KEYS | SCE_SNAP_TO_STRIPS, \ + .snap_step_frames = 2, \ + .snap_step_seconds = 1, \ + .playhead_snap_distance = 20, \ .snap_flag = SCE_SNAP_TO_INCLUDE_EDITED | SCE_SNAP_TO_INCLUDE_NONEDITED, \ .snap_flag_anim = SCE_SNAP, \ + .snap_flag_playhead = 0, \ .snap_transform_mode_flag = SCE_SNAP_TRANSFORM_MODE_TRANSLATE, \ .snap_face_nearest_steps = 1, \ .snap_angle_increment_3d = DEG2RADF(5.0f), \ diff --git a/source/blender/makesdna/DNA_scene_types.h b/source/blender/makesdna/DNA_scene_types.h index f7541b82e96..b938eded0b0 100644 --- a/source/blender/makesdna/DNA_scene_types.h +++ b/source/blender/makesdna/DNA_scene_types.h @@ -1750,14 +1750,15 @@ typedef struct ToolSettings { short snap_mode; short snap_uv_mode; short snap_anim_mode; + short snap_playhead_mode; /** Generic flags (per space-type), #eSnapFlag. */ short snap_flag; short snap_flag_node; short snap_flag_seq; short snap_flag_anim; short snap_flag_driver; + short snap_flag_playhead; short snap_uv_flag; - char _pad[2]; /** Default snap source, #eSnapSourceOP. */ /** * TODO(@gfxcoder): Rename `snap_target` to `snap_source` to avoid previous ambiguity of @@ -1801,7 +1802,7 @@ typedef struct ToolSettings { char workspace_tool_type; - char _pad5[1]; + char _pad5[7]; /** * XXX: these `sculpt_paint_*` fields are deprecated, use the @@ -1845,6 +1846,11 @@ typedef struct ToolSettings { float snap_angle_increment_3d; float snap_angle_increment_3d_precision; + int16_t snap_step_seconds; + int16_t snap_step_frames; + /* Pixel threshold that needs to be crossed before the playhead is snapped to a point. */ + int playhead_snap_distance; + } ToolSettings; /** \} */ @@ -2497,10 +2503,12 @@ ENUM_OPERATORS(eSnapTargetOP, SCE_SNAP_TARGET_NOT_NONEDITED) typedef enum eSnapMode { SCE_SNAP_TO_NONE = 0, - /** #ToolSettings::snap_anim_mode */ + /** #ToolSettings::snap_anim_mode and #ToolSettings::snap_playhead_mode. */ SCE_SNAP_TO_FRAME = (1 << 0), SCE_SNAP_TO_SECOND = (1 << 1), SCE_SNAP_TO_MARKERS = (1 << 2), + SCE_SNAP_TO_KEYS = (1 << 3), + SCE_SNAP_TO_STRIPS = (1 << 4), /** #ToolSettings::snap_mode and #ToolSettings::snap_node_mode and #ToolSettings.snap_uv_mode */ SCE_SNAP_TO_POINT = (1 << 0), diff --git a/source/blender/makesrna/intern/rna_scene.cc b/source/blender/makesrna/intern/rna_scene.cc index d31a6451be8..2d2876a264f 100644 --- a/source/blender/makesrna/intern/rna_scene.cc +++ b/source/blender/makesrna/intern/rna_scene.cc @@ -193,6 +193,15 @@ static const EnumPropertyItem snap_uv_element_items[] = { {0, nullptr, 0, nullptr, nullptr}, }; +const EnumPropertyItem rna_enum_snap_playhead_element_items[] = { + {SCE_SNAP_TO_FRAME, "FRAME", 0, "Frames", "Snap to frame increments"}, + {SCE_SNAP_TO_SECOND, "SECOND", 0, "Seconds", "Snap to second increments"}, + {SCE_SNAP_TO_MARKERS, "MARKER", 0, "Markers", "Snap to markers"}, + {SCE_SNAP_TO_KEYS, "KEY", 0, "Keyframes", "Snap to keyframes"}, + {SCE_SNAP_TO_STRIPS, "Strip", 0, "Strips", "Snap to Strips"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + static const EnumPropertyItem rna_enum_scene_display_aa_methods[] = { {SCE_DISPLAY_AA_OFF, "OFF", @@ -3696,6 +3705,39 @@ static void rna_def_tool_settings(BlenderRNA *brna) RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_UNIT); RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr); /* header redraw */ + prop = RNA_def_property(srna, "use_snap_playhead", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "snap_flag_playhead", SCE_SNAP); + RNA_def_property_flag(prop, PROP_DEG_SYNC_ONLY); + RNA_def_property_ui_text(prop, "Use Snapping", "Snap playhead when scrubbing"); + RNA_def_property_boolean_default(prop, false); + RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr); + + prop = RNA_def_property(srna, "snap_playhead_element", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_bitflag_sdna(prop, nullptr, "snap_playhead_mode"); + RNA_def_property_flag(prop, PROP_DEG_SYNC_ONLY); + RNA_def_property_flag(prop, PROP_ENUM_FLAG); + RNA_def_property_enum_items(prop, rna_enum_snap_playhead_element_items); + RNA_def_property_ui_text(prop, "Snap Playhead Element", "Type of element to snap to"); + RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_UNIT); + RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr); + + prop = RNA_def_property(srna, "snap_playhead_frame_step", PROP_INT, PROP_NONE); + RNA_def_property_int_sdna(prop, nullptr, "snap_step_frames"); + RNA_def_property_range(prop, 1, 32768); + RNA_def_property_ui_text(prop, "Frame Step", "At which interval to snap to frames"); + RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr); + + prop = RNA_def_property(srna, "snap_playhead_second_step", PROP_INT, PROP_NONE); + RNA_def_property_int_sdna(prop, nullptr, "snap_step_seconds"); + RNA_def_property_ui_text(prop, "Second Step", "At which interval to snap to seconds"); + RNA_def_property_range(prop, 1, 32768); + RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr); + + prop = RNA_def_property(srna, "playhead_snap_distance", PROP_INT, PROP_PIXEL); + RNA_def_property_int_sdna(prop, nullptr, "playhead_snap_distance"); + RNA_def_property_ui_range(prop, 1, 100, 1, 1); + RNA_def_property_ui_text(prop, "Snap Distance", "Maximum distance for snapping in pixels"); + /* image editor uses its own set of snap modes */ prop = RNA_def_property(srna, "snap_uv_element", PROP_ENUM, PROP_NONE); RNA_def_property_enum_bitflag_sdna(prop, nullptr, "snap_uv_mode");