diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 01cd3161e78..93f5566a70e 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -6084,6 +6084,7 @@ class VIEW3D_MT_edit_greasepencil_cleanup(Menu): layout = self.layout layout.operator("grease_pencil.clean_loose") + layout.operator("grease_pencil.frame_clean_duplicate") if ob.mode != 'PAINT_GREASE_PENCIL': layout.operator("grease_pencil.stroke_merge_by_distance", text="Merge by Distance") 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 054cce6def3..df3e8aeda6a 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_frames.cc @@ -6,6 +6,7 @@ * \ingroup edgreasepencil */ +#include "BKE_curves.hh" #include "BLI_map.hh" #include "BLI_math_vector_types.hh" #include "BLI_utildefines.h" @@ -413,6 +414,162 @@ static int insert_blank_frame_exec(bContext *C, wmOperator *op) return OPERATOR_FINISHED; } +static bool attributes_varrays_not_equal(const bke::GAttributeReader &attrs_a, + const bke::GAttributeReader &attrs_b) +{ + return (attrs_a.varray.size() != attrs_b.varray.size() || + attrs_a.varray.type() != attrs_b.varray.type()); +} + +static bool attributes_varrays_span_data_equal(const bke::GAttributeReader &attrs_a, + const bke::GAttributeReader &attrs_b) +{ + if (attrs_a.varray.is_span() && attrs_b.varray.is_span()) { + const GSpan attrs_span_a = attrs_a.varray.get_internal_span(); + const GSpan attrs_span_b = attrs_b.varray.get_internal_span(); + + if (attrs_span_a.data() == attrs_span_b.data()) { + return true; + } + } + + return false; +} + +template +static bool attributes_elements_are_equal(const VArray &attributes_a, + const VArray &attributes_b) +{ + const std::optional value_a = attributes_a.get_if_single(); + const std::optional value_b = attributes_b.get_if_single(); + if (value_a.has_value() && value_b.has_value()) { + return value_a.value() == value_b.value(); + } + + const VArraySpan attrs_span_a = attributes_a; + const VArraySpan attrs_span_b = attributes_b; + + return std::equal( + attrs_span_a.begin(), attrs_span_a.end(), attrs_span_b.begin(), attrs_span_b.end()); +} + +static bool curves_geometry_is_equal(const bke::CurvesGeometry &curves_a, + const bke::CurvesGeometry &curves_b) +{ + using namespace blender::bke; + + if (curves_a.points_num() == 0 && curves_b.points_num() == 0) { + return true; + } + + if (curves_a.curves_num() != curves_b.curves_num() || + curves_a.points_num() != curves_b.points_num() || curves_a.offsets() != curves_b.offsets()) + { + return false; + } + + const AttributeAccessor attributes_a = curves_a.attributes(); + const AttributeAccessor attributes_b = curves_b.attributes(); + + const Set ids_a = attributes_a.all_ids(); + const Set ids_b = attributes_b.all_ids(); + if (ids_a != ids_b) { + return false; + } + + for (const AttributeIDRef &id : ids_a) { + GAttributeReader attrs_a = attributes_a.lookup(id); + GAttributeReader attrs_b = attributes_b.lookup(id); + + if (attributes_varrays_not_equal(attrs_a, attrs_b)) { + return false; + } + + if (attributes_varrays_span_data_equal(attrs_a, attrs_b)) { + return true; + } + + bool attributes_are_equal = true; + + attribute_math::convert_to_static_type(attrs_a.varray.type(), [&](auto dummy) { + using T = decltype(dummy); + + const VArray attributes_a = attrs_a.varray.typed(); + const VArray attributes_b = attrs_b.varray.typed(); + + attributes_are_equal = attributes_elements_are_equal(attributes_a, attributes_b); + }); + + if (!attributes_are_equal) { + return false; + } + } + + return true; +} + +static int frame_clean_duplicate_exec(bContext *C, wmOperator *op) +{ + using namespace blender::bke::greasepencil; + Object *object = CTX_data_active_object(C); + GreasePencil &grease_pencil = *static_cast(object->data); + const bool selected = RNA_boolean_get(op->ptr, "selected"); + + bool changed = false; + + for (Layer *layer : grease_pencil.layers_for_write()) { + if (!layer->is_editable()) { + continue; + } + + Vector start_frame_numbers; + for (const FramesMapKeyT key : layer->sorted_keys()) { + const GreasePencilFrame *frame = layer->frames().lookup_ptr(key); + if (selected && !frame->is_selected()) { + continue; + } + if (frame->is_end()) { + continue; + } + start_frame_numbers.append(int(key)); + } + + Vector frame_numbers_to_delete; + for (const int i : start_frame_numbers.index_range().drop_back(1)) { + const int current = start_frame_numbers[i]; + const int next = start_frame_numbers[i + 1]; + + Drawing *drawing = grease_pencil.get_drawing_at(*layer, current); + Drawing *drawing_next = grease_pencil.get_drawing_at(*layer, next); + + if (!drawing || !drawing_next) { + continue; + } + + bke::CurvesGeometry &curves = drawing->strokes_for_write(); + bke::CurvesGeometry &curves_next = drawing_next->strokes_for_write(); + + if (!curves_geometry_is_equal(curves, curves_next)) { + continue; + } + + frame_numbers_to_delete.append(next); + } + + grease_pencil.remove_frames(*layer, frame_numbers_to_delete.as_span()); + + changed = true; + } + + if (changed) { + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil); + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + } + + return OPERATOR_FINISHED; +} + static void GREASE_PENCIL_OT_insert_blank_frame(wmOperatorType *ot) { PropertyRNA *prop; @@ -435,6 +592,27 @@ static void GREASE_PENCIL_OT_insert_blank_frame(wmOperatorType *ot) RNA_def_int(ot->srna, "duration", 0, 0, MAXFRAME, "Duration", "", 0, 100); } +static void GREASE_PENCIL_OT_frame_clean_duplicate(wmOperatorType *ot) +{ + PropertyRNA *prop; + + /* identifiers */ + ot->name = "Delete Duplicate Frames"; + ot->idname = "GREASE_PENCIL_OT_frame_clean_duplicate"; + ot->description = "Remove any keyframe that is a duplicate of the previous one"; + + /* callbacks */ + ot->exec = frame_clean_duplicate_exec; + ot->poll = active_grease_pencil_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* properties */ + prop = RNA_def_boolean( + ot->srna, "selected", false, "Selected", "Only delete selected keyframes"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); +} + bool grease_pencil_copy_keyframes(bAnimContext *ac, KeyframeClipboard &clipboard) { using namespace bke::greasepencil; @@ -635,4 +813,5 @@ void ED_operatortypes_grease_pencil_frames() { using namespace blender::ed::greasepencil; WM_operatortype_append(GREASE_PENCIL_OT_insert_blank_frame); + WM_operatortype_append(GREASE_PENCIL_OT_frame_clean_duplicate); }