VSE: Clamp strip handles to video/audio bounds

This initial commit properly clamps handles for video/audio strips, and
provides functionality to enable/disable the behavior for all strip types
(addresses #90280).

Toggling handle clamping is done with "C",
just like with the redesigned slip operator (#137072).

If a strip is not already clamped when you start moving its handles,
then clamping behavior is disabled starting out. This means no abrupt
clamp until you explicitly ask for it.

Transform logic was altered, fixing a few bugs:
- When initializing a transform, `createTransSeqData` would already
  create some clamping data for channels. This patch replaces it with
  `offset_clamp` (for unconditional clamping which cannot be disabled)
  and `handle_xmin/xmax` (for hold offset clamping, which is optional).
    - Collecting this data ahead of time is necessary for the double
      handle tweak case -- `flushTransSeq` only works one strip at a
      time, so we can't clamp post-hoc.
- In `applySeqSlideValue`, we apply `transform_convert_sequencer_clamp`
  before values are printed to the header, but let the unclamped values
  get flushed to the strips themselves. This is so that we can have the
  data later at the individual strip level to recalculate clamps.
  Otherwise, if transform values are clamped preemptively, then we have
  no idea whether strips are clamped vs. merely resting at their
  boundaries.

Note that currently, handle clamping is drawn identically to overlaps.

More information in PR.

Pull Request: https://projects.blender.org/blender/blender/pulls/134319
This commit is contained in:
John Kiril Swenson
2025-07-16 06:16:19 +02:00
committed by John Kiril Swenson
parent 1cb30b9d1e
commit d910fb88b0
19 changed files with 272 additions and 96 deletions

View File

@@ -5981,6 +5981,7 @@ def km_transform_modal_map(params):
("AUTOCONSTRAINPLANE", {"type": 'MIDDLEMOUSE', "value": 'ANY', "shift": True, **alt_without_navigaton}, None),
("PRECISION", {"type": 'LEFT_SHIFT', "value": 'ANY', "any": True}, None),
("PRECISION", {"type": 'RIGHT_SHIFT', "value": 'ANY', "any": True}, None),
("STRIP_CLAMP_TOGGLE", {"type": 'C', "value": 'PRESS', "any": True}, None),
])
if params.use_alt_navigation:
@@ -6601,7 +6602,7 @@ def km_sequencer_slip_modal_map(_params):
("PRECISION_DISABLE", {"type": 'LEFT_SHIFT', "value": 'RELEASE', "any": True}, None),
("PRECISION_ENABLE", {"type": 'RIGHT_SHIFT', "value": 'PRESS', "any": True}, None),
("PRECISION_DISABLE", {"type": 'RIGHT_SHIFT', "value": 'RELEASE', "any": True}, None),
("CLAMP_TOGGLE", {"type": 'C', "value": 'RELEASE', "any": True}, None),
("CLAMP_TOGGLE", {"type": 'C', "value": 'PRESS', "any": True}, None),
])
return keymap

View File

@@ -3611,6 +3611,7 @@ def km_transform_modal_map(_params):
("AUTOCONSTRAINPLANE", {"type": 'MIDDLEMOUSE', "value": 'ANY', "shift": True}, None),
("PRECISION", {"type": 'LEFT_SHIFT', "value": 'ANY', "any": True}, None),
("PRECISION", {"type": 'RIGHT_SHIFT', "value": 'ANY', "any": True}, None),
("STRIP_CLAMP_TOGGLE", {"type": 'C', "value": 'PRESS', "any": True}, None),
])
return keymap

View File

@@ -1028,9 +1028,12 @@ static void sequencer_add_movie_sync_sound_strip(
/* Make sure that the sound strip start time relative to the movie is taken into account. */
seq::add_sound_av_sync(bmain, scene, strip_sound, load_data);
/* Ensure that the sound strip start/end matches the movie strip even if the actual
* length and true position of the sound doesn't match up exactly.
*/
/* Expand missing sound data in the underlying container to fill the movie strip's length. To the
* user, this missing data is the same as complete silence, so we pretend like it is. */
strip_sound->len = std::max(strip_movie->len, strip_sound->len);
/* Ensure that length matches the movie strip even if the underlying sound data
* doesn't match up (e.g. it is longer). */
seq::time_right_handle_frame_set(
scene, strip_sound, seq::time_right_handle_frame_get(scene, strip_movie));
seq::time_left_handle_frame_set(

View File

@@ -9,6 +9,7 @@
#include "BLI_string_ref.hh"
#include "BLI_string_utils.hh"
#include "BLI_vector.hh"
#include "DNA_sequence_types.h"
#include "MEM_guardedalloc.h"
#include "BLI_fileops.h"
@@ -611,9 +612,7 @@ static SlipData *slip_data_init(const Scene *scene)
VectorSet<Strip *> strips = seq::query_selected_strips(ed->seqbasep);
ListBase *channels = seq::channels_displayed_get(seq::editing_get(scene));
strips.remove_if([&](Strip *strip) {
return ((strip->type & STRIP_TYPE_EFFECT) ||
((strip->type == STRIP_TYPE_IMAGE) && seq::transform_single_image_check(strip)) ||
seq::transform_is_locked(channels, strip));
return (seq::transform_single_image_check(strip) || seq::transform_is_locked(channels, strip));
});
if (strips.is_empty()) {
return nullptr;
@@ -704,6 +703,19 @@ static void slip_strips_delta(wmOperator *op, Scene *scene, SlipData *data, cons
for (Strip *strip : data->strips) {
seq::time_slip_strip(scene, strip, frame_delta, subframe_delta, slip_keyframes);
seq::relations_invalidate_cache(scene, strip);
strip->runtime.flag &= ~(STRIP_CLAMPED_LH | STRIP_CLAMPED_RH);
/* Reconstruct handle clamp state from first principles. */
if (data->clamp == true) {
if (seq::time_left_handle_frame_get(scene, strip) == seq::time_start_frame_get(strip)) {
strip->runtime.flag |= STRIP_CLAMPED_LH;
}
if (seq::time_right_handle_frame_get(scene, strip) ==
seq::time_content_end_frame_get(scene, strip))
{
strip->runtime.flag |= STRIP_CLAMPED_RH;
}
}
}
RNA_float_set(op->ptr, "offset", new_offset);
@@ -716,6 +728,7 @@ static void slip_cleanup(bContext *C, wmOperator *op, Scene *scene)
SlipData *data = static_cast<SlipData *>(op->customdata);
for (Strip *strip : data->strips) {
strip->runtime.flag &= ~(STRIP_CLAMPED_LH | STRIP_CLAMPED_RH);
strip->flag &= ~SEQ_SHOW_OFFSETS;
}
@@ -1957,7 +1970,7 @@ void SEQUENCER_OT_offset_clear(wmOperatorType *ot)
/* Identifiers. */
ot->name = "Clear Strip Offset";
ot->idname = "SEQUENCER_OT_offset_clear";
ot->description = "Clear strip offsets from the start and end frames";
ot->description = "Clear strip in/out offsets from the start and end of content";
/* API callbacks. */
ot->exec = sequencer_offset_clear_exec;

View File

@@ -125,7 +125,7 @@ struct TimelineDrawContext {
/* `sequencer_timeline_draw.cc` */
/** Get handle width in frames (in view-space). */
/* Returns value in frames (view-space), 5px for large strips, 1/4 of the strip for smaller. */
float strip_handle_draw_size_get(const Scene *scene, Strip *strip, float pixelx);
void draw_timeline_seq(const bContext *C, ARegion *region);
void draw_timeline_seq_display(const bContext *C, ARegion *region);

View File

@@ -975,8 +975,8 @@ static void select_linked_time(const Scene *scene,
* Similar to `strip_handle_draw_size_get()`, but returns a larger clickable area that is
* the same for a given zoom level no matter whether "simplified tweaking" is turned off or on.
* `strip_clickable_areas_get` will pad this past strip bounds by 1/3 of the inner handle size,
* making the full handle size either 15 + 5 = 20px or 1/4 + 1/12 = 1/3 of the strip size.
*/
* making the full size 15 + 5 = 20px in frames for large strips, 1/4 + 1/12 = 1/3 of the strip
* size for small ones. */
static float inner_clickable_handle_size_get(const Scene *scene,
const Strip *strip,
const View2D *v2d)

View File

@@ -21,6 +21,7 @@
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_sequence_types.h"
#include "DNA_sound_types.h"
#include "DNA_space_types.h"
#include "DNA_userdef_types.h"
@@ -1410,22 +1411,25 @@ static void strip_data_outline_params_set(const StripDrawContext &strip,
UI_GetThemeColorShade3ubv(TH_BACK, -40, col);
}
const bool translating = (G.moving & G_TRANSFORM_SEQ);
const eSeqOverlapMode overlap_mode = seq::tool_settings_overlap_mode_get(timeline_ctx->scene);
const bool use_overwrite = overlap_mode == SEQ_OVERLAP_OVERWRITE;
const bool overlaps = (strip.strip->flag & SEQ_OVERLAP) && (G.moving & G_TRANSFORM_SEQ);
const bool overlaps = (strip.strip->flag & SEQ_OVERLAP) && translating;
/* Outline while translating strips:
* - Slightly lighter.
* - Red when overlapping with other strips. */
if (G.moving & G_TRANSFORM_SEQ) {
if (overlaps && !use_overwrite) {
col[0] = 255;
col[1] = col[2] = 33;
data.flags |= GPU_SEQ_FLAG_OVERLAP;
}
else if (selected) {
UI_GetColorPtrShade3ubv(col, 70, col);
}
const bool clamped_l = (strip.strip->runtime.flag & STRIP_CLAMPED_LH);
const bool clamped_r = (strip.strip->runtime.flag & STRIP_CLAMPED_RH);
/* Strip outline is:
* - Red when overlapping with other strips or handles are clamped.
* - Slightly lighter while translating strips. */
if ((translating && overlaps && !use_overwrite) || clamped_l || clamped_r) {
col[0] = 255;
col[1] = col[2] = 33;
data.flags |= GPU_SEQ_FLAG_OVERLAP;
}
else if (translating && selected) {
UI_GetColorPtrShade3ubv(col, 70, col);
}
data.col_outline = color_pack(col);
@@ -1490,9 +1494,9 @@ static void draw_strips_foreground(TimelineDrawContext *timeline_ctx,
data.flags |= GPU_SEQ_FLAG_BORDER;
strip_data_missing_media_flags_set(strip, data);
strip_data_lock_flags_set(strip, timeline_ctx, data);
strip_data_handle_flags_set(strip, timeline_ctx, data);
strip_data_outline_params_set(strip, timeline_ctx, data);
strip_data_highlight_flags_set(strip, timeline_ctx, data);
strip_data_handle_flags_set(strip, timeline_ctx, data);
}
strips_batch.flush_batch();

View File

@@ -774,6 +774,11 @@ static bool transform_modal_item_poll(const wmOperator *op, int value)
return false;
}
return t->vod != nullptr;
case TFM_MODAL_STRIP_CLAMP:
if (t->spacetype != SPACE_SEQ) {
return false;
}
break;
}
return true;
}
@@ -830,6 +835,7 @@ wmKeyMap *transform_modal_keymap(wmKeyConfig *keyconf)
{TFM_MODAL_PRECISION, "PRECISION", 0, "Precision Mode", ""},
{TFM_MODAL_PASSTHROUGH_NAVIGATE, "PASSTHROUGH_NAVIGATE", 0, "Navigate", ""},
{TFM_MODAL_NODE_FRAME, "NODE_FRAME", 0, "Attach/Detach Frame", ""},
{TFM_MODAL_STRIP_CLAMP, "STRIP_CLAMP_TOGGLE", 0, "Clamp Strips", ""},
{0, nullptr, 0, nullptr, nullptr},
};
@@ -1376,6 +1382,10 @@ wmOperatorStatus transformEvent(TransInfo *t, wmOperator *op, const wmEvent *eve
t->redraw |= TREDRAW_HARD;
}
break;
case TFM_MODAL_STRIP_CLAMP:
t->modifiers ^= MOD_STRIP_CLAMP_HOLDS;
t->redraw |= TREDRAW_HARD;
break;
default:
break;
}

View File

@@ -202,6 +202,7 @@ enum eTModifier {
MOD_SNAP_FORCED = 1 << 6,
MOD_EDIT_SNAP_SOURCE = 1 << 7,
MOD_NODE_FRAME = 1 << 8,
MOD_STRIP_CLAMP_HOLDS = 1 << 9,
};
ENUM_OPERATORS(eTModifier, MOD_EDIT_SNAP_SOURCE)
@@ -330,6 +331,8 @@ enum {
TFM_MODAL_PASSTHROUGH_NAVIGATE = 36,
TFM_MODAL_NODE_FRAME = 37,
TFM_MODAL_STRIP_CLAMP = 38,
};
/** \} */

View File

@@ -148,7 +148,7 @@ void transform_convert_mesh_customdatacorrect_init(TransInfo *t);
/* `transform_convert_sequencer.cc` */
void transform_convert_sequencer_channel_clamp(TransInfo *t, float r_val[2]);
bool transform_convert_sequencer_clamp(const TransInfo *t, float r_val[2]);
/********************* intern **********************/

View File

@@ -20,6 +20,7 @@
#include "SEQ_animation.hh"
#include "SEQ_channels.hh"
#include "SEQ_edit.hh"
#include "SEQ_effects.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_sequencer.hh"
@@ -60,8 +61,10 @@ struct TransDataSeq {
*/
struct TransSeq {
TransDataSeq *tdseq;
int selection_channel_range_min;
int selection_channel_range_max;
/* Maximum delta allowed along x and y before clamping selected strips/handles. Always active. */
rcti offset_clamp;
/* Maximum delta before clamping handles to the bounds of underlying content. May be disabled. */
int hold_clamp_min, hold_clamp_max;
/* Initial rect of the view2d, used for computing offset during edge panning. */
rctf initial_v2d_cur;
@@ -332,6 +335,7 @@ static void freeSeqData(TransInfo *t, TransDataContainer *tc, TransCustomData *c
t->scene, seqbase_active_get(t), transformed_strips, seq::query_strip_effect_chain);
for (Strip *strip : transformed_strips) {
strip->runtime.flag &= ~(STRIP_CLAMPED_LH | STRIP_CLAMPED_RH);
strip->flag &= ~SEQ_IGNORE_CHANNEL_LOCK;
}
@@ -395,7 +399,7 @@ static Strip *effect_base_input_get(const Scene *scene, Strip *effect, SeqInputS
}
/**
* Strips that aren't stime_dependent_stripselected, but their position entirely depends on
* Strips that aren't selected, but their position entirely depends on
* transformed strips. This collection is used to offset animation.
*/
static void query_time_dependent_strips_strips(TransInfo *t,
@@ -458,6 +462,98 @@ static void query_time_dependent_strips_strips(TransInfo *t,
[&](Strip *strip) { return seq::transform_strip_can_be_translated(strip); });
}
static void create_trans_seq_clamp_data(TransInfo *t, const Scene *scene)
{
TransSeq *ts = (TransSeq *)TRANS_DATA_CONTAINER_FIRST_SINGLE(t)->custom.type.data;
const Editing *ed = seq::editing_get(scene);
bool only_handles_selected = true;
/* Prevent snaps and change in `values` past `offset_clamp` for all selected strips. */
BLI_rcti_init(&ts->offset_clamp, -INT_MAX, INT_MAX, -seq::MAX_CHANNELS, seq::MAX_CHANNELS);
VectorSet<Strip *> strips = seq::query_selected_strips(seq::active_seqbase_get(ed));
for (Strip *strip : strips) {
if (!(strip->type & STRIP_TYPE_EFFECT) || seq::effect_get_num_inputs(strip->type) == 0) {
continue;
}
/* If there is an effect strip with no inputs selected, prevent any x-direction movement,
* since these strips are tied to their inputs and can only move up and down. */
if (!(strip->input1->flag & SELECT) && (!strip->input2 || !(strip->input2->flag & SELECT))) {
ts->offset_clamp.xmin = 0;
ts->offset_clamp.xmax = 0;
}
}
/* Try to clamp handles by default. */
t->modifiers |= MOD_STRIP_CLAMP_HOLDS;
ts->hold_clamp_min = -INT_MAX;
ts->hold_clamp_max = INT_MAX;
for (Strip *strip : strips) {
if (seq::transform_is_locked(seq::channels_displayed_get(ed), strip)) {
continue;
}
bool left_sel = (strip->flag & SEQ_LEFTSEL);
bool right_sel = (strip->flag & SEQ_RIGHTSEL);
/* If any strips start out with hold offsets visible, disable handle clamping on init. */
if ((strip->startofs < 0 || strip->endofs < 0) && !seq::transform_single_image_check(strip)) {
t->modifiers &= ~MOD_STRIP_CLAMP_HOLDS;
}
/* If both handles are selected, there must be enough underlying content to clamp holds. */
bool can_clamp_holds = !(left_sel && right_sel) ||
(strip->len >= seq::time_right_handle_frame_get(scene, strip) -
seq::time_left_handle_frame_get(scene, strip));
can_clamp_holds &= !seq::transform_single_image_check(strip);
/* A handle is selected. Update x-axis clamping data. */
if (left_sel || right_sel) {
if (left_sel) {
/* Ensure that this strip's left handle cannot pass its right handle. */
if (!(left_sel && right_sel)) {
int offset = (seq::time_right_handle_frame_get(scene, strip) - 1) -
seq::time_left_handle_frame_get(scene, strip);
ts->offset_clamp.xmax = min_ii(ts->offset_clamp.xmax, offset);
}
if (can_clamp_holds) {
/* Ensure that the left handle's frame is greater than or equal to the content start. */
ts->hold_clamp_min = max_ii(ts->hold_clamp_min, -strip->startofs);
}
}
if (right_sel) {
if (!(left_sel && right_sel)) {
/* Ensure that this strip's right handle cannot pass its left handle. */
int offset = (seq::time_left_handle_frame_get(scene, strip) + 1) -
seq::time_right_handle_frame_get(scene, strip);
ts->offset_clamp.xmin = max_ii(ts->offset_clamp.xmin, offset);
}
if (can_clamp_holds) {
/* Ensure that the right handle's frame is less than or equal to the content end. */
ts->hold_clamp_max = min_ii(ts->hold_clamp_max, strip->endofs);
}
}
}
/* No handles are selected. Update y-axis channel clamping data. */
else {
ts->offset_clamp.ymin = max_ii(ts->offset_clamp.ymin, 1 - strip->channel);
ts->offset_clamp.ymax = min_ii(ts->offset_clamp.ymax, seq::MAX_CHANNELS - strip->channel);
only_handles_selected = false;
}
}
/* TODO(john): This ensures that y-axis movement is restricted only if all of the selected items
* are handles, since currently it is possible to select whole strips and handles at the same
* time. This should be removed for 5.0 when we make this behavior impossible. */
if (only_handles_selected) {
ts->offset_clamp.ymin = 0;
ts->offset_clamp.ymax = 0;
}
}
static void createTransSeqData(bContext * /*C*/, TransInfo *t)
{
Scene *scene = t->scene;
@@ -515,13 +611,7 @@ static void createTransSeqData(bContext * /*C*/, TransInfo *t)
/* Loop 2: build transdata array. */
SeqToTransData_build(t, ed->seqbasep, td, td2d, tdsq);
ts->selection_channel_range_min = seq::MAX_CHANNELS + 1;
LISTBASE_FOREACH (Strip *, strip, seq::active_seqbase_get(ed)) {
if ((strip->flag & SELECT) != 0) {
ts->selection_channel_range_min = min_ii(ts->selection_channel_range_min, strip->channel);
ts->selection_channel_range_max = max_ii(ts->selection_channel_range_max, strip->channel);
}
}
create_trans_seq_clamp_data(t, scene);
query_time_dependent_strips_strips(t, ts->time_dependent_strips);
}
@@ -532,7 +622,7 @@ static void createTransSeqData(bContext * /*C*/, TransInfo *t)
/** \name UVs Transform Flush
* \{ */
static void view2d_edge_pan_loc_compensate(TransInfo *t, float offset[2])
static void view2d_edge_pan_loc_compensate(TransInfo *t, float r_offset[2])
{
TransSeq *ts = (TransSeq *)TRANS_DATA_CONTAINER_FIRST_SINGLE(t)->custom.type.data;
@@ -555,7 +645,7 @@ static void view2d_edge_pan_loc_compensate(TransInfo *t, float offset[2])
if (t->state != TRANS_CANCEL) {
if (!BLI_rctf_compare(&rect_prev, &t->region->v2d.cur, FLT_EPSILON)) {
/* Additional offset due to change in view2D rect. */
BLI_rctf_transform_pt_v(&t->region->v2d.cur, &rect_prev, offset, offset);
BLI_rctf_transform_pt_v(&t->region->v2d.cur, &rect_prev, r_offset, r_offset);
transformViewUpdate(t);
}
}
@@ -565,17 +655,11 @@ static void flushTransSeq(TransInfo *t)
{
/* Editing null check already done. */
ListBase *seqbasep = seqbase_active_get(t);
int a, new_frame, offset;
TransData *td = nullptr;
TransData2D *td2d = nullptr;
TransDataSeq *tdsq = nullptr;
Strip *strip;
Scene *scene = t->scene;
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
TransData *td = tc->data;
TransData2D *td2d = tc->data_2d;
/* This is calculated for offsetting animation of effects that change position with inputs.
* Maximum(positive or negative) value is used, because individual strips can be clamped. This
@@ -590,34 +674,65 @@ static void flushTransSeq(TransInfo *t)
view2d_edge_pan_loc_compensate(t, edge_pan_offset);
/* Flush to 2D vector from internally used 3D vector. */
for (a = 0, td = tc->data, td2d = tc->data_2d; a < tc->data_len; a++, td++, td2d++) {
tdsq = (TransDataSeq *)td->extra;
strip = tdsq->strip;
for (int a = 0; a < tc->data_len; a++, td++, td2d++) {
TransDataSeq *tdsq = (TransDataSeq *)td->extra;
Strip *strip = tdsq->strip;
new_frame = round_fl_to_int(td->loc[0] + edge_pan_offset[0]);
/* Apply extra offset caused by edge panning. */
add_v2_v2(td->loc, edge_pan_offset);
float offset[2];
float offset_clamped[2];
sub_v2_v2v2(offset, td->loc, td->iloc);
copy_v2_v2(offset_clamped, offset);
if (t->state != TRANS_CANCEL) {
transform_convert_sequencer_clamp(t, offset_clamped);
}
const int new_frame = round_fl_to_int(td->iloc[0] + offset_clamped[0]);
const int new_channel = round_fl_to_int(td->iloc[1] + offset_clamped[1]);
/* Compute handle clamping state to be drawn. */
if (tdsq->sel_flag & SEQ_LEFTSEL) {
strip->runtime.flag &= ~STRIP_CLAMPED_LH;
}
if (tdsq->sel_flag & SEQ_RIGHTSEL) {
strip->runtime.flag &= ~STRIP_CLAMPED_RH;
}
if (!seq::transform_single_image_check(strip) && !(strip->type & STRIP_TYPE_EFFECT)) {
if (offset_clamped[0] > offset[0] && new_frame == seq::time_start_frame_get(strip)) {
strip->runtime.flag |= STRIP_CLAMPED_LH;
}
else if (offset_clamped[0] < offset[0] &&
new_frame == seq::time_content_end_frame_get(scene, strip))
{
strip->runtime.flag |= STRIP_CLAMPED_RH;
}
}
switch (tdsq->sel_flag) {
case SELECT: {
int offset = new_frame - tdsq->start_offset - strip->start;
if (seq::transform_strip_can_be_translated(strip)) {
offset = new_frame - tdsq->start_offset - strip->start;
seq::transform_translate_strip(scene, strip, offset);
if (abs(offset) > abs(max_offset)) {
max_offset = offset;
}
}
seq::strip_channel_set(strip, round_fl_to_int(td->loc[1] + edge_pan_offset[1]));
seq::strip_channel_set(strip, new_channel);
break;
}
case SEQ_LEFTSEL: { /* No vertical transform. */
/* Update right handle first if both handles are selected and the new_frame is right of
/* Update right handle first if both handles are selected and the `new_frame` is right of
* the old one to avoid unexpected left handle clamping when canceling. See #126191. */
bool both_handles_selected = (tdsq->flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL)) ==
(SEQ_LEFTSEL | SEQ_RIGHTSEL);
if (both_handles_selected && new_frame > seq::time_left_handle_frame_get(scene, strip)) {
a++, td++, td2d++;
int new_right_frame = round_fl_to_int(td->loc[0] + edge_pan_offset[0]);
seq::time_right_handle_frame_set(scene, strip, new_right_frame);
const bool both_handles_selected = (tdsq->flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL)) ==
(SEQ_LEFTSEL | SEQ_RIGHTSEL);
if (both_handles_selected && new_frame >= seq::time_right_handle_frame_get(scene, strip)) {
/* For now, move the right handle far enough to avoid the left handle getting clamped.
* The final, correct position will be calculated later. */
seq::time_right_handle_frame_set(scene, strip, new_frame + 1);
}
int old_startdisp = seq::time_left_handle_frame_get(scene, strip);
seq::time_left_handle_frame_set(t->scene, strip, new_frame);
@@ -730,19 +845,32 @@ static void special_aftertrans_update__sequencer(bContext * /*C*/, TransInfo *t)
}
}
void transform_convert_sequencer_channel_clamp(TransInfo *t, float r_val[2])
bool transform_convert_sequencer_clamp(const TransInfo *t, float r_val[2])
{
const TransSeq *ts = (TransSeq *)TRANS_DATA_CONTAINER_FIRST_SINGLE(t)->custom.type.data;
const int channel_offset = round_fl_to_int(r_val[1]);
const int min_channel_after_transform = ts->selection_channel_range_min + channel_offset;
const int max_channel_after_transform = ts->selection_channel_range_max + channel_offset;
int val[2] = {round_fl_to_int(r_val[0]), round_fl_to_int(r_val[1])};
bool clamped = false;
if (max_channel_after_transform > seq::MAX_CHANNELS) {
r_val[1] -= max_channel_after_transform - seq::MAX_CHANNELS;
/* Unconditional channel and handle clamping. Should never be ignored. */
if (BLI_rcti_clamp_pt_v(&ts->offset_clamp, val)) {
r_val[0] = static_cast<float>(val[0]);
r_val[1] = static_cast<float>(val[1]);
clamped = true;
}
if (min_channel_after_transform < 1) {
r_val[1] -= min_channel_after_transform - 1;
/* Optional clamping of handles to underlying holds. Can be disabled by the user. */
if (t->modifiers & MOD_STRIP_CLAMP_HOLDS) {
if (val[0] < ts->hold_clamp_min) {
r_val[0] = static_cast<float>(ts->hold_clamp_min);
clamped = true;
}
else if (val[0] > ts->hold_clamp_max) {
r_val[0] = static_cast<float>(ts->hold_clamp_max);
clamped = true;
}
}
return clamped;
}
/** \} */

View File

@@ -74,7 +74,7 @@ static void applySeqSlideValue(TransInfo *t, const float val[2])
static void applySeqSlide(TransInfo *t)
{
char str[UI_MAX_DRAW_STR];
float values_final[3] = {0.0f};
float values_final[3] = {0.0f}, values_clamped[3] = {0.0f};
if (applyNumInput(&t->num, values_final)) {
if (t->con.mode & CON_APPLY) {
@@ -89,20 +89,20 @@ static void applySeqSlide(TransInfo *t)
else {
copy_v2_v2(values_final, t->values);
transform_snap_mixed_apply(t, values_final);
if (!vse::sequencer_retiming_mode_is_active(t->context)) {
transform_convert_sequencer_channel_clamp(t, values_final);
}
if (t->con.mode & CON_APPLY) {
t->con.applyVec(t, nullptr, nullptr, values_final, values_final);
}
}
values_final[0] = floorf(values_final[0] + 0.5f);
values_final[1] = floorf(values_final[1] + 0.5f);
copy_v2_v2(t->values_final, values_final);
values_final[0] = round_fl_to_int(values_final[0]);
values_final[1] = round_fl_to_int(values_final[1]);
headerSeqSlide(t, t->values_final, str);
copy_v2_v2(values_clamped, values_final);
transform_convert_sequencer_clamp(t, values_clamped);
headerSeqSlide(t, values_clamped, str);
copy_v2_v2(t->values_final, values_final);
applySeqSlideValue(t, t->values_final);
recalc_data(t);

View File

@@ -544,6 +544,11 @@ static bool snap_calc_timeline(TransInfo *t, const TransSeqSnapData *snap_data)
return false;
}
float2 best_offset(static_cast<float>(best_target_frame - best_source_frame), 0.0f);
if (transform_convert_sequencer_clamp(t, best_offset)) {
return false;
}
t->tsnap.snap_target[0] = best_target_frame;
t->tsnap.snap_source[0] = best_source_frame;
return true;

View File

@@ -123,6 +123,7 @@ enum eGPUSeqFlags : uint32_t {
GPU_SEQ_FLAG_SELECTED_LH = (1u << 11u),
GPU_SEQ_FLAG_SELECTED_RH = (1u << 12u),
GPU_SEQ_FLAG_OVERLAP = (1u << 15u),
GPU_SEQ_FLAG_CLAMPED = (1u << 16u),
GPU_SEQ_FLAG_ANY_HANDLE = GPU_SEQ_FLAG_SELECTED_LH | GPU_SEQ_FLAG_SELECTED_RH
};

View File

@@ -193,7 +193,8 @@ void main()
/* 2px outline for all overlapping strips. */
bool overlaps = (strip.flags & GPU_SEQ_FLAG_OVERLAP) != 0;
if (overlaps) {
bool clamped = (strip.flags & GPU_SEQ_FLAG_CLAMPED) != 0;
if (overlaps || clamped) {
col = add_outline(sdf, 1.0f, 3.0f, col, col_outline);
}

View File

@@ -144,14 +144,6 @@ typedef struct StripData {
ColorManagedColorspaceSettings colorspace_settings;
} StripData;
typedef enum eSeqRetimingKeyFlag {
SEQ_SPEED_TRANSITION_IN = (1 << 0),
SEQ_SPEED_TRANSITION_OUT = (1 << 1),
SEQ_FREEZE_FRAME_IN = (1 << 2),
SEQ_FREEZE_FRAME_OUT = (1 << 3),
SEQ_KEY_SELECTED = (1 << 4),
} eSeqRetimingKeyFlag;
typedef struct SeqRetimingKey {
double strip_frame_index;
int flag; /* eSeqRetimingKeyFlag */
@@ -166,6 +158,9 @@ typedef struct SeqRetimingKey {
typedef struct StripRuntime {
SessionUID session_uid;
/** eStripRuntimeFlag */
uint32_t flag;
char _pad[4];
} StripRuntime;
/**
@@ -637,6 +632,21 @@ enum {
#define STRIP_NAME_MAXSTR 64
/** #SeqRetimingKey::flag */
typedef enum eSeqRetimingKeyFlag {
SEQ_SPEED_TRANSITION_IN = (1 << 0),
SEQ_SPEED_TRANSITION_OUT = (1 << 1),
SEQ_FREEZE_FRAME_IN = (1 << 2),
SEQ_FREEZE_FRAME_OUT = (1 << 3),
SEQ_KEY_SELECTED = (1 << 4),
} eSeqRetimingKeyFlag;
/** #StripRuntime::flag */
typedef enum eStripRuntimeFlag {
STRIP_CLAMPED_LH = (1 << 0),
STRIP_CLAMPED_RH = (1 << 1),
} eStripRuntimeFlag;
/* From: `DNA_object_types.h`, see it's doc-string there. */
#define SELECT 1
@@ -693,9 +703,6 @@ enum {
/* convenience define for all selection flags */
#define STRIP_ALLSEL (SELECT + SEQ_LEFTSEL + SEQ_RIGHTSEL)
/* Deprecated, don't use a flag anymore. */
// #define STRIP_ACTIVE 1048576
enum {
SEQ_COLOR_BALANCE_INVERSE_GAIN = 1 << 0,
SEQ_COLOR_BALANCE_INVERSE_GAMMA = 1 << 1,
@@ -724,7 +731,7 @@ enum {
SEQ_PROXY_TC_RECORD_RUN_NO_GAPS = 1 << 1,
};
/** SeqProxy.build_flags */
/** StripProxy.build_flags */
enum {
SEQ_PROXY_SKIP_EXISTING = 1,
};

View File

@@ -21,8 +21,9 @@ namespace blender::seq {
bool transform_strip_can_be_translated(const Strip *strip);
/**
* Used so we can do a quick check for single image strip
* since they work a bit differently to normal image strips (during transform).
* Checks whether the strip functions as a single static display,
* which means it has only one unique frame of content and does not draw holds.
* This includes non-sequence image strips and all effect strips with no inputs (e.g. color, text).
*/
bool transform_single_image_check(const Strip *strip);
bool transform_test_overlap(const Scene *scene, ListBase *seqbasep, Strip *test);

View File

@@ -67,7 +67,7 @@ void fontmap_clear();
* will be created on demand.
*
* \param scene: Scene to query.
* \param seq: Sequencer strip.
* \param strip: Sequencer strip.
* \return True if media file is missing.
*/
bool media_presence_is_missing(Scene *scene, const Strip *strip);

View File

@@ -174,9 +174,7 @@ void time_update_meta_strip_range(const Scene *scene, Strip *strip_meta)
}
strip_meta->start = min + strip_meta->anim_startofs;
strip_meta->len = max - min;
strip_meta->len -= strip_meta->anim_startofs;
strip_meta->len -= strip_meta->anim_endofs;
strip_meta->len = max - strip_meta->anim_endofs - strip_meta->start;
/* Functions `SEQ_time_*_handle_frame_set()` can not be used here, because they are clamped, so
* change must be done at once. */
@@ -510,8 +508,8 @@ void time_left_handle_frame_set(const Scene *scene, Strip *strip, int timeline_f
float offset = timeline_frame - time_start_frame_get(strip);
if (transform_single_image_check(strip)) {
/* This strip has only 1 frame of content, that is always stretched to whole strip length.
* Therefore, strip start should be moved instead of adjusting offset. */
/* This strip has only 1 frame of content that is always stretched to the whole strip length.
* Move strip start left and adjust end offset to be negative (rightwards past the 1 frame). */
time_start_frame_set(scene, strip, timeline_frame);
strip->endofs += offset;
}