Animation: Allow NLA strips to be horizontally shuffled

Allows NLA strips to horizontally translated over each other. If a strip is dropped when translating, it'll cause the strip to shuffle into place.

---

Abstracted large conditional branch in `recalcData_nla` into it's own `nlastrip_fix_overlapping` method for increased readability. No logical changes were made.

---
[Archived Phabricator Patch](https://archive.blender.org/developer/D10102)

Pull Request: https://projects.blender.org/blender/blender/pulls/105532
This commit is contained in:
Nate Rupsis
2023-04-03 17:10:37 +02:00
committed by Nate Rupsis
parent 4bcd59d644
commit 8833f5dbf9
4 changed files with 377 additions and 103 deletions

View File

@@ -812,7 +812,7 @@ void BKE_nlastrips_sort_strips(ListBase *strips)
for (sstrip = tmp.last; sstrip; sstrip = sstrip->prev) {
/* check if add after */
if (sstrip->end <= strip->start) {
if (sstrip->start <= strip->start) {
BLI_insertlinkafter(&tmp, sstrip, strip);
not_added = 0;
break;
@@ -841,7 +841,7 @@ void BKE_nlastrips_add_strip_unsafe(ListBase *strips, NlaStrip *strip)
/* find the right place to add the strip to the nominated track */
for (ns = strips->first; ns; ns = ns->next) {
/* if current strip occurs after the new strip, add it before */
if (ns->start >= strip->end) {
if (ns->start >= strip->start) {
BLI_insertlinkbefore(strips, ns, strip);
not_added = 0;
break;
@@ -1952,9 +1952,30 @@ static void BKE_nlastrip_validate_autoblends(NlaTrack *nlt, NlaStrip *nls)
}
}
/* Ensure every transition's start/end properly set.
* Strip will be removed / freed if it doesn't fit (invalid).
* Return value indicates if passed strip is valid/fixed or invalid/removed. */
static bool nlastrip_validate_transition_start_end(NlaStrip *strip)
{
if (!(strip->type & NLASTRIP_TYPE_TRANSITION)) {
return true;
}
if (strip->prev) {
strip->start = strip->prev->end;
}
if (strip->next) {
strip->end = strip->next->start;
}
if (strip->start >= strip->end || strip->prev == NULL || strip->next == NULL) {
BKE_nlastrip_free(strip, true);
return false;
}
return true;
}
void BKE_nla_validate_state(AnimData *adt)
{
NlaStrip *strip = NULL;
NlaTrack *nlt;
/* sanity checks */
@@ -1965,7 +1986,15 @@ void BKE_nla_validate_state(AnimData *adt)
/* Adjust blending values for auto-blending,
* and also do an initial pass to find the earliest strip. */
for (nlt = adt->nla_tracks.first; nlt; nlt = nlt->next) {
for (strip = nlt->strips.first; strip; strip = strip->next) {
LISTBASE_FOREACH_MUTABLE (NlaStrip *, strip, &nlt->strips) {
if (!nlastrip_validate_transition_start_end(strip)) {
printf(
"While moving NLA strips, a transition strip could no longer be applied to the new "
"positions and was removed.\n");
continue;
}
/* auto-blending first */
BKE_nlastrip_validate_autoblends(nlt, strip);
BKE_nlastrip_recalculate_blend(strip);

View File

@@ -487,7 +487,8 @@ static void nla_draw_strip(SpaceNla *snla,
}
/* draw 'inside' of strip itself */
if (solo && is_nlastrip_enabled(adt, nlt, strip)) {
if (solo && is_nlastrip_enabled(adt, nlt, strip) &&
!(strip->flag & NLASTRIP_FLAG_INVALID_LOCATION)) {
immUnbindProgram();
/* strip is in normal track */
@@ -534,7 +535,11 @@ static void nla_draw_strip(SpaceNla *snla,
/* draw strip outline
* - color used here is to indicate active vs non-active
*/
if (strip->flag & (NLASTRIP_FLAG_ACTIVE | NLASTRIP_FLAG_SELECT)) {
if (strip->flag & NLASTRIP_FLAG_INVALID_LOCATION) {
color[0] = 1.0f;
color[1] = color[2] = 0.15f;
}
else if (strip->flag & NLASTRIP_FLAG_ACTIVE) {
/* strip should appear 'sunken', so draw a light border around it */
color[0] = color[1] = color[2] = 1.0f; /* FIXME: hardcoded temp-hack colors */
}

View File

@@ -5,6 +5,8 @@
* \ingroup edtransform
*/
#include <stdio.h>
#include "DNA_anim_types.h"
#include "DNA_space_types.h"
@@ -13,6 +15,7 @@
#include "BLI_listbase.h"
#include "BLI_math.h"
#include "BKE_anim_data.h"
#include "BKE_context.h"
#include "BKE_nla.h"
@@ -55,6 +58,86 @@ typedef struct TransDataNla {
int handle;
} TransDataNla;
static bool is_overlap(const float left_bound_a,
const float right_bound_a,
const float left_bound_b,
const float right_bound_b)
{
return (left_bound_a < right_bound_b) && (right_bound_a > left_bound_b);
}
static bool nlastrip_is_overlap(const NlaStrip *strip_a,
const float offset_a,
const NlaStrip *strip_b,
const float offset_b)
{
return is_overlap(strip_a->start + offset_a,
strip_a->end + offset_a,
strip_b->start + offset_b,
strip_b->end + offset_b);
}
/** Assumes strips to horizontally translate (shuffle) are tagged with
* NLASTRIP_FLAG_INVALID_LOCATION.
*
* \returns The total sided offset that results in no overlaps between tagged strips and non-tagged
* strips.
*/
static float transdata_get_time_shuffle_offset_side(ListBase *trans_datas, const bool shuffle_left)
{
float total_offset = 0;
float offset;
do {
offset = 0;
LISTBASE_FOREACH (LinkData *, link, trans_datas) {
TransDataNla *trans_data = (TransDataNla *)link->data;
NlaStrip *xformed_strip = trans_data->strip;
LISTBASE_FOREACH (NlaStrip *, non_xformed_strip, &trans_data->nlt->strips) {
if (non_xformed_strip->flag & NLASTRIP_FLAG_INVALID_LOCATION) {
continue;
}
/* Allow overlap with transitions. */
if (non_xformed_strip->type & NLASTRIP_TYPE_TRANSITION) {
continue;
}
if (!nlastrip_is_overlap(non_xformed_strip, 0, xformed_strip, total_offset)) {
continue;
}
offset = shuffle_left ?
fmin(offset, non_xformed_strip->start - (xformed_strip->end + total_offset)) :
fmax(offset, non_xformed_strip->end - (xformed_strip->start + total_offset));
}
}
total_offset += offset;
} while (!IS_EQF(offset, 0.0f));
return total_offset;
}
/** Assumes strips to horizontally translate (shuffle) are tagged with
* NLASTRIP_FLAG_INVALID_LOCATION.
*
* \returns The minimal total signed offset that results in no overlaps between tagged strips and
* non-tagged strips.
*/
static float transdata_get_time_shuffle_offset(ListBase *trans_datas)
{
const float offset_left = transdata_get_time_shuffle_offset_side(trans_datas, true);
const float offset_right = transdata_get_time_shuffle_offset_side(trans_datas, false);
BLI_assert(offset_left <= 0);
BLI_assert(offset_right >= 0);
return -offset_left < offset_right ? offset_left : offset_right;
}
/* -------------------------------------------------------------------- */
/** \name Transform application to NLA strips
* \{ */
@@ -81,6 +164,117 @@ static void applyTransformNLA_timeScale(PointerRNA *strip_rna_ptr, const float v
RNA_float_set(strip_rna_ptr, "scale", value);
}
/** Reorder strips for proper nla stack evaluation while dragging. */
static void nlastrip_overlap_reorder(TransDataNla *tdn, NlaStrip *strip)
{
while (strip->prev != NULL && tdn->h1[0] < strip->prev->start) {
BLI_listbase_swaplinks(&tdn->nlt->strips, strip, strip->prev);
}
while (strip->next != NULL && tdn->h1[0] > strip->next->start) {
BLI_listbase_swaplinks(&tdn->nlt->strips, strip, strip->next);
}
}
/** Flag overlaps with adjacent strips.
*
* Since the strips are re-ordered as they're transformed, we only have to check adjacent
* strips for overlap instead of all of them. */
static void nlastrip_flag_overlaps(NlaStrip *strip)
{
NlaStrip *adj_strip = strip->prev;
if (adj_strip != NULL && !(adj_strip->flag & NLASTRIP_FLAG_SELECT) &&
nlastrip_is_overlap(strip, 0, adj_strip, 0)) {
strip->flag |= NLASTRIP_FLAG_INVALID_LOCATION;
}
adj_strip = strip->next;
if (adj_strip != NULL && !(adj_strip->flag & NLASTRIP_FLAG_SELECT) &&
nlastrip_is_overlap(strip, 0, adj_strip, 0)) {
strip->flag |= NLASTRIP_FLAG_INVALID_LOCATION;
}
}
/** Check the Transformation data for the given Strip, and fix any overlap. Then
* apply the Transformation.
*/
static void nlastrip_fix_overlapping(TransInfo *t, TransDataNla *tdn, NlaStrip *strip)
{
/* firstly, check if the proposed transform locations would overlap with any neighboring
* strips (barring transitions) which are absolute barriers since they are not being moved
*
* this is done as a iterative procedure (done 5 times max for now)
*/
short iter_max = 4;
NlaStrip *prev = BKE_nlastrip_prev_in_track(strip, true);
NlaStrip *next = BKE_nlastrip_next_in_track(strip, true);
PointerRNA strip_ptr;
for (short iter = 0; iter <= iter_max; iter++) {
const bool p_exceeded = (prev != NULL) && (tdn->h1[0] < prev->end);
const bool n_exceeded = (next != NULL) && (tdn->h2[0] > next->start);
if ((p_exceeded && n_exceeded) || (iter == iter_max)) {
/* both endpoints exceeded (or iteration ping-pong'd meaning that we need a
* compromise)
* - Simply crop strip to fit within the bounds of the strips bounding it
* - If there were no neighbors, clear the transforms
* (make it default to the strip's current values).
*/
if (prev && next) {
tdn->h1[0] = prev->end;
tdn->h2[0] = next->start;
}
else {
tdn->h1[0] = strip->start;
tdn->h2[0] = strip->end;
}
}
else if (n_exceeded) {
/* move backwards */
float offset = tdn->h2[0] - next->start;
tdn->h1[0] -= offset;
tdn->h2[0] -= offset;
}
else if (p_exceeded) {
/* more forwards */
float offset = prev->end - tdn->h1[0];
tdn->h1[0] += offset;
tdn->h2[0] += offset;
}
else { /* all is fine and well */
break;
}
}
/* Use RNA to write the values to ensure that constraints on these are obeyed
* (e.g. for transition strips, the values are taken from the neighbors)
*/
RNA_pointer_create(NULL, &RNA_NlaStrip, strip, &strip_ptr);
switch (t->mode) {
case TFM_TIME_EXTEND:
case TFM_TIME_SCALE: {
/* The final scale is the product of the original strip scale (from before the
* transform operation started) and the current scale value of this transform
* operation. */
const float originalStripScale = tdn->h1[2];
const float newStripScale = originalStripScale * t->values_final[0];
applyTransformNLA_timeScale(&strip_ptr, newStripScale);
applyTransformNLA_translation(&strip_ptr, tdn);
break;
}
case TFM_TRANSLATION:
applyTransformNLA_translation(&strip_ptr, tdn);
break;
default:
printf("recalcData_nla: unsupported NLA transformation mode %d\n", t->mode);
break;
}
}
/** \} */
/* -------------------------------------------------------------------- */
@@ -189,9 +383,9 @@ static void createTransNlaData(bContext *C, TransInfo *t)
/* our transform data is constructed as follows:
* - only the handles on the right side of the current-frame get included
* - td structs are transform-elements operated on by the transform system
* and represent a single handle. The storage/pointer used (val or loc) depends on
* whether we're scaling or transforming. Ultimately though, the handles
* the td writes to will simply be a dummy in tdn
* and represent a single handle. The storage/pointer used (val or loc) depends
* on whether we're scaling or transforming. Ultimately though, the handles the td
* writes to will simply be a dummy in tdn
* - for each strip being transformed, a single tdn struct is used, so in some
* cases, there will need to be 1 of these tdn elements in the array skipped...
*/
@@ -304,13 +498,13 @@ static void recalcData_nla(TransInfo *t)
TransDataNla *tdn = tc->custom.type.data;
for (int i = 0; i < tc->data_len; i++, tdn++) {
NlaStrip *strip = tdn->strip;
PointerRNA strip_ptr;
int delta_y1, delta_y2;
/* if this tdn has no handles, that means it is just a dummy that should be skipped */
if (tdn->handle == 0) {
continue;
}
strip->flag &= ~NLASTRIP_FLAG_INVALID_LOCATION;
/* set refresh tags for objects using this animation,
* BUT only if realtime updates are enabled
@@ -355,75 +549,18 @@ static void recalcData_nla(TransInfo *t)
continue;
}
/* firstly, check if the proposed transform locations would overlap with any neighboring strips
* (barring transitions) which are absolute barriers since they are not being moved
*
* this is done as a iterative procedure (done 5 times max for now)
*/
NlaStrip *prev = BKE_nlastrip_prev_in_track(strip, true);
NlaStrip *next = BKE_nlastrip_next_in_track(strip, true);
const bool nlatrack_isliboverride = BKE_nlatrack_is_nonlocal_in_liboverride(tdn->id, tdn->nlt);
const bool allow_overlap = !nlatrack_isliboverride && ELEM(t->mode, TFM_TRANSLATION);
for (short iter = 0; iter < 5; iter++) {
const bool pExceeded = (prev != NULL) && (tdn->h1[0] < prev->end);
const bool nExceeded = (next != NULL) && (tdn->h2[0] > next->start);
if (allow_overlap) {
nlastrip_overlap_reorder(tdn, strip);
if ((pExceeded && nExceeded) || (iter == 4)) {
/* both endpoints exceeded (or iteration ping-pong'd meaning that we need a
* compromise)
* - Simply crop strip to fit within the bounds of the strips bounding it
* - If there were no neighbors, clear the transforms
* (make it default to the strip's current values).
*/
if (prev && next) {
tdn->h1[0] = prev->end;
tdn->h2[0] = next->start;
}
else {
tdn->h1[0] = strip->start;
tdn->h2[0] = strip->end;
}
}
else if (nExceeded) {
/* move backwards */
float offset = tdn->h2[0] - next->start;
tdn->h1[0] -= offset;
tdn->h2[0] -= offset;
}
else if (pExceeded) {
/* more forwards */
float offset = prev->end - tdn->h1[0];
tdn->h1[0] += offset;
tdn->h2[0] += offset;
}
else { /* all is fine and well */
break;
}
/* Directly flush. */
strip->start = tdn->h1[0];
strip->end = tdn->h2[0];
}
/* Use RNA to write the values to ensure that constraints on these are obeyed
* (e.g. for transition strips, the values are taken from the neighbors)
*/
RNA_pointer_create(NULL, &RNA_NlaStrip, strip, &strip_ptr);
switch (t->mode) {
case TFM_TIME_EXTEND:
case TFM_TIME_SCALE: {
/* The final scale is the product of the original strip scale (from before the transform
* operation started) and the current scale value of this transform operation. */
const float originalStripScale = tdn->h1[2];
const float newStripScale = originalStripScale * t->values_final[0];
applyTransformNLA_timeScale(&strip_ptr, newStripScale);
applyTransformNLA_translation(&strip_ptr, tdn);
break;
}
case TFM_TRANSLATION:
applyTransformNLA_translation(&strip_ptr, tdn);
break;
default:
printf("recalcData_nla: unsupported NLA transformation mode %d\n", t->mode);
continue;
else {
nlastrip_fix_overlapping(t, tdn, strip);
}
/* flush transforms to child strips (since this should be a meta) */
@@ -433,9 +570,10 @@ static void recalcData_nla(TransInfo *t)
* - we need to calculate both,
* as only one may have been altered by transform if only 1 handle moved.
*/
/* In LibOverride case, we cannot move strips across tracks that come from the linked data. */
/* In LibOverride case, we cannot move strips across tracks that come from the linked data.
*/
const bool is_liboverride = ID_IS_OVERRIDE_LIBRARY(tdn->id);
if (BKE_nlatrack_is_nonlocal_in_liboverride(tdn->id, tdn->nlt)) {
if (nlatrack_isliboverride) {
continue;
}
@@ -489,6 +627,8 @@ static void recalcData_nla(TransInfo *t)
}
}
}
nlastrip_flag_overlaps(strip);
}
}
@@ -498,7 +638,83 @@ static void recalcData_nla(TransInfo *t)
/** \name Special After Transform NLA
* \{ */
static void special_aftertrans_update__nla(bContext *C, TransInfo *UNUSED(t))
typedef struct IDGroupedTransData {
struct IDGroupedTransData *next, *prev;
ID *id;
ListBase trans_datas;
} IDGroupedTransData;
/** horizontally translate (shuffle) the transformed strip to a non-overlapping state. */
static void nlastrip_shuffle_transformed(TransDataContainer *tc, TransDataNla *first_trans_data)
{
/* Element: (IDGroupedTransData*) */
ListBase grouped_trans_datas = {NULL, NULL};
/* Flag all non-library-override transformed strips so we can distinguish them when
* shuffling.
*
* Group trans_datas by ID so shuffling is unique per ID.
*/
{
TransDataNla *tdn = first_trans_data;
for (int i = 0; i < tc->data_len; i++, tdn++) {
/* Skip dummy handles. */
if (tdn->handle == 0) {
continue;
}
/* For strips within library override tracks, don't do any shuffling at all. Unsure how
* library overrides should behave so, for now, they're treated as mostly immutable. */
if ((tdn->nlt->flag & NLATRACK_OVERRIDELIBRARY_LOCAL) == 0) {
continue;
}
tdn->strip->flag |= NLASTRIP_FLAG_INVALID_LOCATION;
IDGroupedTransData *dst_group = NULL;
/* Find dst_group with matching ID. */
LISTBASE_FOREACH (IDGroupedTransData *, group, &grouped_trans_datas) {
if (group->id == tdn->id) {
dst_group = group;
break;
}
}
if (dst_group == NULL) {
dst_group = MEM_callocN(sizeof(IDGroupedTransData), __func__);
dst_group->id = tdn->id;
BLI_addhead(&grouped_trans_datas, dst_group);
}
BLI_addtail(&dst_group->trans_datas, BLI_genericNodeN(tdn));
}
}
/* Apply shuffling. */
LISTBASE_FOREACH (IDGroupedTransData *, group, &grouped_trans_datas) {
ListBase *trans_datas = &group->trans_datas;
/* Apply horizontal shuffle. */
const float minimum_time_offset = transdata_get_time_shuffle_offset(trans_datas);
LISTBASE_FOREACH (LinkData *, link, trans_datas) {
TransDataNla *trans_data = (TransDataNla *)link->data;
NlaStrip *strip = trans_data->strip;
strip->start += minimum_time_offset;
strip->end += minimum_time_offset;
BKE_nlameta_flush_transforms(strip);
}
}
/* Memory cleanup. */
LISTBASE_FOREACH (IDGroupedTransData *, group, &grouped_trans_datas) {
BLI_freelistN(&group->trans_datas);
}
BLI_freelistN(&grouped_trans_datas);
}
static void special_aftertrans_update__nla(bContext *C, TransInfo *t)
{
bAnimContext ac;
@@ -507,36 +723,55 @@ static void special_aftertrans_update__nla(bContext *C, TransInfo *UNUSED(t))
return;
}
if (ac.datatype) {
ListBase anim_data = {NULL, NULL};
bAnimListElem *ale;
short filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FOREDIT | ANIMFILTER_FCURVESONLY);
if (!ac.datatype) {
return;
}
/* get channels to work on */
ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
TransDataNla *first_trans_data = tc->custom.type.data;
for (ale = anim_data.first; ale; ale = ale->next) {
NlaTrack *nlt = (NlaTrack *)ale->data;
/* Shuffle transformed strips. */
if (ELEM(t->mode, TFM_TRANSLATION)) {
nlastrip_shuffle_transformed(tc, first_trans_data);
}
/* make sure strips are in order again */
BKE_nlatrack_sort_strips(nlt);
/* remove the temp metas */
BKE_nlastrips_clear_metas(&nlt->strips, 0, 1);
/* Clear NLASTRIP_FLAG_INVALID_LOCATION flag. */
TransDataNla *tdn = first_trans_data;
for (int i = 0; i < tc->data_len; i++, tdn++) {
if (tdn->strip == NULL) {
continue;
}
/* General refresh for the outliner because the following might have happened:
* - strips moved between tracks
* - strips swapped order
* - duplicate-move moves to different track. */
WM_event_add_notifier(C, NC_ANIMATION | ND_NLA | NA_ADDED, NULL);
/* free temp memory */
ANIM_animdata_freelist(&anim_data);
/* Perform after-transform validation. */
ED_nla_postop_refresh(&ac);
tdn->strip->flag &= ~NLASTRIP_FLAG_INVALID_LOCATION;
}
ListBase anim_data = {NULL, NULL};
short filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FOREDIT | ANIMFILTER_FCURVESONLY);
/* get channels to work on */
ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
NlaTrack *nlt = (NlaTrack *)ale->data;
/* make sure strips are in order again */
BKE_nlatrack_sort_strips(nlt);
/* remove the temp metas */
BKE_nlastrips_clear_metas(&nlt->strips, 0, 1);
}
/* General refresh for the outliner because the following might have happened:
* - strips moved between tracks
* - strips swapped order
* - duplicate-move moves to different track. */
WM_event_add_notifier(C, NC_ANIMATION | ND_NLA | NA_ADDED, NULL);
/* free temp memory */
ANIM_animdata_freelist(&anim_data);
/* Perform after-transform validation. */
ED_nla_postop_refresh(&ac);
}
/** \} */

View File

@@ -832,6 +832,11 @@ typedef enum eNlaStrip_Flag {
/* NLASTRIP_FLAG_MIRROR = (1 << 13), */ /* UNUSED */
/* temporary editing flags */
/** When transforming strips, this flag is set when the strip is placed in an invalid location
* such as overlapping another strip or moved to a locked track. In such cases, the strip's
* location must be corrected after the transform operator is done. */
NLASTRIP_FLAG_INVALID_LOCATION = (1 << 28),
/** NLA strip should ignore frame range and hold settings, and evaluate at global time. */
NLASTRIP_FLAG_NO_TIME_MAP = (1 << 29),
/** NLA-Strip is really just a temporary meta used to facilitate easier transform code */