GPv3: Copy and Paste operators for copying strokes and points

This PR implements the Copy and Paste operators for GPv3. The operators
are available in Edit Mode. The Copy operator copies the selected strokes/
points to a clipboard. The Paste operator pastes the  strokes/points from
the clipboard to the active layer.

Keyboard shortcuts:
- `Ctrl`-`C` for copy
- `Ctrl`-`V` for paste
- `Ctrl`-`Shift`-`V` for paste in the back (behind all existing strokes)

Pull Request: https://projects.blender.org/blender/blender/pulls/114145
This commit is contained in:
Sietse Brouwer 2024-03-04 11:02:25 +01:00 committed by Falk David
parent d0c9246fcc
commit ad8180c54c
7 changed files with 320 additions and 26 deletions

View File

@ -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

View File

@ -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")

View File

@ -35,6 +35,7 @@ set(SRC
set(LIB
bf_blenkernel
PRIVATE bf::animrig
PRIVATE bf::blenlib
PRIVATE bf::depsgraph
PRIVATE bf::dna

View File

@ -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<std::pair<unsigned int, int>> 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<GreasePencil *>(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<unsigned int, Material *> 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<int> 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<bke::GeometrySet> 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<int> material_indices = attributes.lookup_or_add_for_write_span<int>(
"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<GreasePencil *>(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<bke::GeometrySet> set_of_copied_curves;
/* Collect all selected strokes/points on all editable layers. */
const Vector<MutableDrawingInfo> 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<int> material_indices = *attributes.lookup_or_default<int>(
"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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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,