From 127eee87ce31fc827d240be109bd33ccb2371d09 Mon Sep 17 00:00:00 2001 From: Amelie Fondevilla Date: Tue, 12 Sep 2023 12:07:15 +0200 Subject: [PATCH] GPv3: Duplicate keyframes Implementation of the duplicate and move operator for grease pencil frames. The `ACTION_OT_duplicate` operator now creates duplicates of the selected frames and stores them in the layer runtime data. The `TRANSFORM_OT_transform` operator is updated to take care of these duplicated frames. Pull Request: https://projects.blender.org/blender/blender/pulls/111051 --- .../blender/blenkernel/BKE_grease_pencil.hh | 4 + .../blenkernel/intern/grease_pencil.cc | 37 +++-- .../intern/grease_pencil_frames.cc | 33 ++++ .../editors/include/ED_grease_pencil.hh | 5 + .../editors/space_action/action_edit.cc | 4 +- .../transform/transform_convert_action.cc | 141 +++++++++++++----- .../makesdna/DNA_grease_pencil_types.h | 16 ++ 7 files changed, 194 insertions(+), 46 deletions(-) diff --git a/source/blender/blenkernel/BKE_grease_pencil.hh b/source/blender/blenkernel/BKE_grease_pencil.hh index c21396a01df..5bd517dd881 100644 --- a/source/blender/blenkernel/BKE_grease_pencil.hh +++ b/source/blender/blenkernel/BKE_grease_pencil.hh @@ -260,6 +260,10 @@ struct LayerTransformData { * i.e. each frame that is not an implicit hold. */ Map frames_duration; + /* Temporary copy of duplicated frames before we decide on a place to insert them. + * Used in the move+duplicate operator. */ + Map temp_frames_buffer; + FrameTransformationStatus status{TRANS_CLEAR}; }; diff --git a/source/blender/blenkernel/intern/grease_pencil.cc b/source/blender/blenkernel/intern/grease_pencil.cc index c538896ea7e..baf2c66699d 100644 --- a/source/blender/blenkernel/intern/grease_pencil.cc +++ b/source/blender/blenkernel/intern/grease_pencil.cc @@ -1564,26 +1564,43 @@ void GreasePencil::remove_drawings_with_no_users() void GreasePencil::move_frames(blender::bke::greasepencil::Layer &layer, const blender::Map &frame_number_destinations) { - using namespace blender; + return this->move_duplicate_frames( + layer, frame_number_destinations, blender::Map()); +} +void GreasePencil::move_duplicate_frames( + blender::bke::greasepencil::Layer &layer, + const blender::Map &frame_number_destinations, + const blender::Map &duplicate_frames) +{ + 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); + /* Copy frames durations. */ + Map layer_frames_durations; + for (const auto [frame_number, frame] : layer.frames().items()) { + if (!frame.is_implicit_hold()) { + layer_frames_durations.add(frame_number, layer.get_frame_duration_at(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)) { + const bool use_duplicate = duplicate_frames.contains(src_frame_number); + + const Map &frame_map = use_duplicate ? duplicate_frames : + layer_frames_copy; + + if (!frame_map.contains(src_frame_number)) { continue; } - const GreasePencilFrame src_frame = layer_frames_copy.lookup(src_frame_number); + const GreasePencilFrame src_frame = frame_map.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); + const int duration = layer_frames_durations.lookup_default(src_frame_number, 0); + + if (!use_duplicate) { + layer.remove_frame(src_frame_number); + } /* Add and overwrite the frame at the destination number. */ if (layer.frames().contains(dst_frame_number)) { diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc index a5128cccd52..3140e12d004 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc @@ -94,6 +94,39 @@ bool mirror_selected_frames(GreasePencil &grease_pencil, return changed; } +bool duplicate_selected_frames(GreasePencil &grease_pencil, bke::greasepencil::Layer &layer) +{ + using namespace bke::greasepencil; + bool changed = false; + LayerTransformData &trans_data = layer.runtime->trans_data_; + + for (auto [frame_number, frame] : layer.frames_for_write().items()) { + if (!frame.is_selected()) { + continue; + } + + /* Create the duplicate drawing. */ + const Drawing *drawing = grease_pencil.get_editable_drawing_at(&layer, frame_number); + if (drawing == nullptr) { + continue; + } + const int duplicated_drawing_index = grease_pencil.drawings().size(); + grease_pencil.add_duplicate_drawings(1, *drawing); + + /* Make a copy of the frame in the duplicates. */ + GreasePencilFrame frame_duplicate = frame; + frame_duplicate.drawing_index = duplicated_drawing_index; + trans_data.temp_frames_buffer.add_overwrite(frame_number, frame_duplicate); + + /* Deselect the current frame, so that only the copy is selected. */ + frame.flag ^= GP_FRAME_SELECTED; + + changed = true; + } + + return changed; +} + bool remove_all_selected_frames(GreasePencil &grease_pencil, bke::greasepencil::Layer &layer) { Vector frames_to_remove; diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index 7336ff10fc2..3735af478d4 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -49,6 +49,11 @@ namespace blender::ed::greasepencil { void set_selected_frames_type(bke::greasepencil::Layer &layer, const eBezTriple_KeyframeType key_type); + +/* Creates duplicate frames for each selected frame in the layer. The duplicates are stored in the + * LayerTransformData structure of the layer runtime data. This function also unselects the + * selected frames, while keeping the duplicates selected. */ +bool duplicate_selected_frames(GreasePencil &grease_pencil, bke::greasepencil::Layer &layer); bool mirror_selected_frames(GreasePencil &grease_pencil, bke::greasepencil::Layer &layer, diff --git a/source/blender/editors/space_action/action_edit.cc b/source/blender/editors/space_action/action_edit.cc index 89827d20bbc..f103a4734d4 100644 --- a/source/blender/editors/space_action/action_edit.cc +++ b/source/blender/editors/space_action/action_edit.cc @@ -1011,7 +1011,9 @@ static bool duplicate_action_keys(bAnimContext *ac) changed |= ED_gpencil_layer_frame_select_check((bGPDlayer *)ale->data); } else if (ale->type == ANIMTYPE_GREASE_PENCIL_LAYER) { - /* GPv3: To be implemented. */ + changed |= blender::ed::greasepencil::duplicate_selected_frames( + *reinterpret_cast(ale->id), + static_cast(ale->data)->wrap()); } else if (ale->type == ANIMTYPE_MASKLAYER) { ED_masklayer_frames_duplicate((MaskLayer *)ale->data); diff --git a/source/blender/editors/transform/transform_convert_action.cc b/source/blender/editors/transform/transform_convert_action.cc index 922de50f509..a67cfcaa366 100644 --- a/source/blender/editors/transform/transform_convert_action.cc +++ b/source/blender/editors/transform/transform_convert_action.cc @@ -100,7 +100,8 @@ static bool grease_pencil_layer_reset_trans_data(blender::bke::greasepencil::Lay static bool grease_pencil_layer_update_trans_data(blender::bke::greasepencil::Layer &layer, const int src_frame_number, - const int dst_frame_number) + const int dst_frame_number, + const bool duplicated) { using namespace blender::bke::greasepencil; LayerTransformData &trans_data = layer.runtime->trans_data_; @@ -118,13 +119,28 @@ static bool grease_pencil_layer_update_trans_data(blender::bke::greasepencil::La trans_data.status = LayerTransformData::TRANS_RUNNING; } - /* Apply the transformation directly in the frame map, so that we display the transformed + const bool use_duplicated = duplicated && + trans_data.temp_frames_buffer.contains(src_frame_number); + const blender::Map &frame_map = use_duplicated ? + (trans_data.temp_frames_buffer) : + (trans_data.frames_copy); + + if (!frame_map.contains(src_frame_number)) { + return false; + } + + /* Apply the transformation directly in the layer 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 GreasePencilFrame src_frame = frame_map.lookup(src_frame_number); const int src_duration = trans_data.frames_duration.lookup_default(src_frame_number, 0); - layer.remove_frame(src_frame_number); + + if (!use_duplicated) { + 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; @@ -136,7 +152,8 @@ static bool grease_pencil_layer_update_trans_data(blender::bke::greasepencil::La static bool grease_pencil_layer_apply_trans_data(GreasePencil &grease_pencil, blender::bke::greasepencil::Layer &layer, - const bool canceled) + const bool canceled, + const bool duplicate) { using namespace blender::bke::greasepencil; LayerTransformData &trans_data = layer.runtime->trans_data_; @@ -151,13 +168,28 @@ static bool grease_pencil_layer_apply_trans_data(GreasePencil &grease_pencil, layer.tag_frames_map_keys_changed(); if (!canceled) { - /* Apply the transformation. */ - grease_pencil.move_frames(layer, trans_data.frames_destination); + /* Moves all the selected frames according to the transformation, and inserts the potential + * duplicate frames in the layer. */ + grease_pencil.move_duplicate_frames( + layer, trans_data.frames_destination, trans_data.temp_frames_buffer); + } + + if (canceled && duplicate) { + /* Duplicates were done, so we need to delete the corresponding duplicate drawings. */ + for (const GreasePencilFrame &duplicate_frame : trans_data.temp_frames_buffer.values()) { + GreasePencilDrawingBase *drawing_base = grease_pencil.drawings( + duplicate_frame.drawing_index); + if (drawing_base->type == GP_DRAWING) { + reinterpret_cast(drawing_base)->wrap().remove_user(); + } + } + grease_pencil.remove_drawings_with_no_users(); } /* Clear the frames copy. */ trans_data.frames_copy.clear(); trans_data.frames_destination.clear(); + trans_data.temp_frames_buffer.clear(); trans_data.status = LayerTransformData::TRANS_CLEAR; return true; @@ -224,9 +256,10 @@ static int count_gplayer_frames(bGPDlayer *gpl, char side, float cfra, bool is_p } static int count_grease_pencil_frames(const blender::bke::greasepencil::Layer *layer, - char side, - float cfra, - bool is_prop_edit) + const char side, + const float cfra, + const bool is_prop_edit, + const bool use_duplicated) { if (layer == nullptr) { return 0; @@ -235,15 +268,22 @@ static int count_grease_pencil_frames(const blender::bke::greasepencil::Layer *l 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 (use_duplicated) { + /* Only count the frames that were duplicated. */ + count_selected += layer->runtime->trans_data_.temp_frames_buffer.size(); + count_all += count_selected; + } + else { + /* 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 (frame.is_selected()) { - count_selected++; - } - count_all++; } if (is_prop_edit && count_selected > 0) { @@ -413,24 +453,26 @@ static int GPLayerToTransData(TransData *td, static int GreasePencilLayerToTransData(TransData *td, TransData2D *td2d, blender::bke::greasepencil::Layer *layer, - char side, - float cfra, - bool is_prop_edit, - float ypos) + const char side, + const float cfra, + const bool is_prop_edit, + const float ypos, + const bool duplicate) { 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()) { + + const auto grease_pencil_frame_to_trans_data = [&](const int frame_number, + const bool frame_selected) { /* 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; + if ((!is_prop_edit && !frame_selected) || !FrameOnMouseSide(side, frame_number, cfra)) { + return; } - any_frame_affected = true; td2d->loc[0] = float(frame_number); @@ -440,7 +482,7 @@ static int GreasePencilLayerToTransData(TransData *td, td->center[0] = td->ival; td->center[1] = ypos; - if (frame.is_selected()) { + if (frame_selected) { td->flag |= TD_SELECTED; } /* Set a pointer to the layer in the transform data so that we can apply the transformation @@ -453,6 +495,14 @@ static int GreasePencilLayerToTransData(TransData *td, td++; td2d++; total_trans_frames++; + any_frame_affected = true; + }; + + const blender::Map &frame_map = + duplicate ? (layer->runtime->trans_data_.temp_frames_buffer) : (layer->frames()); + + for (const auto [frame_number, frame] : frame_map.items()) { + grease_pencil_frame_to_trans_data(frame_number, frame.is_selected()); } if (total_trans_frames == 0) { @@ -510,6 +560,10 @@ static void createTransActionData(bContext *C, TransInfo *t) TransData2D *td2d = nullptr; tGPFtransdata *tfd = nullptr; + /* The T_DUPLICATED_KEYFRAMES flag is only set if we made some duplicates of the selected frames, + * and they are the ones that are being transformed. */ + const bool use_duplicated = (t->flag & T_DUPLICATED_KEYFRAMES) != 0; + rcti *mask = &t->region->v2d.mask; rctf *datamask = &t->region->v2d.cur; @@ -572,7 +626,7 @@ static void createTransActionData(bContext *C, TransInfo *t) else if (ale->type == ANIMTYPE_GREASE_PENCIL_LAYER) { using namespace blender::bke::greasepencil; adt_count = count_grease_pencil_frames( - static_cast(ale->data), t->frame_side, cfra, is_prop_edit); + static_cast(ale->data), t->frame_side, cfra, is_prop_edit, use_duplicated); } else if (ale->type == ANIMTYPE_MASKLAYER) { adt_count = count_masklayer_frames( @@ -647,7 +701,8 @@ static void createTransActionData(bContext *C, TransInfo *t) Layer *layer = static_cast(ale->data); int i; - i = GreasePencilLayerToTransData(td, td2d, layer, t->frame_side, cfra, is_prop_edit, ypos); + i = GreasePencilLayerToTransData( + td, td2d, layer, t->frame_side, cfra, is_prop_edit, ypos, use_duplicated); td += i; td2d += i; } @@ -715,15 +770,16 @@ static void createTransActionData(bContext *C, TransInfo *t) 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()) { + const auto grease_pencil_closest_selected_frame = [&](const int frame_number, + const bool frame_selected) { + if (frame_selected) { td->dist = td->rdist = 0.0f; ++td; - continue; + return; } int closest_selected = INT_MAX; - for (auto [neighbor_frame_number, neighbor_frame] : layer->frames_for_write().items()) { + for (const auto [neighbor_frame_number, neighbor_frame] : layer->frames().items()) { if (!neighbor_frame.is_selected() || !FrameOnMouseSide(t->frame_side, float(neighbor_frame_number), cfra)) { @@ -735,6 +791,18 @@ static void createTransActionData(bContext *C, TransInfo *t) td->dist = td->rdist = closest_selected; ++td; + }; + + for (const auto [frame_number, frame] : layer->frames().items()) { + grease_pencil_closest_selected_frame(frame_number, frame.is_selected()); + } + + if (use_duplicated) { + /* Also count for duplicated frames. */ + for (const auto [frame_number, frame] : + layer->runtime->trans_data_.temp_frames_buffer.items()) { + grease_pencil_closest_selected_frame(frame_number, frame.is_selected()); + } } } else if (ale->type == ANIMTYPE_MASKLAYER) { @@ -886,10 +954,12 @@ static void recalcData_actedit(TransInfo *t) transform_convert_flush_handle2D(td, td2d, 0.0f); if ((t->state == TRANS_RUNNING) && ((td->flag & TD_GREASE_PENCIL_FRAME) != 0)) { + const bool use_duplicated = (t->flag & T_DUPLICATED_KEYFRAMES) != 0; grease_pencil_layer_update_trans_data( *static_cast(td->extra), round_fl_to_int(td->ival), - round_fl_to_int(td2d->loc[0])); + round_fl_to_int(td2d->loc[0]), + use_duplicated); } } @@ -1175,7 +1245,8 @@ static void special_aftertrans_update__actedit(bContext *C, TransInfo *t) grease_pencil_layer_apply_trans_data( *grease_pencil, *static_cast(ale->data), - canceled); + canceled, + duplicate); break; } diff --git a/source/blender/makesdna/DNA_grease_pencil_types.h b/source/blender/makesdna/DNA_grease_pencil_types.h index 1c0287cf4c6..004fe8edf7c 100644 --- a/source/blender/makesdna/DNA_grease_pencil_types.h +++ b/source/blender/makesdna/DNA_grease_pencil_types.h @@ -517,6 +517,22 @@ typedef struct GreasePencil { void move_frames(blender::bke::greasepencil::Layer &layer, const blender::Map &frame_number_destinations); + /** + * Moves and/or inserts duplicates of a set of frames in a \a layer. + * + * \param frame_number_destination describes all transformations that should be applied on the + * frame keys. + * \param duplicate_frames the frames that should be duplicated instead of moved. Keys of the map + * are the keys of the corresponding source frames. Frames will be inserted at the key given by + * the map \a frame_number_destination. + * + * 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_duplicate_frames(blender::bke::greasepencil::Layer &layer, + const blender::Map &frame_number_destinations, + const blender::Map &duplicate_frames); + /** * Returns an editable drawing on \a layer at frame \a frame_number or `nullptr` if no such * drawing exists.