From 7cd5e25743cd85fcf3ca907797714aca23196704 Mon Sep 17 00:00:00 2001 From: Falk David Date: Thu, 20 Jul 2023 15:31:51 +0200 Subject: [PATCH] GPv3: Stroke Simplify operator This adds the `stroke_simplify` operator. Implements a version of the Ramer Douglas Peucker algorithm for marking the points to delete. Pull Request: https://projects.blender.org/blender/blender/pulls/110266 --- scripts/startup/bl_ui/space_view3d.py | 2 +- .../intern/grease_pencil_edit.cc | 158 +++++++++++++++++- .../editors/include/ED_grease_pencil.h | 2 + 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index bec4f6de7ec..9cf0935dfc2 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -5526,7 +5526,7 @@ class VIEW3D_MT_edit_greasepencil_stroke(Menu): def draw(self, _context): layout = self.layout layout.operator("grease_pencil.stroke_smooth") - pass + layout.operator("grease_pencil.stroke_simplify") class VIEW3D_MT_edit_curves(Menu): 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 d8239f943e7..b41c805f9b2 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc @@ -6,10 +6,12 @@ * \ingroup edgreasepencil */ +#include "BLI_array_utils.hh" #include "BLI_index_mask.hh" #include "BLI_index_range.hh" #include "BLI_math_vector_types.hh" #include "BLI_span.hh" +#include "BLI_stack.hh" #include "BKE_context.h" #include "BKE_grease_pencil.hh" @@ -103,7 +105,8 @@ static void gaussian_blur_1D(const Span src, const bool is_cyclic, MutableSpan dst) { - /* 1D Gaussian-like smoothing function. + /** + * 1D Gaussian-like smoothing function. * * NOTE: This is the algorithm used by #BKE_gpencil_stroke_smooth_point (legacy), * but generalized and written in C++. @@ -391,12 +394,165 @@ static void GREASE_PENCIL_OT_stroke_smooth(wmOperatorType *ot) RNA_def_boolean(ot->srna, "smooth_opacity", false, "Opacity", ""); } +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Simplify Stroke Operator. + * \{ */ + +/** + * An implementation of the Ramer-Douglas-Peucker algorithm. + * + * \param epsilon: The threshold distance from the coord between two points for when a point + * in-between needs to be kept. + * \param points_to_delete: Writes true to the indecies for which the points should be removed. + */ +void ramer_douglas_peucker_simplify(const Span src, + const float epsilon, + MutableSpan points_to_delete) +{ + BLI_assert(src.size() == points_to_delete.size()); + + /* Mark all points to not be removed. */ + points_to_delete.fill(false); + + Stack stack; + stack.push(src.index_range()); + while (!stack.is_empty()) { + const IndexRange range = stack.pop(); + const Span slice = src.slice(range); + + float max_dist = -1.0f; + int max_index = -1; + for (const int64_t index : slice.index_range().drop_front(1).drop_back(1)) { + const float dist = dist_to_line_v3(slice[index], slice.first(), slice.last()); + if (dist > max_dist) { + max_dist = dist; + max_index = index; + } + } + + if (max_dist > epsilon) { + /* Found point outside the epsilon-sized tube. Repeat the search on the left & right side. */ + stack.push(IndexRange(range.first(), max_index + 1)); + stack.push(IndexRange(range.first() + max_index, range.size() - max_index)); + } + else { + /* Points in `range` are inside the epsilon-sized tube. Mark them to be deleted. */ + points_to_delete.slice(range.drop_front(1).drop_back(1)).fill(true); + } + } +} + +static int grease_pencil_stroke_simplify_exec(bContext *C, wmOperator *op) +{ + using namespace blender; + const Scene *scene = CTX_data_scene(C); + Object *object = CTX_data_active_object(C); + GreasePencil &grease_pencil = *static_cast(object->data); + + const float epsilon = RNA_float_get(op->ptr, "factor"); + + bool changed = false; + grease_pencil.foreach_editable_drawing( + scene->r.cfra, [&](int /*drawing_index*/, bke::greasepencil::Drawing &drawing) { + bke::CurvesGeometry &curves = drawing.strokes_for_write(); + if (curves.points_num() == 0) { + return; + } + + if (!ed::curves::has_anything_selected(curves)) { + return; + } + + const Span positions = curves.positions(); + const VArray cyclic = curves.cyclic(); + const OffsetIndices points_by_curve = curves.points_by_curve(); + const VArray selection = *curves.attributes().lookup_or_default( + ".selection", ATTR_DOMAIN_POINT, true); + + Array points_to_delete(curves.points_num()); + selection.materialize(points_to_delete); + + threading::parallel_for(curves.curves_range(), 128, [&](const IndexRange range) { + for (const int curve_i : range) { + const IndexRange points = points_by_curve[curve_i]; + const Span curve_selection = points_to_delete.as_span().slice(points); + if (!curve_selection.contains(true)) { + continue; + } + + const bool is_last_segment_selected = (curve_selection.first() && + curve_selection.last()); + + const Vector selection_ranges = array_utils::find_all_ranges( + curve_selection, true); + threading::parallel_for( + selection_ranges.index_range(), 16, [&](const IndexRange range_of_ranges) { + for (const IndexRange range : selection_ranges.as_span().slice(range_of_ranges)) + { + ramer_douglas_peucker_simplify( + positions.slice(range.shift(points.start())), + epsilon, + points_to_delete.as_mutable_span().slice(range.shift(points.start()))); + } + }); + + /* For cyclic curves, simplify the last segment. */ + if (cyclic[curve_i] && curves.points_num() > 2 && is_last_segment_selected) { + const float dist = dist_to_line_v3( + positions[points.last()], positions[points.last(1)], positions[points.first()]); + if (dist <= epsilon) { + points_to_delete[points.last()] = true; + } + } + } + }); + + if (points_to_delete.as_span().contains(true)) { + IndexMaskMemory memory; + curves.remove_points(IndexMask::from_bools(points_to_delete, memory)); + drawing.tag_topology_changed(); + 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); + } + return OPERATOR_FINISHED; +} + +static void GREASE_PENCIL_OT_stroke_simplify(wmOperatorType *ot) +{ + PropertyRNA *prop; + + /* Identifiers. */ + ot->name = "Simplify Stroke"; + ot->idname = "GREASE_PENCIL_OT_stroke_simplify"; + ot->description = "Simplify selected strokes"; + + /* Callbacks. */ + ot->exec = grease_pencil_stroke_simplify_exec; + ot->poll = editable_grease_pencil_point_selection_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* Simplify parameters. */ + prop = RNA_def_float(ot->srna, "factor", 0.001f, 0.0f, 100.0f, "Factor", "", 0.0f, 100.0f); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); +} + +/** \} */ + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil_edit() { using namespace blender::ed::greasepencil; WM_operatortype_append(GREASE_PENCIL_OT_stroke_smooth); + WM_operatortype_append(GREASE_PENCIL_OT_stroke_simplify); } void ED_keymap_grease_pencil(wmKeyConfig *keyconf) diff --git a/source/blender/editors/include/ED_grease_pencil.h b/source/blender/editors/include/ED_grease_pencil.h index 5e82d155ead..466205e1a86 100644 --- a/source/blender/editors/include/ED_grease_pencil.h +++ b/source/blender/editors/include/ED_grease_pencil.h @@ -72,5 +72,7 @@ void gaussian_blur_1D(const GSpan src, bool is_cyclic, GMutableSpan dst); +void ramer_douglas_peucker_simplify(const Span src, float epsilon, MutableSpan dst); + } // namespace blender::ed::greasepencil #endif