diff --git a/source/blender/blenkernel/BKE_grease_pencil.hh b/source/blender/blenkernel/BKE_grease_pencil.hh index b040e6dddf4..70e5b521d4e 100644 --- a/source/blender/blenkernel/BKE_grease_pencil.hh +++ b/source/blender/blenkernel/BKE_grease_pencil.hh @@ -192,6 +192,26 @@ class LayerMask : public ::GreasePencilLayerMask { ~LayerMask(); }; +/** + * Structure used to transform frames in a grease pencil layer. + */ +struct LayerTransformData { + enum FrameTransformationStatus { TRANS_CLEAR, TRANS_INIT, TRANS_RUNNING }; + + /* Map of frame keys describing the transformation of the frames. Keys of the map are the source + * frame indices, and the values of the map are the destination frame indices. */ + Map frames_destination; + + /* Copy of the layer frames map. This allows to display the transformation while running, without + * removing any drawing. */ + Map frames_copy; + /* Map containing the duration (in frames) for each frame in the layer that has a fixed duration, + * i.e. each frame that is not an implicit hold. */ + Map frames_duration; + + FrameTransformationStatus status{TRANS_CLEAR}; +}; + /* The key of a GreasePencilFrame in the frames map is the starting scene frame number (int) of * that frame. */ using FramesMapKey = int; @@ -233,6 +253,9 @@ class LayerRuntime { * A layer can have zero or more layer masks. */ Vector masks_; + + /* Runtime data used for frame transformations.*/ + LayerTransformData trans_data_; }; /** @@ -321,6 +344,12 @@ class Layer : public ::GreasePencilLayer { const GreasePencilFrame *frame_at(const int frame_number) const; GreasePencilFrame *frame_at(const int frame_number); + /** + * \returns the frame duration of the active frame at \a frame_number or -1 if there is no active + * frame or the active frame is the last frame. + */ + int get_frame_duration_at(const int frame_number) const; + void tag_frames_map_changed(); /** diff --git a/source/blender/blenkernel/intern/grease_pencil.cc b/source/blender/blenkernel/intern/grease_pencil.cc index 6ca05eaaa00..2a191d42d06 100644 --- a/source/blender/blenkernel/intern/grease_pencil.cc +++ b/source/blender/blenkernel/intern/grease_pencil.cc @@ -758,6 +758,20 @@ int Layer::drawing_index_at(const int frame_number) const return (frame != nullptr) ? frame->drawing_index : -1; } +int Layer::get_frame_duration_at(const int frame_number) const +{ + const FramesMapKey frame_key = this->frame_key_at(frame_number); + if (frame_key == -1) { + return -1; + } + SortedKeysIterator frame_number_it = std::next(this->sorted_keys().begin(), frame_key); + if (*frame_number_it == this->sorted_keys().last()) { + return -1; + } + const int next_frame_number = *(std::next(frame_number_it)); + return next_frame_number - frame_number; +} + void Layer::tag_frames_map_changed() { this->frames_storage.flag |= GP_LAYER_FRAMES_STORAGE_DIRTY; @@ -1415,19 +1429,6 @@ bool GreasePencil::insert_blank_frame(blender::bke::greasepencil::Layer &layer, return true; } -static int get_frame_duration(const blender::bke::greasepencil::Layer &layer, - const int frame_number) -{ - Span sorted_keys = layer.sorted_keys(); - const int *frame_number_it = std::lower_bound( - sorted_keys.begin(), sorted_keys.end(), frame_number); - if (std::next(frame_number_it) == sorted_keys.end()) { - return 0; - } - const int next_frame_number = *(std::next(frame_number_it)); - return next_frame_number - frame_number; -} - bool GreasePencil::insert_duplicate_frame(blender::bke::greasepencil::Layer &layer, const int src_frame_number, const int dst_frame_number, @@ -1444,8 +1445,9 @@ bool GreasePencil::insert_duplicate_frame(blender::bke::greasepencil::Layer &lay * If we want to make an instance of the source frame, the drawing index gets copied from the * source frame. Otherwise, we set the drawing index to the size of the drawings array, since we * are going to add a new drawing copied from the source drawing. */ - const int duration = src_frame.is_implicit_hold() ? 0 : - get_frame_duration(layer, src_frame_number); + const int duration = src_frame.is_implicit_hold() ? + 0 : + layer.get_frame_duration_at(src_frame_number); const int drawing_index = do_instance ? src_frame.drawing_index : int(this->drawings().size()); GreasePencilFrame *dst_frame = layer.add_frame(dst_frame_number, drawing_index, duration); @@ -1603,6 +1605,47 @@ void GreasePencil::remove_drawings_with_no_users() remove_drawings_unchecked(*this, drawings_to_be_removed.as_span()); } +void GreasePencil::move_frames(blender::bke::greasepencil::Layer &layer, + const blender::Map &frame_number_destinations) +{ + using namespace blender; + + Map layer_frames_copy = layer.frames(); + + /* Remove all frames that have a mapping. */ + for (const int frame_number : frame_number_destinations.keys()) { + layer.remove_frame(frame_number); + } + + /* Insert all frames of the transformation. */ + for (const auto [src_frame_number, dst_frame_number] : frame_number_destinations.items()) { + if (!layer_frames_copy.contains(src_frame_number)) { + continue; + } + + const GreasePencilFrame src_frame = layer_frames_copy.lookup(src_frame_number); + const int drawing_index = src_frame.drawing_index; + const int duration = src_frame.is_implicit_hold() ? + 0 : + layer.get_frame_duration_at(src_frame_number); + + /* Add and overwrite the frame at the destination number. */ + if (layer.frames().contains(dst_frame_number)) { + GreasePencilFrame frame_to_overwrite = layer.frames().lookup(dst_frame_number); + GreasePencilDrawingBase *drawing_base = this->drawings(frame_to_overwrite.drawing_index); + if (drawing_base->type == GP_DRAWING) { + reinterpret_cast(drawing_base)->wrap().remove_user(); + } + layer.remove_frame(dst_frame_number); + } + GreasePencilFrame *frame = layer.add_frame(dst_frame_number, drawing_index, duration); + *frame = src_frame; + } + + /* Remove drawings if they no longer have users. */ + this->remove_drawings_with_no_users(); +} + blender::bke::greasepencil::Drawing *GreasePencil::get_editable_drawing_at( const blender::bke::greasepencil::Layer *layer, const int frame_number) const { diff --git a/source/blender/editors/transform/transform_convert_action.cc b/source/blender/editors/transform/transform_convert_action.cc index d066dea7691..d572699fbde 100644 --- a/source/blender/editors/transform/transform_convert_action.cc +++ b/source/blender/editors/transform/transform_convert_action.cc @@ -19,6 +19,7 @@ #include "BKE_context.h" #include "BKE_fcurve.h" #include "BKE_gpencil_legacy.h" +#include "BKE_grease_pencil.hh" #include "BKE_key.h" #include "BKE_layer.h" #include "BKE_mask.h" @@ -45,6 +46,122 @@ struct tGPFtransdata { int *sdata; /* pointer to gpf->framenum */ }; +/* -------------------------------------------------------------------- */ +/** \name Grease Pencil Transfrom helpers + * \{ */ + +static bool grease_pencil_layer_initialize_trans_data(blender::bke::greasepencil::Layer &layer) +{ + using namespace blender::bke::greasepencil; + LayerTransformData &trans_data = layer.runtime->trans_data_; + + if (trans_data.status != LayerTransformData::TRANS_CLEAR) { + return false; + } + + /* Make a copy of the current frames in the layer. This map will be changed during the + * transformation, and we need to be able to reset it if the operation is cancelled. */ + trans_data.frames_copy = layer.frames(); + trans_data.frames_duration.clear(); + trans_data.frames_destination.clear(); + + for (const auto [frame_number, frame] : layer.frames().items()) { + if (frame.is_null()) { + continue; + } + + /* Store frames' duration to keep them visually correct while moving the frames */ + if (!frame.is_implicit_hold()) { + trans_data.frames_duration.add(frame_number, layer.get_frame_duration_at(frame_number)); + } + } + + trans_data.status = LayerTransformData::TRANS_INIT; + return true; +} + +static bool grease_pencil_layer_reset_trans_data(blender::bke::greasepencil::Layer &layer) +{ + using namespace blender::bke::greasepencil; + LayerTransformData &trans_data = layer.runtime->trans_data_; + + /* If the layer frame map was affected by the transformation, set its status to initialized so + * that the frames map gets reset the next time this modal function is called. + */ + if (trans_data.status == LayerTransformData::TRANS_CLEAR) { + return false; + } + trans_data.status = LayerTransformData::TRANS_INIT; + return true; +} + +static bool grease_pencil_layer_update_trans_data(blender::bke::greasepencil::Layer &layer, + const int src_frame_number, + const int dst_frame_number) +{ + using namespace blender::bke::greasepencil; + LayerTransformData &trans_data = layer.runtime->trans_data_; + + if (trans_data.status == LayerTransformData::TRANS_CLEAR) { + return false; + } + + if (trans_data.status == LayerTransformData::TRANS_INIT) { + /* The transdata was only initialized. No transformation was applied yet. + * The frame mapping is always defined relatively to the initial frame map, so we first need + * to set the frames back to its initial state before applying any frame transformation. */ + layer.frames_for_write() = trans_data.frames_copy; + layer.tag_frames_map_keys_changed(); + trans_data.status = LayerTransformData::TRANS_RUNNING; + } + + /* Apply the transformation directly in the frame map, so that we display the transformed + * frame numbers. We don't want to edit the frames or remove any drawing here. This will be + * done at once at the end of the transformation. */ + const GreasePencilFrame src_frame = trans_data.frames_copy.lookup(src_frame_number); + const int src_duration = trans_data.frames_duration.lookup_default(src_frame_number, 0); + layer.remove_frame(src_frame_number); + layer.remove_frame(dst_frame_number); + GreasePencilFrame *frame = layer.add_frame( + dst_frame_number, src_frame.drawing_index, src_duration); + *frame = src_frame; + + trans_data.frames_destination.add_overwrite(src_frame_number, dst_frame_number); + + return true; +} + +static bool grease_pencil_layer_apply_trans_data(GreasePencil &grease_pencil, + blender::bke::greasepencil::Layer &layer, + const bool canceled) +{ + using namespace blender::bke::greasepencil; + LayerTransformData &trans_data = layer.runtime->trans_data_; + + if (trans_data.status == LayerTransformData::TRANS_CLEAR) { + /* The layer was not affected by the transformation, so do nothing. */ + return false; + } + + /* Reset the frames to their initial state. */ + layer.frames_for_write() = trans_data.frames_copy; + layer.tag_frames_map_keys_changed(); + + if (!canceled) { + /* Apply the transformation. */ + grease_pencil.move_frames(layer, trans_data.frames_destination); + } + + /* Clear the frames copy. */ + trans_data.frames_copy.clear(); + trans_data.frames_destination.clear(); + trans_data.status = LayerTransformData::TRANS_CLEAR; + + return true; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Action Transform Creation * \{ */ @@ -103,6 +220,35 @@ static int count_gplayer_frames(bGPDlayer *gpl, char side, float cfra, bool is_p return count; } +static int count_grease_pencil_frames(const blender::bke::greasepencil::Layer *layer, + char side, + float cfra, + bool is_prop_edit) +{ + if (layer == nullptr) { + return 0; + } + + int count_selected = 0; + int count_all = 0; + + /* Only include points that occur on the right side of cfra. */ + for (const auto &[frame_number, frame] : layer->frames().items()) { + if (!FrameOnMouseSide(side, float(frame_number), cfra)) { + continue; + } + if (frame.is_selected()) { + count_selected++; + } + count_all++; + } + + if (is_prop_edit && count_selected > 0) { + return count_all; + } + return count_selected; +} + /* fully select selected beztriples, but only include if it's on the right side of cfra */ static int count_masklayer_frames(MaskLayer *masklay, char side, float cfra, bool is_prop_edit) { @@ -256,6 +402,69 @@ static int GPLayerToTransData(TransData *td, return count; } +/** + * Fills \a td and \a td2d with transform data for each frame of the grease pencil \a layer that is + * on the \a side of the frame \a cfra. It also updates the runtime data of the \a layer to keep + * track of the transform. This is why the \a layer is not const here. + */ +static int GreasePencilLayerToTransData(TransData *td, + TransData2D *td2d, + blender::bke::greasepencil::Layer *layer, + char side, + float cfra, + bool is_prop_edit, + float ypos) +{ + using namespace blender; + using namespace bke::greasepencil; + + int total_trans_frames = 0; + bool any_frame_affected = false; + for (auto [frame_number, frame] : layer->frames().items()) { + /* We only add transform data for selected frames that are on the right side of current frame. + * If proportional edit is set, then we should also account for non selected frames. + */ + if ((!is_prop_edit && !frame.is_selected()) || !FrameOnMouseSide(side, frame_number, cfra)) { + continue; + } + any_frame_affected = true; + + td2d->loc[0] = float(frame_number); + + td->val = td->loc = &td2d->loc[0]; + td->ival = td->iloc[0] = td2d->loc[0]; + + td->center[0] = td->ival; + td->center[1] = ypos; + + if (frame.is_selected()) { + td->flag |= TD_SELECTED; + } + /* Set a pointer to the layer in the transform data so that we can apply the transformation + * while the operator is running. + */ + td->flag |= TD_GREASE_PENCIL_FRAME; + td->extra = layer; + + /* Advance `td` now. */ + td++; + td2d++; + total_trans_frames++; + } + + if (total_trans_frames == 0) { + return total_trans_frames; + } + + /* If it was not previously done, initialize the transform data in the layer, and if some frames + * are actually concerned by the transform. */ + if (any_frame_affected) { + grease_pencil_layer_initialize_trans_data(*layer); + } + + return total_trans_frames; +} + /* refer to comment above #GPLayerToTransData, this is the same but for masks */ static int MaskLayerToTransData(TransData *td, tGPFtransdata *tfd, @@ -358,7 +567,9 @@ static void createTransActionData(bContext *C, TransInfo *t) static_cast(ale->data), t->frame_side, cfra, is_prop_edit); } else if (ale->type == ANIMTYPE_GREASE_PENCIL_LAYER) { - /* GPv3: To be implemented. */ + using namespace blender::bke::greasepencil; + adt_count = count_grease_pencil_frames( + static_cast(ale->data), t->frame_side, cfra, is_prop_edit); } else if (ale->type == ANIMTYPE_MASKLAYER) { adt_count = count_masklayer_frames( @@ -428,6 +639,15 @@ static void createTransActionData(bContext *C, TransInfo *t) td += i; tfd += i; } + else if (ale->type == ANIMTYPE_GREASE_PENCIL_LAYER) { + using namespace blender::bke::greasepencil; + Layer *layer = static_cast(ale->data); + int i; + + i = GreasePencilLayerToTransData(td, td2d, layer, t->frame_side, cfra, is_prop_edit, ypos); + td += i; + td2d += i; + } else if (ale->type == ANIMTYPE_MASKLAYER) { MaskLayer *masklay = (MaskLayer *)ale->data; int i; @@ -488,6 +708,32 @@ static void createTransActionData(bContext *C, TransInfo *t) td++; } } + else if (ale->type == ANIMTYPE_GREASE_PENCIL_LAYER) { + using namespace blender::bke::greasepencil; + Layer *layer = static_cast(ale->data); + + for (auto [frame_number, frame] : layer->frames_for_write().items()) { + if (frame.is_selected()) { + td->dist = td->rdist = 0.0f; + ++td; + continue; + } + + int closest_selected = INT_MAX; + for (auto [neighbor_frame_number, neighbor_frame] : layer->frames_for_write().items()) { + if (!neighbor_frame.is_selected() || + !FrameOnMouseSide(t->frame_side, float(neighbor_frame_number), cfra)) + { + continue; + } + const int distance = abs(neighbor_frame_number - frame_number); + closest_selected = std::min(closest_selected, distance); + } + + td->dist = td->rdist = closest_selected; + ++td; + } + } else if (ale->type == ANIMTYPE_MASKLAYER) { MaskLayer *masklay = (MaskLayer *)ale->data; @@ -557,7 +803,8 @@ static void createTransActionData(bContext *C, TransInfo *t) /** \name Action Transform Flush * \{ */ -/* This function helps flush transdata written to tempdata into the gp-frames. */ +/* (Grease Pencil Legacy) + * This function helps flush transdata written to tempdata into the gp-frames. */ static void flushTransIntFrameActionData(TransInfo *t) { TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t); @@ -616,6 +863,13 @@ static void recalcData_actedit(TransInfo *t) td->loc[1] = td->iloc[1]; transform_convert_flush_handle2D(td, td2d, 0.0f); + + if ((t->state == TRANS_RUNNING) && ((td->flag & TD_GREASE_PENCIL_FRAME) != 0)) { + grease_pencil_layer_update_trans_data( + *static_cast(td->extra), + round_fl_to_int(td->ival), + round_fl_to_int(td2d->loc[0])); + } } if (ac.datatype != ANIMCONT_MASK) { @@ -633,10 +887,25 @@ static void recalcData_actedit(TransInfo *t) /* set refresh tags for objects using this animation */ ANIM_list_elem_update(CTX_data_main(t->context), t->scene, ale); } + + /* now free temp channels */ + ANIM_animdata_freelist(&anim_data); } - /* now free temp channels */ - ANIM_animdata_freelist(&anim_data); + if (ac.datatype == ANIMCONT_GPENCIL) { + filter = ANIMFILTER_DATA_VISIBLE; + ANIM_animdata_filter( + &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype)); + + LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { + if (ale->type != ANIMTYPE_GREASE_PENCIL_LAYER) { + continue; + } + grease_pencil_layer_reset_trans_data( + *static_cast(ale->data)); + } + ANIM_animdata_freelist(&anim_data); + } } } @@ -865,20 +1134,35 @@ static void special_aftertrans_update__actedit(bContext *C, TransInfo *t) * 3) canceled + duplicate -> user canceled the transform, * but we made duplicates, so get rid of these */ - if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) { - ListBase anim_data = {nullptr, nullptr}; - const int filter = ANIMFILTER_DATA_VISIBLE; - ANIM_animdata_filter( - &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype)); + ListBase anim_data = {nullptr, nullptr}; + const int filter = ANIMFILTER_DATA_VISIBLE; + ANIM_animdata_filter( + &ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype)); - LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { - if (ale->datatype == ALE_GPFRAME) { - ale->id->tag &= ~LIB_TAG_DOIT; - posttrans_gpd_clean((bGPdata *)ale->id); + LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) { + switch (ale->datatype) { + case ALE_GPFRAME: + /* Grease Pencil legacy. */ + if ((saction->flag & SACTION_NOTRANSKEYCULL) == 0 && ((canceled == 0) || (duplicate))) { + ale->id->tag &= ~LIB_TAG_DOIT; + posttrans_gpd_clean((bGPdata *)ale->id); + } + break; + + case ALE_GREASE_PENCIL_CEL: { + GreasePencil *grease_pencil = reinterpret_cast(ale->id); + grease_pencil_layer_apply_trans_data( + *grease_pencil, + *static_cast(ale->data), + canceled); + break; } + + default: + break; } - ANIM_animdata_freelist(&anim_data); } + ANIM_animdata_freelist(&anim_data); } else if (ac.datatype == ANIMCONT_MASK) { /* remove duplicate frames and also make sure points are in order! */ diff --git a/source/blender/editors/transform/transform_data.hh b/source/blender/editors/transform/transform_data.hh index 860cdddc455..83bdb66b65c 100644 --- a/source/blender/editors/transform/transform_data.hh +++ b/source/blender/editors/transform/transform_data.hh @@ -186,6 +186,8 @@ enum { TD_PBONE_LOCAL_MTX_P = 1 << 17, /** Same as above but for a child bone. */ TD_PBONE_LOCAL_MTX_C = 1 << 18, + /* Grease pencil layer frames. */ + TD_GREASE_PENCIL_FRAME = 1 << 19, }; /* Hard min/max for proportional size. */ diff --git a/source/blender/makesdna/DNA_grease_pencil_types.h b/source/blender/makesdna/DNA_grease_pencil_types.h index c0bbd6f0849..9c8ecad1b98 100644 --- a/source/blender/makesdna/DNA_grease_pencil_types.h +++ b/source/blender/makesdna/DNA_grease_pencil_types.h @@ -504,6 +504,19 @@ typedef struct GreasePencil { */ void remove_drawings_with_no_users(); + /** + * Move a set of frames in a \a layer. + * + * \param frame_number_destinations describes all transformations that should be applied on the + * frame keys. + * + * If a transformation overlaps another frames, the frame will be overwritten, and the + * corresponding drawing may be removed, if it no longer has users. + * + */ + void move_frames(blender::bke::greasepencil::Layer &layer, + const blender::Map &frame_number_destinations); + /** * Returns an editable drawing on \a layer at frame \a frame_number or `nullptr` if no such * drawing exists.