diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index a4f12c4cdfe..68560d18c8e 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -5823,8 +5823,8 @@ class VIEW3D_MT_edit_greasepencil_stroke(Menu): layout.separator() - layout.operator_menu_enum("grease_pencil.set_curve_type", property="type") - layout.operator("grease_pencil.set_curve_resolution") + layout.operator_menu_enum("grease_pencil.convert_curve_type", text="Convert Type", property="type") + layout.operator("grease_pencil.set_curve_resolution", text="Set Resolution") layout.separator() @@ -8345,6 +8345,10 @@ class VIEW3D_MT_greasepencil_edit_context_menu(Menu): col.separator() col.operator("grease_pencil.separate", text="Separate").mode = 'SELECTED' + + col.separator() + col.operator_menu_enum("grease_pencil.convert_curve_type", text="Convert Type", property="type") + layout.operator("grease_pencil.set_curve_resolution", text="Convert Type") else: col = row.column(align=True) col.label(text="Point", icon='GP_SELECT_POINTS') @@ -8393,6 +8397,9 @@ class VIEW3D_MT_greasepencil_edit_context_menu(Menu): col.operator_enum("grease_pencil.dissolve", "type") + col.separator() + col.operator_menu_enum("grease_pencil.convert_curve_type", text="Convert Type", property="type") + class GREASE_PENCIL_MT_Layers(Menu): bl_label = "Layers" 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 e2cc6abf4c8..1bf2e056cc2 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc @@ -20,6 +20,7 @@ #include "BLI_span.hh" #include "BLI_string.h" #include "BLI_utildefines.h" +#include "BLI_vector.hh" #include "BLT_translation.hh" #include "DNA_anim_types.h" @@ -63,6 +64,7 @@ #include "ED_view3d.hh" #include "GEO_curves_remove_and_split.hh" +#include "GEO_fit_curves.hh" #include "GEO_join_geometries.hh" #include "GEO_realize_instances.hh" #include "GEO_reorder.hh" @@ -4391,6 +4393,216 @@ static void GREASE_PENCIL_OT_outline(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Convert Curve Type Operator + * \{ */ + +static const bke::CurvesGeometry fit_poly_curves(bke::CurvesGeometry &curves, + const IndexMask &selection, + const float threshold) +{ + const VArray thresholds = VArray::ForSingle(threshold, curves.curves_num()); + /* TODO: Detect or manually provide corners. */ + const VArray corners = VArray::ForSingle(false, curves.points_num()); + return geometry::fit_poly_to_bezier_curves( + curves, selection, thresholds, corners, geometry::FitMethod::Refit, {}); +} + +static void convert_to_catmull_rom(bke::CurvesGeometry &curves, + const IndexMask &selection, + const float threshold) +{ + if (curves.is_single_type(CURVE_TYPE_CATMULL_ROM)) { + return; + } + IndexMaskMemory memory; + const IndexMask non_catmull_rom_curves_selection = + curves.indices_for_curve_type(CURVE_TYPE_CATMULL_ROM, selection, memory) + .complement(selection, memory); + BLI_assert(!non_catmull_rom_curves_selection.is_empty()); + curves = geometry::resample_to_evaluated(curves, non_catmull_rom_curves_selection); + + /* To avoid having too many control points, simplify the position attribute based on the + * threshold. This doesn't replace an actual curve fitting (which would be better), but + * is a decent approximation for the meantime. */ + const IndexMask points_to_remove = geometry::simplify_curve_attribute( + curves.positions(), + non_catmull_rom_curves_selection, + curves.points_by_curve(), + curves.cyclic(), + threshold, + curves.positions(), + memory); + curves.remove_points(points_to_remove, {}); + + geometry::ConvertCurvesOptions options; + options.convert_bezier_handles_to_poly_points = false; + options.convert_bezier_handles_to_catmull_rom_points = false; + options.keep_bezier_shape_as_nurbs = true; + options.keep_catmull_rom_shape_as_nurbs = true; + curves = geometry::convert_curves( + curves, non_catmull_rom_curves_selection, CURVE_TYPE_CATMULL_ROM, {}, options); +} + +static void convert_to_poly(bke::CurvesGeometry &curves, const IndexMask &selection) +{ + if (curves.is_single_type(CURVE_TYPE_POLY)) { + return; + } + IndexMaskMemory memory; + const IndexMask non_poly_curves_selection = curves + .indices_for_curve_type( + CURVE_TYPE_POLY, selection, memory) + .complement(selection, memory); + BLI_assert(!non_poly_curves_selection.is_empty()); + curves = geometry::resample_to_evaluated(curves, non_poly_curves_selection); +} + +static void convert_to_bezier(bke::CurvesGeometry &curves, + const IndexMask &selection, + const float threshold) +{ + if (curves.is_single_type(CURVE_TYPE_BEZIER)) { + return; + } + IndexMaskMemory memory; + const IndexMask poly_curves_selection = curves.indices_for_curve_type( + CURVE_TYPE_POLY, selection, memory); + if (!poly_curves_selection.is_empty()) { + curves = fit_poly_curves(curves, poly_curves_selection, threshold); + } + + geometry::ConvertCurvesOptions options; + options.convert_bezier_handles_to_poly_points = false; + options.convert_bezier_handles_to_catmull_rom_points = false; + options.keep_bezier_shape_as_nurbs = true; + options.keep_catmull_rom_shape_as_nurbs = true; + curves = geometry::convert_curves(curves, selection, CURVE_TYPE_BEZIER, {}, options); +} + +static void convert_to_nurbs(bke::CurvesGeometry &curves, + const IndexMask &selection, + const float threshold) +{ + if (curves.is_single_type(CURVE_TYPE_NURBS)) { + return; + } + + IndexMaskMemory memory; + const IndexMask poly_curves_selection = curves.indices_for_curve_type( + CURVE_TYPE_POLY, selection, memory); + if (!poly_curves_selection.is_empty()) { + curves = fit_poly_curves(curves, poly_curves_selection, threshold); + } + + geometry::ConvertCurvesOptions options; + options.convert_bezier_handles_to_poly_points = false; + options.convert_bezier_handles_to_catmull_rom_points = false; + options.keep_bezier_shape_as_nurbs = true; + options.keep_catmull_rom_shape_as_nurbs = true; + curves = geometry::convert_curves(curves, selection, CURVE_TYPE_NURBS, {}, options); +} + +static wmOperatorStatus grease_pencil_convert_curve_type_exec(bContext *C, wmOperator *op) +{ + const Scene *scene = CTX_data_scene(C); + Object *object = CTX_data_active_object(C); + GreasePencil &grease_pencil = *static_cast(object->data); + + const CurveType dst_type = CurveType(RNA_enum_get(op->ptr, "type")); + const float threshold = RNA_float_get(op->ptr, "threshold"); + + std::atomic changed = false; + const Vector drawings = retrieve_editable_drawings(*scene, grease_pencil); + threading::parallel_for_each(drawings, [&](const MutableDrawingInfo &info) { + bke::CurvesGeometry &curves = info.drawing.strokes_for_write(); + IndexMaskMemory memory; + const IndexMask strokes = ed::greasepencil::retrieve_editable_and_selected_strokes( + *object, info.drawing, info.layer_index, memory); + if (strokes.is_empty()) { + return; + } + + switch (dst_type) { + case CURVE_TYPE_CATMULL_ROM: + convert_to_catmull_rom(curves, strokes, threshold); + break; + case CURVE_TYPE_POLY: + convert_to_poly(curves, strokes); + break; + case CURVE_TYPE_BEZIER: + convert_to_bezier(curves, strokes, threshold); + break; + case CURVE_TYPE_NURBS: + convert_to_nurbs(curves, strokes, threshold); + break; + } + + info.drawing.tag_topology_changed(); + changed.store(true, std::memory_order_relaxed); + }); + + 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_convert_curve_type_ui(bContext *C, wmOperator *op) +{ + uiLayout *layout = op->layout; + wmWindowManager *wm = CTX_wm_manager(C); + + PointerRNA ptr = RNA_pointer_create_discrete(&wm->id, op->type->srna, op->properties); + + uiLayoutSetPropSep(layout, true); + uiLayoutSetPropDecorate(layout, false); + + layout->prop(&ptr, "type", UI_ITEM_NONE, std::nullopt, ICON_NONE); + + const CurveType dst_type = CurveType(RNA_enum_get(op->ptr, "type")); + + if (dst_type == CURVE_TYPE_POLY) { + return; + } + + layout->prop(&ptr, "threshold", UI_ITEM_NONE, std::nullopt, ICON_NONE); +} + +static void GREASE_PENCIL_OT_convert_curve_type(wmOperatorType *ot) +{ + ot->name = "Convert Curve Type"; + ot->idname = "GREASE_PENCIL_OT_convert_curve_type"; + ot->description = "Convert type of selected curves"; + + ot->invoke = WM_menu_invoke; + ot->exec = grease_pencil_convert_curve_type_exec; + ot->poll = editable_grease_pencil_poll; + ot->ui = grease_pencil_convert_curve_type_ui; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + ot->prop = RNA_def_enum( + ot->srna, "type", rna_enum_curves_type_items, CURVE_TYPE_POLY, "Type", ""); + RNA_def_property_flag(ot->prop, PROP_SKIP_SAVE); + + PropertyRNA *prop = RNA_def_float( + ot->srna, + "threshold", + 0.01f, + 0.0f, + 100.0f, + "Threshold", + "The distance that the resulting points are allowed to be within", + 0.0f, + 100.0f); + RNA_def_property_subtype(prop, PROP_DISTANCE); +} + +/** \} */ + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil_edit() @@ -4433,6 +4645,7 @@ void ED_operatortypes_grease_pencil_edit() WM_operatortype_append(GREASE_PENCIL_OT_stroke_split); WM_operatortype_append(GREASE_PENCIL_OT_remove_fill_guides); WM_operatortype_append(GREASE_PENCIL_OT_outline); + WM_operatortype_append(GREASE_PENCIL_OT_convert_curve_type); } /* -------------------------------------------------------------------- */ diff --git a/source/blender/geometry/CMakeLists.txt b/source/blender/geometry/CMakeLists.txt index 6cc9cbffa22..4d02374cd77 100644 --- a/source/blender/geometry/CMakeLists.txt +++ b/source/blender/geometry/CMakeLists.txt @@ -18,6 +18,7 @@ set(SRC intern/extend_curves.cc intern/extract_elements.cc intern/fillet_curves.cc + intern/fit_curves.cc intern/interpolate_curves.cc intern/join_geometries.cc intern/merge_curves.cc @@ -61,6 +62,7 @@ set(SRC GEO_extend_curves.hh GEO_extract_elements.hh GEO_fillet_curves.hh + GEO_fit_curves.hh GEO_interpolate_curves.hh GEO_join_geometries.hh GEO_merge_curves.hh @@ -109,6 +111,7 @@ set(LIB PRIVATE bf::intern::atomic PRIVATE bf::intern::guardedalloc PRIVATE bf::extern::fmtlib + PRIVATE bf::extern::curve_fit_nd PRIVATE bf::dependencies::optional::manifold ) diff --git a/source/blender/geometry/GEO_fit_curves.hh b/source/blender/geometry/GEO_fit_curves.hh new file mode 100644 index 00000000000..5487b262462 --- /dev/null +++ b/source/blender/geometry/GEO_fit_curves.hh @@ -0,0 +1,42 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "BKE_curves.hh" + +namespace blender::geometry { + +enum class FitMethod { + /** + * Iteratively removes knots/control points with the least error starting with a dense curve. + */ + Refit, + /** + * Uses a least squares solver to recursively find the control points. + */ + Split +}; + +/** + * Fit the selected curves to Bézier curves. + * + * \param src_curves: The input curves. + * \param curve_selection: A selection of curves to fit. The selected curves will be replaced by + * the fitted bézier curves and the unselected curves are copied to the output geometry. + * \param thresholds: A error threshold (fit distance) for each input curve. The fitted curve + * should be within this distance. + * \param corners: Boolean value for each input point. When this is true, the point is treated as a + * corner in the curve fitting. The resulting bézier curve will include this point and the handles + * will be "free", resulting in a sharp corner. + * \param method: The fitting algorithm to use. See #FitMethod. + */ +bke::CurvesGeometry fit_poly_to_bezier_curves(const bke::CurvesGeometry &src_curves, + const IndexMask &curve_selection, + const VArray &thresholds, + const VArray &corners, + FitMethod method, + const bke::AttributeFilter &attribute_filter); + +} // namespace blender::geometry diff --git a/source/blender/geometry/intern/fit_curves.cc b/source/blender/geometry/intern/fit_curves.cc new file mode 100644 index 00000000000..aff4a9c6153 --- /dev/null +++ b/source/blender/geometry/intern/fit_curves.cc @@ -0,0 +1,275 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_array_utils.hh" +#include "BLI_task.hh" + +#include "BKE_curves_utils.hh" +#include "BKE_deform.hh" + +#include "GEO_fit_curves.hh" + +extern "C" { +#include "curve_fit_nd.h" +} + +namespace blender::geometry { + +bke::CurvesGeometry fit_poly_to_bezier_curves(const bke::CurvesGeometry &src_curves, + const IndexMask &curve_selection, + const VArray &thresholds, + const VArray &corners, + const FitMethod method, + const bke::AttributeFilter &attribute_filter) +{ + if (curve_selection.is_empty()) { + return src_curves; + } + + BLI_assert(thresholds.size() == src_curves.curves_num()); + BLI_assert(corners.size() == src_curves.points_num()); + + const OffsetIndices src_points_by_curve = src_curves.offsets(); + const Span src_positions = src_curves.positions(); + const VArray src_cyclic = src_curves.cyclic(); + + bke::CurvesGeometry dst_curves = bke::curves::copy_only_curve_domain(src_curves); + BKE_defgroup_copy_list(&dst_curves.vertex_group_names, &src_curves.vertex_group_names); + + IndexMaskMemory memory; + const IndexMask unselected_curves = curve_selection.complement(src_curves.curves_range(), + memory); + + /* Write the new sizes to the dst_curve_sizes, they will be accumulated later to offsets. */ + MutableSpan dst_curve_sizes = dst_curves.offsets_for_write(); + offset_indices::copy_group_sizes(src_points_by_curve, unselected_curves, dst_curve_sizes); + MutableSpan dst_curve_types = dst_curves.curve_types_for_write(); + + /* NOTE: These spans own the data from the curve fit C-API. */ + Array> cubic_array_per_curve(curve_selection.size()); + Array> corner_indices_per_curve(curve_selection.size()); + Array> original_indices_per_curve(curve_selection.size()); + + std::atomic success = false; + curve_selection.foreach_index(GrainSize(32), [&](const int64_t curve_i, const int64_t pos) { + const IndexRange points = src_points_by_curve[curve_i]; + const Span curve_positions = src_positions.slice(points); + const bool is_cyclic = src_cyclic[curve_i]; + const float epsilon = thresholds[curve_i]; + + /* Both curve fitting algorithms expect the first and last points for non-cyclic curves to be + * treated as if they were corners. */ + const bool use_first_as_corner = !is_cyclic && !corners[points.first()]; + const bool use_last_as_corner = !is_cyclic && !corners[points.last()]; + Vector src_corners; + if (use_first_as_corner) { + src_corners.append(0); + } + if (points.size() > 2) { + for (const int i : IndexRange::from_begin_end(1, points.size() - 1)) { + if (corners[points[i]]) { + src_corners.append(i); + } + } + } + if (use_last_as_corner) { + src_corners.append(points.last()); + } + const uint *src_corners_ptr = src_corners.is_empty() ? + nullptr : + reinterpret_cast(src_corners.data()); + + const uint8_t flag = CURVE_FIT_CALC_HIGH_QUALIY | ((is_cyclic) ? CURVE_FIT_CALC_CYCLIC : 0); + + float *cubic_array = nullptr; + uint32_t *orig_index_map = nullptr; + uint32_t cubic_array_size = 0; + uint32_t *corner_index_array = nullptr; + uint32_t corner_index_array_size = 0; + int error = 1; + if (method == FitMethod::Split) { + error = curve_fit_cubic_to_points_fl(curve_positions.cast().data(), + curve_positions.size(), + 3, + epsilon, + flag, + src_corners_ptr, + src_corners.size(), + &cubic_array, + &cubic_array_size, + &orig_index_map, + &corner_index_array, + &corner_index_array_size); + } + else if (method == FitMethod::Refit) { + error = curve_fit_cubic_to_points_refit_fl(curve_positions.cast().data(), + curve_positions.size(), + 3, + epsilon, + flag, + src_corners_ptr, + src_corners.size(), + /* Don't use automatic corner detection. */ + FLT_MAX, + &cubic_array, + &cubic_array_size, + &orig_index_map, + &corner_index_array, + &corner_index_array_size); + } + + if (error) { + /* Some error occured. Fall back to using the input positions as the (poly) curve. */ + dst_curve_sizes[curve_i] = points.size(); + dst_curve_types[curve_i] = CURVE_TYPE_POLY; + return; + } + + success.store(true, std::memory_order_relaxed); + + const int dst_points_num = cubic_array_size; + BLI_assert(dst_points_num > 0); + + dst_curve_sizes[curve_i] = dst_points_num; + dst_curve_types[curve_i] = CURVE_TYPE_BEZIER; + + cubic_array_per_curve[pos] = MutableSpan(reinterpret_cast(cubic_array), + dst_points_num * 3); + corner_indices_per_curve[pos] = MutableSpan(reinterpret_cast(corner_index_array), + corner_index_array_size); + original_indices_per_curve[pos] = MutableSpan(reinterpret_cast(orig_index_map), + dst_points_num); + }); + + if (!success) { + /* None of the curve fittings succeeded. */ + return src_curves; + } + + const OffsetIndices dst_points_by_curve = offset_indices::accumulate_counts_to_offsets( + dst_curve_sizes); + dst_curves.resize(dst_curves.offsets().last(), dst_curves.curves_num()); + + const Span src_handles_left = src_curves.handle_positions_left(); + const Span src_handles_right = src_curves.handle_positions_right(); + const VArraySpan src_handle_types_left = src_curves.handle_types_left(); + const VArraySpan src_handle_types_right = src_curves.handle_types_right(); + + MutableSpan dst_positions = dst_curves.positions_for_write(); + MutableSpan dst_handles_left = dst_curves.handle_positions_left_for_write(); + MutableSpan dst_handles_right = dst_curves.handle_positions_right_for_write(); + MutableSpan dst_handle_types_left = dst_curves.handle_types_left_for_write(); + MutableSpan dst_handle_types_right = dst_curves.handle_types_right_for_write(); + + /* First handle the unselected curves. */ + if (!src_handles_left.is_empty()) { + array_utils::copy_group_to_group(src_points_by_curve, + dst_points_by_curve, + unselected_curves, + src_handles_left, + dst_handles_left); + } + array_utils::copy_group_to_group( + src_points_by_curve, dst_points_by_curve, unselected_curves, src_positions, dst_positions); + if (!src_handles_right.is_empty()) { + array_utils::copy_group_to_group(src_points_by_curve, + dst_points_by_curve, + unselected_curves, + src_handles_right, + dst_handles_right); + } + if (!src_handle_types_left.is_empty()) { + array_utils::copy_group_to_group(src_points_by_curve, + dst_points_by_curve, + unselected_curves, + src_handle_types_left, + dst_handle_types_left); + } + if (!src_handle_types_right.is_empty()) { + array_utils::copy_group_to_group(src_points_by_curve, + dst_points_by_curve, + unselected_curves, + src_handle_types_right, + dst_handle_types_right); + } + + Array old_by_new_map(dst_curves.points_num()); + unselected_curves.foreach_index(GrainSize(1024), [&](const int64_t curve_i) { + const IndexRange src_points = src_points_by_curve[curve_i]; + const IndexRange dst_points = dst_points_by_curve[curve_i]; + array_utils::fill_index_range(old_by_new_map.as_mutable_span().slice(dst_points), + src_points.start()); + }); + + /* Now copy the data of the newly fitted curves. */ + curve_selection.foreach_index(GrainSize(1024), [&](const int64_t curve_i, const int64_t pos) { + const IndexRange src_points = src_points_by_curve[curve_i]; + const IndexRange dst_points = dst_points_by_curve[curve_i]; + MutableSpan positions = dst_positions.slice(dst_points); + MutableSpan old_by_new = old_by_new_map.as_mutable_span().slice(dst_points); + + if (dst_curve_types[curve_i] == CURVE_TYPE_POLY) { + /* Handle the curves for which the curve fitting has failed. */ + BLI_assert(src_points.size() == dst_points.size()); + positions.copy_from(src_positions.slice(src_points)); + dst_handles_left.slice(dst_points).copy_from(src_positions.slice(src_points)); + dst_handles_right.slice(dst_points).copy_from(src_positions.slice(src_points)); + dst_handle_types_left.slice(dst_points).fill(BEZIER_HANDLE_FREE); + dst_handle_types_right.slice(dst_points).fill(BEZIER_HANDLE_FREE); + array_utils::fill_index_range(old_by_new, src_points.start()); + return; + } + + const Span cubic_array = cubic_array_per_curve[pos]; + BLI_assert(dst_points.size() * 3 == cubic_array.size()); + MutableSpan left_handles = dst_handles_left.slice(dst_points); + MutableSpan right_handles = dst_handles_right.slice(dst_points); + threading::parallel_for(dst_points.index_range(), 8192, [&](const IndexRange range) { + for (const int i : range) { + const int index = i * 3; + positions[i] = cubic_array[index + 1]; + left_handles[i] = cubic_array[index]; + right_handles[i] = cubic_array[index + 2]; + } + }); + + const Span corner_indices = corner_indices_per_curve[pos]; + dst_handle_types_left.slice(dst_points).fill_indices(corner_indices, BEZIER_HANDLE_FREE); + dst_handle_types_right.slice(dst_points).fill_indices(corner_indices, BEZIER_HANDLE_FREE); + + const Span original_indices = original_indices_per_curve[pos]; + threading::parallel_for(dst_points.index_range(), 8192, [&](const IndexRange range) { + for (const int i : range) { + old_by_new[i] = src_points[original_indices[i]]; + } + }); + }); + + dst_curves.update_curve_types(); + + bke::gather_attributes( + src_curves.attributes(), + bke::AttrDomain::Point, + bke::AttrDomain::Point, + bke::attribute_filter_with_skip_ref( + attribute_filter, + {"position", "handle_left", "handle_right", "handle_type_left", "handle_type_right"}), + old_by_new_map, + dst_curves.attributes_for_write()); + + /* Free all the data from the C-API. */ + for (MutableSpan cubic_array : cubic_array_per_curve) { + free(cubic_array.data()); + } + for (MutableSpan corner_indices : corner_indices_per_curve) { + free(corner_indices.data()); + } + for (MutableSpan original_indices : original_indices_per_curve) { + free(original_indices.data()); + } + + return dst_curves; +} + +} // namespace blender::geometry