From 889751b0c52ea0857c6733da834c22e01e24bb07 Mon Sep 17 00:00:00 2001 From: Falk David Date: Wed, 4 Jun 2025 11:48:15 +0200 Subject: [PATCH] Grease Pencil: Add `Convert Curve Type` operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new operator in Grease Pencil edit mode to convert between curve types. This acts as a replacment for the `Set Curve Type` operator as the new operator better aligns with previous workflows and artist expectations. Specifically using a threshold to adjust how well the resulting curves fit to the original. It can be found in the `Stroke` > `Convert Type` menu. This operator aims at keeping visual fidelity between the curves. When converting to a non-poly curve type, there's a `threshold` parameter that dictates how closley the shapes will match (a value of zero meaning an almost perfect match, and higher values will result in less accuracy but lower control point count). The conversion to `Catmull-Rom` does not do an actual curve fitting. For now, this will resample the curves and then do an adaptive simplification of the line (using the threshold parameter) to simulate a curve fitting. The `Set Curve Type` operator is no longer exposed in the `Stroke` menu. This also adds a new `geometry::fit_curves` function. The function will fit a selection of curves to bézier curves. The selected curves are treated as if they were poly curves. The `thresholds` virtual array is the error threshold distance for each curve that the fit should be within. The size of the virtual array is assumed to have the same size as the total number of input curves. The `corners` virtual array allows specific input points to be treated as sharp corners. The resulting bezier curve will have this point and the handles will be set to "free". There are two fitting methods: * **Split**: Uses a least squares solver to find the control points (faster, but less accurate). * **Refit**: Iteratively removes knots with the least error starting with a dense curve (slower, more accurate fit). Co-authored-by: Casey Bianco-Davis Co-authored-by: Hans Goudey Pull Request: https://projects.blender.org/blender/blender/pulls/137808 --- scripts/startup/bl_ui/space_view3d.py | 11 +- .../intern/grease_pencil_edit.cc | 213 ++++++++++++++ source/blender/geometry/CMakeLists.txt | 3 + source/blender/geometry/GEO_fit_curves.hh | 42 +++ source/blender/geometry/intern/fit_curves.cc | 275 ++++++++++++++++++ 5 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 source/blender/geometry/GEO_fit_curves.hh create mode 100644 source/blender/geometry/intern/fit_curves.cc 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