diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 34194d51578..decec16396b 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4616,6 +4616,11 @@ def km_grease_pencil_edit_mode(params): # Dissolve ("grease_pencil.dissolve", {"type": 'X', "value": 'PRESS', "ctrl": True}, None), ("grease_pencil.dissolve", {"type": 'DEL', "value": 'PRESS', "ctrl": True}, None), + # Copy/paste + ("grease_pencil.copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None), + ("grease_pencil.paste", {"type": 'V', "value": 'PRESS', "ctrl": True}, None), + ("grease_pencil.paste", {"type": 'V', "value": 'PRESS', "shift": True, "ctrl": True}, + {"properties": [("paste_back", True)]}), # Separate ("grease_pencil.separate", {"type": 'P', "value": 'PRESS'}, None), # Delete all active frames diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 2157c0df84e..7ab68e5c005 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -5831,6 +5831,11 @@ class VIEW3D_MT_edit_greasepencil(Menu): layout.separator() + layout.operator("grease_pencil.copy", text="Copy", icon='COPYDOWN') + layout.operator("grease_pencil.paste", text="Paste", icon='PASTEDOWN') + + layout.separator() + layout.menu("VIEW3D_MT_edit_greasepencil_showhide") layout.operator_menu_enum("grease_pencil.separate", "mode", text="Separate") layout.operator("grease_pencil.clean_loose") @@ -8220,6 +8225,12 @@ class VIEW3D_MT_greasepencil_edit_context_menu(Menu): col = row.column(align=True) col.label(text="Point", icon='GP_SELECT_POINTS') + # Copy/paste + col.operator("grease_pencil.copy", text="Copy", icon="COPYDOWN") + col.operator("grease_pencil.paste", text="Paste", icon="PASTEDOWN") + + col.separator() + # Main Strokes Operators col.operator("grease_pencil.stroke_subdivide", text="Subdivide") col.operator("grease_pencil.stroke_subdivide_smooth", text="Subdivide and Smooth") @@ -8256,6 +8267,12 @@ class VIEW3D_MT_greasepencil_edit_context_menu(Menu): col = row.column(align=True) col.label(text="Stroke", icon='GP_SELECT_STROKES') + # Copy/paste + col.operator("grease_pencil.copy", text="Copy", icon="COPYDOWN") + col.operator("grease_pencil.paste", text="Paste", icon="PASTEDOWN") + + col.separator() + # Main Strokes Operators col.operator("grease_pencil.stroke_subdivide", text="Subdivide") col.operator("grease_pencil.stroke_subdivide_smooth", text="Subdivide and Smooth") diff --git a/source/blender/editors/grease_pencil/CMakeLists.txt b/source/blender/editors/grease_pencil/CMakeLists.txt index 3e9ed92c995..2c15a93d16b 100644 --- a/source/blender/editors/grease_pencil/CMakeLists.txt +++ b/source/blender/editors/grease_pencil/CMakeLists.txt @@ -35,6 +35,7 @@ set(SRC set(LIB bf_blenkernel + PRIVATE bf::animrig PRIVATE bf::blenlib PRIVATE bf::depsgraph PRIVATE bf::dna diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc index 77c9c9488ab..d8dddf5e949 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc @@ -22,6 +22,7 @@ #include "BKE_curves_utils.hh" #include "BKE_grease_pencil.hh" #include "BKE_lib_id.hh" +#include "BKE_main.hh" #include "BKE_material.h" #include "BKE_report.hh" @@ -2178,6 +2179,253 @@ static void GREASE_PENCIL_OT_separate(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Copy and Paste Operator + * \{ */ + +/* Global clipboard for Grease Pencil curves. */ +struct Clipboard { + bke::CurvesGeometry curves; + /* We store the material uid's of the copied curves, so we can match those when pasting the + * clipboard into another object. */ + Vector> materials; + int materials_in_source_num; +}; + +static Clipboard &get_grease_pencil_clipboard() +{ + static Clipboard clipboard; + return clipboard; +} + +static int grease_pencil_paste_strokes_exec(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + const Scene &scene = *CTX_data_scene(C); + Object *object = CTX_data_active_object(C); + const bke::AttrDomain selection_domain = ED_grease_pencil_selection_domain_get( + scene.toolsettings); + GreasePencil &grease_pencil = *static_cast(object->data); + const bool paste_on_back = RNA_boolean_get(op->ptr, "paste_back"); + + Clipboard &clipboard = get_grease_pencil_clipboard(); + + /* Get active layer in the target object. */ + if (!grease_pencil.has_active_layer()) { + BKE_report(op->reports, RPT_ERROR, "No active Grease Pencil layer"); + return OPERATOR_CANCELLED; + } + const bke::greasepencil::Layer &active_layer = *grease_pencil.get_active_layer(); + if (!active_layer.is_editable()) { + BKE_report(op->reports, RPT_ERROR, "Active layer is locked or hidden"); + return OPERATOR_CANCELLED; + } + + /* Ensure active keyframe. */ + if (!ensure_active_keyframe(scene, grease_pencil)) { + BKE_report(op->reports, RPT_ERROR, "No Grease Pencil frame to draw on"); + return OPERATOR_CANCELLED; + } + bke::greasepencil::Drawing *target_drawing = grease_pencil.get_editable_drawing_at(active_layer, + scene.r.cfra); + if (target_drawing == nullptr) { + return OPERATOR_CANCELLED; + } + + /* Deselect everything in the target layer. The pasted strokes are the only ones then after the + * paste. That's convenient for the user. */ + bke::GSpanAttributeWriter selection_in_target = ed::curves::ensure_selection_attribute( + target_drawing->strokes_for_write(), selection_domain, CD_PROP_BOOL); + ed::curves::fill_selection_false(selection_in_target.span); + selection_in_target.finish(); + + /* Get a list of all materials in the scene. */ + Map scene_materials; + LISTBASE_FOREACH (Material *, material, &bmain->materials) { + scene_materials.add(material->id.session_uid, material); + } + + /* Map the materials used in the clipboard curves to the materials in the target object. */ + Array clipboard_material_remap(clipboard.materials_in_source_num, 0); + for (const int i : clipboard.materials.index_range()) { + /* Check if the material name exists in the scene. */ + int target_index; + unsigned int material_id = clipboard.materials[i].first; + Material *material = scene_materials.lookup_default(material_id, nullptr); + if (!material) { + /* Material is removed, so create a new material. */ + BKE_grease_pencil_object_material_new(bmain, object, nullptr, &target_index); + clipboard_material_remap[clipboard.materials[i].second] = target_index; + continue; + } + + /* Find or add the material to the target object. */ + target_index = BKE_object_material_ensure(bmain, object, material); + clipboard_material_remap[clipboard.materials[i].second] = target_index; + } + + /* Get the index range of the pasted curves in the target layer. */ + IndexRange pasted_curves_range = paste_on_back ? + IndexRange(0, clipboard.curves.curves_num()) : + IndexRange(target_drawing->strokes().curves_num(), + clipboard.curves.curves_num()); + + /* Append the geometry from the clipboard to the target layer. */ + Curves *clipboard_curves = curves_new_nomain(clipboard.curves); + Curves *target_curves = curves_new_nomain(std::move(target_drawing->strokes_for_write())); + Array geometry_sets = { + bke::GeometrySet::from_curves(paste_on_back ? clipboard_curves : target_curves), + bke::GeometrySet::from_curves(paste_on_back ? target_curves : clipboard_curves)}; + bke::GeometrySet joined_curves = geometry::join_geometries(geometry_sets, {}); + target_drawing->strokes_for_write() = std::move( + joined_curves.get_curves_for_write()->geometry.wrap()); + + /* Remap the material indices of the pasted curves to the target object material indices. */ + bke::MutableAttributeAccessor attributes = + target_drawing->strokes_for_write().attributes_for_write(); + bke::SpanAttributeWriter material_indices = attributes.lookup_or_add_for_write_span( + "material_index", bke::AttrDomain::Curve); + if (material_indices) { + for (const int i : pasted_curves_range) { + material_indices.span[i] = clipboard_material_remap[material_indices.span[i]]; + } + material_indices.finish(); + } + + target_drawing->tag_topology_changed(); + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil); + + return OPERATOR_FINISHED; +} + +static int grease_pencil_copy_strokes_exec(bContext *C, wmOperator *op) +{ + const Scene *scene = CTX_data_scene(C); + const Object *object = CTX_data_active_object(C); + GreasePencil &grease_pencil = *static_cast(object->data); + const bke::AttrDomain selection_domain = ED_grease_pencil_selection_domain_get( + scene->toolsettings); + + Clipboard &clipboard = get_grease_pencil_clipboard(); + + bool anything_copied = false; + int num_copied = 0; + Vector set_of_copied_curves; + + /* Collect all selected strokes/points on all editable layers. */ + const Vector drawings = retrieve_editable_drawings(*scene, grease_pencil); + for (const MutableDrawingInfo &drawing_info : drawings) { + const bke::CurvesGeometry &curves = drawing_info.drawing.strokes(); + + if (curves.curves_num() == 0) { + continue; + } + if (!ed::curves::has_anything_selected(curves)) { + continue; + } + + /* Get a copy of the selected geometry on this layer. */ + IndexMaskMemory memory; + bke::CurvesGeometry copied_curves; + + if (selection_domain == bke::AttrDomain::Curve) { + const IndexMask selected_curves = ed::curves::retrieve_selected_curves(curves, memory); + copied_curves = curves_copy_curve_selection(curves, selected_curves, {}); + num_copied += copied_curves.curves_num(); + } + else if (selection_domain == bke::AttrDomain::Point) { + const IndexMask selected_points = ed::curves::retrieve_selected_points(curves, memory); + copied_curves = curves_copy_point_selection(curves, selected_points, {}); + num_copied += copied_curves.points_num(); + } + + /* Add the layer selection to the set of copied curves. */ + Curves *layer_curves = curves_new_nomain(std::move(copied_curves)); + set_of_copied_curves.append(bke::GeometrySet::from_curves(layer_curves)); + anything_copied = true; + } + + if (!anything_copied) { + return OPERATOR_CANCELLED; + } + + /* Merge all copied curves into one CurvesGeometry object and assign it to the clipboard. */ + bke::GeometrySet joined_copied_curves = geometry::join_geometries(set_of_copied_curves, {}); + clipboard.curves = std::move(joined_copied_curves.get_curves_for_write()->geometry.wrap()); + + /* Store the session uid of the materials used by the curves in the clipboard. We use the uid to + * remap the material indices when pasting. */ + clipboard.materials.clear(); + clipboard.materials_in_source_num = grease_pencil.material_array_num; + const bke::AttributeAccessor attributes = clipboard.curves.attributes(); + const VArraySpan material_indices = *attributes.lookup_or_default( + "material_index", bke::AttrDomain::Curve, 0); + for (const int material_index : IndexRange(grease_pencil.material_array_num)) { + if (!material_indices.contains(material_index)) { + continue; + } + const Material *material = grease_pencil.material_array[material_index]; + clipboard.materials.append({material->id.session_uid, material_index}); + } + + /* Report the numbers. */ + if (selection_domain == bke::AttrDomain::Curve) { + BKE_reportf(op->reports, RPT_INFO, "Copied %d selected curve(s)", num_copied); + } + else if (selection_domain == bke::AttrDomain::Point) { + BKE_reportf(op->reports, RPT_INFO, "Copied %d selected point(s)", num_copied); + } + + return OPERATOR_FINISHED; +} + +static bool grease_pencil_paste_strokes_poll(bContext *C) +{ + if (!editable_grease_pencil_poll(C)) { + return false; + } + + /* Check for curves in the Grease Pencil clipboard. */ + Clipboard &clipboard = get_grease_pencil_clipboard(); + return (clipboard.curves.curves_num() > 0); +} + +static void GREASE_PENCIL_OT_paste(wmOperatorType *ot) +{ + /* Identifiers. */ + ot->name = "Paste Strokes"; + ot->idname = "GREASE_PENCIL_OT_paste"; + ot->description = + "Paste Grease Pencil points or strokes from the internal clipboard to the active layer"; + + /* Callbacks. */ + ot->exec = grease_pencil_paste_strokes_exec; + ot->poll = grease_pencil_paste_strokes_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + ot->prop = RNA_def_boolean( + ot->srna, "paste_back", false, "Paste on Back", "Add pasted strokes behind all strokes"); + RNA_def_property_flag(ot->prop, PROP_SKIP_SAVE); +} + +static void GREASE_PENCIL_OT_copy(wmOperatorType *ot) +{ + /* Identifiers. */ + ot->name = "Copy Strokes"; + ot->idname = "GREASE_PENCIL_OT_copy"; + ot->description = "Copy the selected Grease Pencil points or strokes to the internal clipboard"; + + /* Callbacks. */ + ot->exec = grease_pencil_copy_strokes_exec; + ot->poll = editable_grease_pencil_poll; + + ot->flag = OPTYPE_REGISTER; +} + +/** \} */ + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil_edit() @@ -2202,4 +2450,6 @@ void ED_operatortypes_grease_pencil_edit() WM_operatortype_append(GREASE_PENCIL_OT_stroke_subdivide); WM_operatortype_append(GREASE_PENCIL_OT_stroke_reorder); WM_operatortype_append(GREASE_PENCIL_OT_move_to_layer); + WM_operatortype_append(GREASE_PENCIL_OT_copy); + WM_operatortype_append(GREASE_PENCIL_OT_paste); } 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 54cbd6720b9..38fda2a5d1c 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc @@ -12,11 +12,14 @@ #include "BKE_context.hh" #include "BKE_grease_pencil.hh" +#include "BKE_report.hh" #include "DEG_depsgraph.hh" #include "DNA_scene_types.h" +#include "ANIM_keyframing.hh" + #include "ED_grease_pencil.hh" #include "ED_keyframes_edit.hh" #include "ED_markers.hh" @@ -330,6 +333,40 @@ void create_keyframe_edit_data_selected_frames_list(KeyframeEditData *ked, } } +bool ensure_active_keyframe(const Scene &scene, GreasePencil &grease_pencil) +{ + const int current_frame = scene.r.cfra; + bke::greasepencil::Layer &active_layer = *grease_pencil.get_active_layer(); + + if (!active_layer.has_drawing_at(current_frame) && !blender::animrig::is_autokey_on(&scene)) { + return false; + } + + /* If auto-key is on and the drawing at the current frame starts before the current frame a new + * keyframe needs to be inserted. */ + const bool is_first = active_layer.sorted_keys().is_empty() || + (active_layer.sorted_keys().first() > current_frame); + const bool needs_new_drawing = is_first || + (*active_layer.frame_key_at(current_frame) < current_frame); + + if (blender::animrig::is_autokey_on(&scene) && needs_new_drawing) { + if ((scene.toolsettings->gpencil_flags & GP_TOOL_FLAG_RETAIN_LAST) != 0) { + /* For additive drawing, we duplicate the frame that's currently visible and insert it at the + * current frame. */ + grease_pencil.insert_duplicate_frame( + active_layer, *active_layer.frame_key_at(current_frame), current_frame, false); + } + else { + /* Otherwise we just insert a blank keyframe at the current frame. */ + grease_pencil.insert_blank_frame(active_layer, current_frame, 0, BEZT_KEYTYPE_KEYFRAME); + } + } + /* There should now always be a drawing at the current frame. */ + BLI_assert(active_layer.has_drawing_at(current_frame)); + + return true; +} + static int insert_blank_frame_exec(bContext *C, wmOperator *op) { using namespace blender::bke::greasepencil; diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index 243ef5dfd42..ea1f353e043 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -23,6 +23,7 @@ struct Main; struct Object; struct KeyframeEditData; struct wmKeyConfig; +struct wmOperator; struct ToolSettings; struct Scene; struct UndoType; @@ -157,6 +158,13 @@ void select_frames_range(bke::greasepencil::TreeNode &node, */ bool has_any_frame_selected(const bke::greasepencil::Layer &layer); +/** + * Check for an active keyframe at the current scene time. When there is not, create one when + * Autokey is on (taking Additive drawing setting into account). + * Returns false when no keyframe could be found or created. + */ +bool ensure_active_keyframe(const Scene &scene, GreasePencil &grease_pencil); + void create_keyframe_edit_data_selected_frames_list(KeyframeEditData *ked, const bke::greasepencil::Layer &layer); diff --git a/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc b/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc index 847a166b574..9526dd83bfd 100644 --- a/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc +++ b/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc @@ -149,7 +149,6 @@ static int grease_pencil_brush_stroke_invoke(bContext *C, wmOperator *op, const return OPERATOR_CANCELLED; } - const int current_frame = scene->r.cfra; bke::greasepencil::Layer &active_layer = *grease_pencil.get_active_layer(); if (!active_layer.is_editable()) { @@ -157,35 +156,12 @@ static int grease_pencil_brush_stroke_invoke(bContext *C, wmOperator *op, const return OPERATOR_CANCELLED; } - /* If there is no drawing at the current frame and auto-key is off, then */ - if (!active_layer.has_drawing_at(current_frame) && !blender::animrig::is_autokey_on(scene)) { + /* Ensure a drawing at the current keyframe. */ + if (!ed::greasepencil::ensure_active_keyframe(*scene, grease_pencil)) { BKE_report(op->reports, RPT_ERROR, "No Grease Pencil frame to draw on"); return OPERATOR_CANCELLED; } - /* If auto-key is on and the drawing at the current frame starts before the current frame a new - * keyframe needs to be inserted. */ - const bool is_first = active_layer.sorted_keys().is_empty() || - (active_layer.sorted_keys().first() > current_frame); - const bool needs_new_drawing = is_first || - (*active_layer.frame_key_at(current_frame) < current_frame); - - if (blender::animrig::is_autokey_on(scene) && needs_new_drawing) { - const ToolSettings *ts = CTX_data_tool_settings(C); - if ((ts->gpencil_flags & GP_TOOL_FLAG_RETAIN_LAST) != 0) { - /* For additive drawing, we duplicate the frame that's currently visible and insert it at the - * current frame. */ - grease_pencil.insert_duplicate_frame( - active_layer, *active_layer.frame_key_at(current_frame), current_frame, false); - } - else { - /* Otherwise we just insert a blank keyframe at the current frame. */ - grease_pencil.insert_blank_frame(active_layer, current_frame, 0, BEZT_KEYTYPE_KEYFRAME); - } - } - /* There should now always be a drawing at the current frame. */ - BLI_assert(active_layer.has_drawing_at(current_frame)); - op->customdata = paint_stroke_new(C, op, stroke_get_location,