tornavis/source/blender/editors/transform/transform_convert_sequencer.cc

729 lines
22 KiB
C++

/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edtransform
*/
#include "DNA_space_types.h"
#include "MEM_guardedalloc.h"
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_vector.h"
#include "BKE_context.hh"
#include "BKE_main.h"
#include "BKE_report.h"
#include "ED_markers.hh"
#include "ED_time_scrub_ui.hh"
#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"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "SEQ_utils.hh"
#include "UI_view2d.hh"
#include "transform.hh"
#include "transform_convert.hh"
#define SEQ_EDGE_PAN_INSIDE_PAD 3.5
#define SEQ_EDGE_PAN_OUTSIDE_PAD 0 /* Disable clamping for panning, use whole screen. */
#define SEQ_EDGE_PAN_SPEED_RAMP 1
#define SEQ_EDGE_PAN_MAX_SPEED 4 /* In UI units per second, slower than default. */
#define SEQ_EDGE_PAN_DELAY 1.0f
#define SEQ_EDGE_PAN_ZOOM_INFLUENCE 0.5f
/** Used for sequencer transform. */
struct TransDataSeq {
Sequence *seq;
/** A copy of #Sequence.flag that may be modified for nested strips. */
int flag;
/** Use this so we can have transform data at the strips start,
* but apply correctly to the start frame. */
int start_offset;
/** one of #SELECT, #SEQ_LEFTSEL and #SEQ_RIGHTSEL. */
short sel_flag;
};
/**
* Sequencer transform customdata (stored in #TransCustomDataContainer).
*/
struct TransSeq {
TransDataSeq *tdseq;
int selection_channel_range_min;
int selection_channel_range_max;
/* Initial rect of the view2d, used for computing offset during edge panning */
rctf initial_v2d_cur;
View2DEdgePanData edge_pan;
/* Strips that aren't selected, but their position entirely depends on transformed strips. */
blender::VectorSet<Sequence *> time_dependent_strips;
};
/* -------------------------------------------------------------------- */
/** \name Sequencer Transform Creation
* \{ */
/* This function applies the rules for transforming a strip so duplicate
* checks don't need to be added in multiple places.
*
* count and flag MUST be set.
*/
static void SeqTransInfo(TransInfo *t, Sequence *seq, int *r_count, int *r_flag)
{
Scene *scene = t->scene;
Editing *ed = SEQ_editing_get(t->scene);
ListBase *channels = SEQ_channels_displayed_get(ed);
/* for extend we need to do some tricks */
if (t->mode == TFM_TIME_EXTEND) {
/* *** Extend Transform *** */
int cfra = scene->r.cfra;
int left = SEQ_time_left_handle_frame_get(scene, seq);
int right = SEQ_time_right_handle_frame_get(scene, seq);
if ((seq->flag & SELECT) == 0 || SEQ_transform_is_locked(channels, seq)) {
*r_count = 0;
*r_flag = 0;
}
else {
*r_count = 1; /* unless its set to 0, extend will never set 2 handles at once */
*r_flag = (seq->flag | SELECT) & ~(SEQ_LEFTSEL | SEQ_RIGHTSEL);
if (t->frame_side == 'R') {
if (right <= cfra) {
*r_count = *r_flag = 0;
} /* ignore */
else if (left > cfra) {
} /* keep the selection */
else {
*r_flag |= SEQ_RIGHTSEL;
}
}
else {
if (left >= cfra) {
*r_count = *r_flag = 0;
} /* ignore */
else if (right < cfra) {
} /* keep the selection */
else {
*r_flag |= SEQ_LEFTSEL;
}
}
}
}
else {
t->frame_side = 'B';
/* *** Normal Transform *** */
/* Count */
/* Non nested strips (reset selection and handles). */
if ((seq->flag & SELECT) == 0 || SEQ_transform_is_locked(channels, seq)) {
*r_count = 0;
*r_flag = 0;
}
else {
if ((seq->flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL)) == (SEQ_LEFTSEL | SEQ_RIGHTSEL)) {
*r_flag = seq->flag;
*r_count = 2; /* we need 2 transdata's */
}
else {
*r_flag = seq->flag;
*r_count = 1; /* selected or with a handle selected */
}
}
}
}
static int SeqTransCount(TransInfo *t, ListBase *seqbase)
{
int tot = 0, count, flag;
LISTBASE_FOREACH (Sequence *, seq, seqbase) {
SeqTransInfo(t, seq, &count, &flag); /* ignore the flag */
tot += count;
}
return tot;
}
static TransData *SeqToTransData(Scene *scene,
TransData *td,
TransData2D *td2d,
TransDataSeq *tdsq,
Sequence *seq,
int flag,
int sel_flag)
{
int start_left;
switch (sel_flag) {
case SELECT:
/* Use seq_tx_get_final_left() and an offset here
* so transform has the left hand location of the strip.
* tdsq->start_offset is used when flushing the tx data back */
start_left = SEQ_time_left_handle_frame_get(scene, seq);
td2d->loc[0] = start_left;
tdsq->start_offset = start_left - seq->start; /* use to apply the original location */
break;
case SEQ_LEFTSEL:
start_left = SEQ_time_left_handle_frame_get(scene, seq);
td2d->loc[0] = start_left;
break;
case SEQ_RIGHTSEL:
td2d->loc[0] = SEQ_time_right_handle_frame_get(scene, seq);
break;
}
td2d->loc[1] = seq->machine; /* channel - Y location */
td2d->loc[2] = 0.0f;
td2d->loc2d = nullptr;
tdsq->seq = seq;
/* Use instead of seq->flag for nested strips and other
* cases where the selection may need to be modified */
tdsq->flag = flag;
tdsq->sel_flag = sel_flag;
td->extra = (void *)tdsq; /* allow us to update the strip from here */
td->flag = 0;
td->loc = td2d->loc;
copy_v3_v3(td->center, td->loc);
copy_v3_v3(td->iloc, td->loc);
memset(td->axismtx, 0, sizeof(td->axismtx));
td->axismtx[2][2] = 1.0f;
td->ext = nullptr;
td->val = nullptr;
td->flag |= TD_SELECTED;
td->dist = 0.0;
unit_m3(td->mtx);
unit_m3(td->smtx);
/* Time Transform (extend) */
td->val = td2d->loc;
td->ival = td2d->loc[0];
return td;
}
static int SeqToTransData_build(
TransInfo *t, ListBase *seqbase, TransData *td, TransData2D *td2d, TransDataSeq *tdsq)
{
Scene *scene = t->scene;
int count, flag;
int tot = 0;
LISTBASE_FOREACH (Sequence *, seq, seqbase) {
SeqTransInfo(t, seq, &count, &flag);
/* use 'flag' which is derived from seq->flag but modified for special cases */
if (flag & SELECT) {
if (flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL)) {
if (flag & SEQ_LEFTSEL) {
SeqToTransData(scene, td++, td2d++, tdsq++, seq, flag, SEQ_LEFTSEL);
tot++;
}
if (flag & SEQ_RIGHTSEL) {
SeqToTransData(scene, td++, td2d++, tdsq++, seq, flag, SEQ_RIGHTSEL);
tot++;
}
}
else {
SeqToTransData(scene, td++, td2d++, tdsq++, seq, flag, SELECT);
tot++;
}
}
}
return tot;
}
static void free_transform_custom_data(TransCustomData *custom_data)
{
if ((custom_data->data != nullptr) && custom_data->use_free) {
TransSeq *ts = static_cast<TransSeq *>(custom_data->data);
MEM_freeN(ts->tdseq);
MEM_delete(ts);
custom_data->data = nullptr;
}
}
/* Canceled, need to update the strips display. */
static void seq_transform_cancel(TransInfo *t, blender::Span<Sequence *> transformed_strips)
{
ListBase *seqbase = SEQ_active_seqbase_get(SEQ_editing_get(t->scene));
for (Sequence *seq : transformed_strips) {
/* Handle pre-existing overlapping strips even when operator is canceled.
* This is necessary for SEQUENCER_OT_duplicate_move macro for example. */
if (SEQ_transform_test_overlap(t->scene, seqbase, seq)) {
SEQ_transform_seqbase_shuffle(seqbase, seq, t->scene);
}
}
}
static ListBase *seqbase_active_get(const TransInfo *t)
{
Editing *ed = SEQ_editing_get(t->scene);
return SEQ_active_seqbase_get(ed);
}
static bool seq_transform_check_overlap(blender::Span<Sequence *> transformed_strips)
{
for (Sequence *seq : transformed_strips) {
if (seq->flag & SEQ_OVERLAP) {
return true;
}
}
return false;
}
static blender::VectorSet<Sequence *> seq_transform_collection_from_transdata(
TransDataContainer *tc)
{
blender::VectorSet<Sequence *> strips;
TransData *td = tc->data;
for (int a = 0; a < tc->data_len; a++, td++) {
Sequence *seq = ((TransDataSeq *)td->extra)->seq;
strips.add(seq);
}
return strips;
}
static void freeSeqData(TransInfo *t, TransDataContainer *tc, TransCustomData *custom_data)
{
Editing *ed = SEQ_editing_get(t->scene);
if (ed == nullptr) {
free_transform_custom_data(custom_data);
return;
}
blender::VectorSet transformed_strips = seq_transform_collection_from_transdata(tc);
SEQ_iterator_set_expand(
t->scene, seqbase_active_get(t), transformed_strips, SEQ_query_strip_effect_chain);
for (Sequence *seq : transformed_strips) {
seq->flag &= ~SEQ_IGNORE_CHANNEL_LOCK;
}
if (t->state == TRANS_CANCEL) {
seq_transform_cancel(t, transformed_strips);
free_transform_custom_data(custom_data);
return;
}
TransSeq *ts = static_cast<TransSeq *>(tc->custom.type.data);
ListBase *seqbasep = seqbase_active_get(t);
Scene *scene = t->scene;
const bool use_sync_markers = (((SpaceSeq *)t->area->spacedata.first)->flag &
SEQ_MARKER_TRANS) != 0;
if (seq_transform_check_overlap(transformed_strips)) {
SEQ_transform_handle_overlap(
scene, seqbasep, transformed_strips, ts->time_dependent_strips, use_sync_markers);
}
DEG_id_tag_update(&t->scene->id, ID_RECALC_SEQUENCER_STRIPS);
free_transform_custom_data(custom_data);
}
static blender::VectorSet<Sequence *> query_selected_strips_no_handles(ListBase *seqbase)
{
blender::VectorSet<Sequence *> strips;
LISTBASE_FOREACH (Sequence *, seq, seqbase) {
if ((seq->flag & SELECT) != 0 && ((seq->flag & (SEQ_LEFTSEL | SEQ_RIGHTSEL)) == 0)) {
strips.add(seq);
}
}
return strips;
}
enum SeqInputSide {
SEQ_INPUT_LEFT = -1,
SEQ_INPUT_RIGHT = 1,
};
static Sequence *effect_input_get(const Scene *scene, Sequence *effect, SeqInputSide side)
{
Sequence *input = effect->seq1;
if (effect->seq2 && (SEQ_time_left_handle_frame_get(scene, effect->seq2) -
SEQ_time_left_handle_frame_get(scene, effect->seq1)) *
side >
0)
{
input = effect->seq2;
}
return input;
}
static Sequence *effect_base_input_get(const Scene *scene, Sequence *effect, SeqInputSide side)
{
Sequence *input = effect, *seq_iter = effect;
while (seq_iter != nullptr) {
input = seq_iter;
seq_iter = effect_input_get(scene, seq_iter, side);
}
return input;
}
/**
* Strips that aren't stime_dependent_stripselected, but their position entirely depends on
* transformed strips. This collection is used to offset animation.
*/
static void query_time_dependent_strips_strips(
TransInfo *t, blender::VectorSet<Sequence *> &time_dependent_strips)
{
ListBase *seqbase = seqbase_active_get(t);
/* Query dependent strips where used strips do not have handles selected.
* If all inputs of any effect even indirectly(through another effect) points to selected strip,
* its position will change. */
blender::VectorSet<Sequence *> strips_no_handles = query_selected_strips_no_handles(seqbase);
time_dependent_strips.add_multiple(strips_no_handles);
SEQ_iterator_set_expand(t->scene, seqbase, strips_no_handles, SEQ_query_strip_effect_chain);
bool strip_added = true;
while (strip_added) {
strip_added = false;
for (Sequence *seq : strips_no_handles) {
if (time_dependent_strips.contains(seq)) {
continue; /* Strip is already in collection, skip it. */
}
/* If both seq1 and seq2 exist, both must be selected. */
if (seq->seq1 && time_dependent_strips.contains(seq->seq1)) {
if (seq->seq2 && !time_dependent_strips.contains(seq->seq2)) {
continue;
}
strip_added = true;
time_dependent_strips.add(seq);
}
}
}
/* Query dependent strips where used strips do have handles selected.
* If any 2-input effect changes position because handles were moved, animation should be offset.
* With single input effect, it is less likely desirable to move animation. */
blender::VectorSet selected_strips = SEQ_query_selected_strips(seqbase);
SEQ_iterator_set_expand(t->scene, seqbase, selected_strips, SEQ_query_strip_effect_chain);
for (Sequence *seq : selected_strips) {
/* Check only 2 input effects. */
if (seq->seq1 == nullptr || seq->seq2 == nullptr) {
continue;
}
/* Find immediate base inputs(left and right side). */
Sequence *input_left = effect_base_input_get(t->scene, seq, SEQ_INPUT_LEFT);
Sequence *input_right = effect_base_input_get(t->scene, seq, SEQ_INPUT_RIGHT);
if ((input_left->flag & SEQ_RIGHTSEL) != 0 && (input_right->flag & SEQ_LEFTSEL) != 0) {
time_dependent_strips.add(seq);
}
}
/* Remove all non-effects. */
time_dependent_strips.remove_if(
[&](Sequence *seq) { return SEQ_transform_sequence_can_be_translated(seq); });
}
static void createTransSeqData(bContext * /*C*/, TransInfo *t)
{
Scene *scene = t->scene;
Editing *ed = SEQ_editing_get(t->scene);
TransData *td = nullptr;
TransData2D *td2d = nullptr;
TransDataSeq *tdsq = nullptr;
TransSeq *ts = nullptr;
int count = 0;
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
if (ed == nullptr) {
tc->data_len = 0;
return;
}
/* Disable cursor wrapping for edge pan. */
if (t->mode == TFM_TRANSLATION) {
t->flag |= T_NO_CURSOR_WRAP;
}
tc->custom.type.free_cb = freeSeqData;
t->frame_side = transform_convert_frame_side_dir_get(t, float(scene->r.cfra));
count = SeqTransCount(t, ed->seqbasep);
/* allocate memory for data */
tc->data_len = count;
/* stop if trying to build list if nothing selected */
if (count == 0) {
return;
}
tc->custom.type.data = ts = MEM_new<TransSeq>(__func__);
tc->custom.type.use_free = true;
td = tc->data = static_cast<TransData *>(
MEM_callocN(tc->data_len * sizeof(TransData), "TransSeq TransData"));
td2d = tc->data_2d = static_cast<TransData2D *>(
MEM_callocN(tc->data_len * sizeof(TransData2D), "TransSeq TransData2D"));
ts->tdseq = tdsq = static_cast<TransDataSeq *>(
MEM_callocN(tc->data_len * sizeof(TransDataSeq), "TransSeq TransDataSeq"));
/* Custom data to enable edge panning during transformation. */
UI_view2d_edge_pan_init(t->context,
&ts->edge_pan,
SEQ_EDGE_PAN_INSIDE_PAD,
SEQ_EDGE_PAN_OUTSIDE_PAD,
SEQ_EDGE_PAN_SPEED_RAMP,
SEQ_EDGE_PAN_MAX_SPEED,
SEQ_EDGE_PAN_DELAY,
SEQ_EDGE_PAN_ZOOM_INFLUENCE);
UI_view2d_edge_pan_set_limits(&ts->edge_pan, -FLT_MAX, FLT_MAX, 1, MAXSEQ + 1);
ts->initial_v2d_cur = t->region->v2d.cur;
/* loop 2: build transdata array */
SeqToTransData_build(t, ed->seqbasep, td, td2d, tdsq);
ts->selection_channel_range_min = MAXSEQ + 1;
LISTBASE_FOREACH (Sequence *, seq, SEQ_active_seqbase_get(ed)) {
if ((seq->flag & SELECT) != 0) {
ts->selection_channel_range_min = min_ii(ts->selection_channel_range_min, seq->machine);
ts->selection_channel_range_max = max_ii(ts->selection_channel_range_max, seq->machine);
}
}
query_time_dependent_strips_strips(t, ts->time_dependent_strips);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name UVs Transform Flush
* \{ */
static void view2d_edge_pan_loc_compensate(TransInfo *t, float loc_in[2], float r_loc[2])
{
TransSeq *ts = (TransSeq *)TRANS_DATA_CONTAINER_FIRST_SINGLE(t)->custom.type.data;
/* Initial and current view2D rects for additional transform due to view panning and zooming */
const rctf *rect_src = &ts->initial_v2d_cur;
const rctf *rect_dst = &t->region->v2d.cur;
if (t->options & CTX_VIEW2D_EDGE_PAN) {
if (t->state == TRANS_CANCEL) {
UI_view2d_edge_pan_cancel(t->context, &ts->edge_pan);
}
else {
/* Edge panning functions expect window coordinates, mval is relative to region */
const int xy[2] = {
t->region->winrct.xmin + int(t->mval[0]),
t->region->winrct.ymin + int(t->mval[1]),
};
UI_view2d_edge_pan_apply(t->context, &ts->edge_pan, xy);
}
}
copy_v2_v2(r_loc, loc_in);
/* Additional offset due to change in view2D rect. */
BLI_rctf_transform_pt_v(rect_dst, rect_src, r_loc, r_loc);
}
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;
Sequence *seq;
Scene *scene = t->scene;
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
/* 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
* works fairly well in most scenarios, but there can be some edge cases.
*
* Better solution would be to store effect position and calculate real offset. However with many
* (>5) effects in chain, there is visible lag in strip position update, because during
* recalculation, hierarchy is not taken into account. */
int max_offset = 0;
/* 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;
seq = tdsq->seq;
float loc[2];
view2d_edge_pan_loc_compensate(t, td->loc, loc);
new_frame = round_fl_to_int(loc[0]);
switch (tdsq->sel_flag) {
case SELECT: {
if (SEQ_transform_sequence_can_be_translated(seq)) {
offset = new_frame - tdsq->start_offset - seq->start;
SEQ_transform_translate_sequence(scene, seq, offset);
if (abs(offset) > abs(max_offset)) {
max_offset = offset;
}
}
seq->machine = round_fl_to_int(loc[1]);
CLAMP(seq->machine, 1, MAXSEQ);
break;
}
case SEQ_LEFTSEL: { /* No vertical transform. */
int old_startdisp = SEQ_time_left_handle_frame_get(scene, seq);
SEQ_time_left_handle_frame_set(t->scene, seq, new_frame);
if (abs(SEQ_time_left_handle_frame_get(scene, seq) - old_startdisp) > abs(max_offset)) {
max_offset = SEQ_time_left_handle_frame_get(scene, seq) - old_startdisp;
}
break;
}
case SEQ_RIGHTSEL: { /* No vertical transform. */
int old_enddisp = SEQ_time_right_handle_frame_get(scene, seq);
SEQ_time_right_handle_frame_set(t->scene, seq, new_frame);
if (abs(SEQ_time_right_handle_frame_get(scene, seq) - old_enddisp) > abs(max_offset)) {
max_offset = SEQ_time_right_handle_frame_get(scene, seq) - old_enddisp;
}
break;
}
}
}
TransSeq *ts = (TransSeq *)TRANS_DATA_CONTAINER_FIRST_SINGLE(t)->custom.type.data;
/* Update animation for effects. */
for (Sequence *seq : ts->time_dependent_strips) {
SEQ_offset_animdata(t->scene, seq, max_offset);
}
/* need to do the overlap check in a new loop otherwise adjacent strips
* will not be updated and we'll get false positives */
blender::VectorSet transformed_strips = seq_transform_collection_from_transdata(tc);
SEQ_iterator_set_expand(
t->scene, seqbase_active_get(t), transformed_strips, SEQ_query_strip_effect_chain);
for (Sequence *seq : transformed_strips) {
/* test overlap, displays red outline */
seq->flag &= ~SEQ_OVERLAP;
if (SEQ_transform_test_overlap(scene, seqbasep, seq)) {
seq->flag |= SEQ_OVERLAP;
}
}
}
static void recalcData_sequencer(TransInfo *t)
{
TransData *td;
int a;
Sequence *seq_prev = nullptr;
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
for (a = 0, td = tc->data; a < tc->data_len; a++, td++) {
TransDataSeq *tdsq = (TransDataSeq *)td->extra;
Sequence *seq = tdsq->seq;
if (seq != seq_prev) {
SEQ_relations_invalidate_cache_composite(t->scene, seq);
}
seq_prev = seq;
}
DEG_id_tag_update(&t->scene->id, ID_RECALC_SEQUENCER_STRIPS);
flushTransSeq(t);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Special After Transform Sequencer
* \{ */
static void special_aftertrans_update__sequencer(bContext * /*C*/, TransInfo *t)
{
if (t->state == TRANS_CANCEL) {
return;
}
/* freeSeqData in transform_conversions.c does this
* keep here so the else at the end won't run... */
SpaceSeq *sseq = (SpaceSeq *)t->area->spacedata.first;
/* Marker transform, not especially nice but we may want to move markers
* at the same time as strips in the Video Sequencer. */
if (sseq->flag & SEQ_MARKER_TRANS) {
/* can't use TFM_TIME_EXTEND
* for some reason EXTEND is changed into TRANSLATE, so use frame_side instead */
if (t->mode == TFM_SEQ_SLIDE) {
if (t->frame_side == 'B') {
ED_markers_post_apply_transform(
&t->scene->markers, t->scene, TFM_TIME_TRANSLATE, t->values_final[0], t->frame_side);
}
}
else if (ELEM(t->frame_side, 'L', 'R')) {
ED_markers_post_apply_transform(
&t->scene->markers, t->scene, TFM_TIME_EXTEND, t->values_final[0], t->frame_side);
}
}
}
void transform_convert_sequencer_channel_clamp(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;
if (max_channel_after_transform > MAXSEQ) {
r_val[1] -= max_channel_after_transform - MAXSEQ;
}
if (min_channel_after_transform < 1) {
r_val[1] -= min_channel_after_transform - 1;
}
}
/** \} */
TransConvertTypeInfo TransConvertType_Sequencer = {
/*flags*/ (T_POINTS | T_2D_EDIT),
/*create_trans_data*/ createTransSeqData,
/*recalc_data*/ recalcData_sequencer,
/*special_aftertrans_update*/ special_aftertrans_update__sequencer,
};