diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 3a055156107..b1a0592e8cc 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4578,7 +4578,7 @@ def km_gpencil_legacy_stroke_vertex_replace(_params): # Grease Pencil v3 -def km_grease_pencil_paint_mode(_params): +def km_grease_pencil_paint_mode(params): items = [] keymap = ( "Grease Pencil Paint Mode", @@ -4600,6 +4600,10 @@ def km_grease_pencil_paint_mode(_params): # Isolate Layer ("grease_pencil.layer_isolate", {"type": 'NUMPAD_ASTERIX', "value": 'PRESS'}, None), + op_tool_optional( + ("grease_pencil.interpolate", {"type": 'E', "value": 'PRESS', "ctrl": True}, None), + (op_tool_cycle, "builtin.interpolate"), params), + op_asset_shelf_popup( "VIEW3D_AST_brush_gpencil_paint", {"type": 'SPACE', "value": 'PRESS', "shift": True} @@ -4730,6 +4734,9 @@ def km_grease_pencil_edit_mode(params): # Set Handle Type ("grease_pencil.set_handle_type", {"type": 'V', "value": 'PRESS'}, None), + op_tool_optional( + ("grease_pencil.interpolate", {"type": 'E', "value": 'PRESS', "ctrl": True}, None), + (op_tool_cycle, "builtin.interpolate"), params), ]) return keymap @@ -8626,6 +8633,27 @@ def km_3d_view_tool_paint_gpencil_interpolate(params): ) +def km_grease_pencil_interpolate_tool_modal_map(params): + items = [] + keymap = ( + "Interpolate Tool Modal Map", + {"space_type": 'EMPTY', "region_type": 'WINDOW', "modal": True}, + {"items": items}, + ) + + items.extend([ + ("CANCEL", {"type": 'ESC', "value": 'PRESS', "any": True}, None), + ("CANCEL", {"type": 'RIGHTMOUSE', "value": 'PRESS', "any": True}, None), + ("CONFIRM", {"type": 'RET', "value": 'PRESS', "any": True}, None), + ("CONFIRM", {"type": 'NUMPAD_ENTER', "value": 'PRESS', "any": True}, None), + ("CONFIRM", {"type": 'LEFTMOUSE', "value": 'RELEASE', "any": True}, None), + ("INCREASE", {"type": 'WHEELUPMOUSE', "value": 'PRESS'}, None), + ("DECREASE", {"type": 'WHEELDOWNMOUSE', "value": 'PRESS'}, None), + ]) + + return keymap + + # ------------------------------------------------------------------------------ # Tool System (3D View, Grease Pencil, Edit) @@ -8967,6 +8995,28 @@ def km_sequencer_editor_tool_scale(params): ) +def km_3d_view_tool_edit_grease_pencil_interpolate(params): + return ( + "3D View Tool: Edit Grease Pencil, Interpolate", + {"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, + {"items": [ + ("grease_pencil.interpolate", params.tool_maybe_tweak_event, + None), + ]}, + ) + + +def km_3d_view_tool_paint_grease_pencil_interpolate(params): + return ( + "3D View Tool: Paint Grease Pencil, Interpolate", + {"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, + {"items": [ + ("grease_pencil.interpolate", params.tool_maybe_tweak_event, + None), + ]}, + ) + + # ------------------------------------------------------------------------------ # Full Configuration @@ -9116,6 +9166,7 @@ def generate_keymaps(params=None): km_node_link_modal_map(params), km_grease_pencil_primitive_tool_modal_map(params), km_grease_pencil_fill_tool_modal_map(params), + km_grease_pencil_interpolate_tool_modal_map(params), # Gizmos. km_generic_gizmo(params), @@ -9265,6 +9316,8 @@ def generate_keymaps(params=None): km_sequencer_editor_tool_move(params), km_sequencer_editor_tool_rotate(params), km_sequencer_editor_tool_scale(params), + km_3d_view_tool_edit_grease_pencil_interpolate(params), + km_3d_view_tool_paint_grease_pencil_interpolate(params), ] # ------------------------------------------------------------------------------ diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 7d84c179dcd..97ceedf2c67 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -2174,6 +2174,48 @@ class _defs_grease_pencil_paint: draw_settings=draw_settings, ) + @ToolDef.from_fn + def interpolate(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("grease_pencil.interpolate") + layout.prop(props, "layers") + layout.prop(props, "exclude_breakdowns") + layout.prop(props, "flip") + layout.prop(props, "smooth_factor") + layout.prop(props, "smooth_steps") + + return dict( + idname="builtin.interpolate", + label="Interpolate", + icon="ops.pose.breakdowner", + cursor='DEFAULT', + widget=None, + keymap=(), + draw_settings=draw_settings, + ) + + +class _defs_grease_pencil_edit: + @ToolDef.from_fn + def interpolate(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("grease_pencil.interpolate") + layout.prop(props, "layers") + layout.prop(props, "exclude_breakdowns") + layout.prop(props, "flip") + layout.prop(props, "smooth_factor") + layout.prop(props, "smooth_steps") + + return dict( + idname="builtin.interpolate", + label="Interpolate", + icon="ops.pose.breakdowner", + cursor='DEFAULT', + widget=None, + keymap=(), + draw_settings=draw_settings, + ) + class _defs_image_generic: @@ -3610,6 +3652,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_edit_mesh.tosphere, ), None, + _defs_grease_pencil_edit.interpolate, + None, *_tools_annotate, ], 'PARTICLE': [ @@ -3725,6 +3769,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_grease_pencil_paint.curve, _defs_grease_pencil_paint.box, _defs_grease_pencil_paint.circle, + None, + _defs_grease_pencil_paint.interpolate, ], 'PAINT_GPENCIL': [ _defs_view3d_generic.cursor, diff --git a/source/blender/blenkernel/BKE_grease_pencil.hh b/source/blender/blenkernel/BKE_grease_pencil.hh index 48be9d72e6a..4b6ecc8e3ad 100644 --- a/source/blender/blenkernel/BKE_grease_pencil.hh +++ b/source/blender/blenkernel/BKE_grease_pencil.hh @@ -403,12 +403,13 @@ class LayerRuntime { */ class Layer : public ::GreasePencilLayer { public: + using SortedKeysIterator = const int *; + Layer(); explicit Layer(StringRefNull name); Layer(const Layer &other); ~Layer(); - public: /* Define the common functions for #TreeNode. */ TREENODE_COMMON_METHODS; /** @@ -486,6 +487,11 @@ class Layer : public ::GreasePencilLayer { * exists. */ int sorted_keys_index_at(int frame_number) const; + /** + * \returns an iterator into the `sorted_keys` span to the frame at \a frame_number or nullptr if + * no such frame exists. + */ + SortedKeysIterator sorted_keys_iterator_at(int frame_number) const; /** * \returns a pointer to the active frame at \a frame_number or nullptr if there is no frame. @@ -557,14 +563,6 @@ class Layer : public ::GreasePencilLayer { void set_view_layer_name(const char *new_name); private: - using SortedKeysIterator = const int *; - - private: - /** - * \returns an iterator into the `sorted_keys` span to the frame at \a frame_number or nullptr if - * no such frame exists. - */ - SortedKeysIterator sorted_keys_iterator_at(int frame_number) const; /** * \returns the key of the active frame at \a frame_number or #std::nullopt if no such frame * exists. diff --git a/source/blender/blenlib/BLI_length_parameterize.hh b/source/blender/blenlib/BLI_length_parameterize.hh index 659c2e3516b..b675911f823 100644 --- a/source/blender/blenlib/BLI_length_parameterize.hh +++ b/source/blender/blenlib/BLI_length_parameterize.hh @@ -157,6 +157,20 @@ void sample_uniform(Span accumulated_segment_lengths, MutableSpan r_segment_indices, MutableSpan r_factors); +/** + * Find evenly spaced samples along the lengths, starting at the end. + * + * \param accumulated_segment_lengths: The accumulated lengths of the original elements being + * sampled. Could be calculated by #accumulate_lengths. + * \param include_first_point: Generally false for cyclic sequences and true otherwise. + * \param r_segment_indices: The index of the previous point at each sample. + * \param r_factors: The portion of the length in each segment at each sample. + */ +void sample_uniform_reverse(Span accumulated_segment_lengths, + bool include_first_point, + MutableSpan r_segment_indices, + MutableSpan r_factors); + /** * For each provided sample length, find the segment index and interpolation factor. * diff --git a/source/blender/blenlib/intern/length_parameterize.cc b/source/blender/blenlib/intern/length_parameterize.cc index 812ce18ed05..3b8bddb10c7 100644 --- a/source/blender/blenlib/intern/length_parameterize.cc +++ b/source/blender/blenlib/intern/length_parameterize.cc @@ -4,6 +4,7 @@ #include "BLI_length_parameterize.hh" #include "BLI_task.hh" +#include namespace blender::length_parameterize { @@ -36,6 +37,35 @@ void sample_uniform(const Span accumulated_segment_lengths, }); } +void sample_uniform_reverse(const Span accumulated_segment_lengths, + const bool include_first_point, + MutableSpan r_segment_indices, + MutableSpan r_factors) +{ + const int count = r_segment_indices.size(); + BLI_assert(count > 0); + BLI_assert(accumulated_segment_lengths.size() >= 1); + BLI_assert( + std::is_sorted(accumulated_segment_lengths.begin(), accumulated_segment_lengths.end())); + + if (count == 1) { + r_segment_indices[0] = accumulated_segment_lengths.size() - 1; + r_factors[0] = 1.0f; + return; + } + const float total_length = accumulated_segment_lengths.last(); + const float step_length = total_length / (count - include_first_point); + threading::parallel_for(IndexRange(count), 512, [&](const IndexRange range) { + SampleSegmentHint hint; + for (const int i : range) { + /* Use maximum to avoid issues with floating point accuracy. */ + const float sample_length = std::max(0.0f, total_length - i * step_length); + sample_at_length( + accumulated_segment_lengths, sample_length, r_segment_indices[i], r_factors[i], &hint); + } + }); +} + void sample_at_lengths(const Span accumulated_segment_lengths, const Span sample_lengths, MutableSpan r_segment_indices, diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc index 4faaf39abe2..f73d43c2098 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc @@ -199,6 +199,7 @@ void ED_operatortypes_grease_pencil() ED_operatortypes_grease_pencil_material(); ED_operatortypes_grease_pencil_primitives(); ED_operatortypes_grease_pencil_weight_paint(); + ED_operatortypes_grease_pencil_interpolate(); } void ED_operatormacros_grease_pencil() @@ -247,4 +248,5 @@ void ED_keymap_grease_pencil(wmKeyConfig *keyconf) keymap_grease_pencil_fill_tool(keyconf); ED_primitivetool_modal_keymap(keyconf); ED_filltool_modal_keymap(keyconf); + ED_interpolatetool_modal_keymap(keyconf); } diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_undo.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_undo.cc index 3c27c68d5ed..d6eecae9f84 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_undo.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_undo.cc @@ -23,6 +23,7 @@ #include "DEG_depsgraph.hh" #include "DEG_depsgraph_build.hh" +#include "DNA_grease_pencil_types.h" #include "ED_grease_pencil.hh" #include "ED_undo.hh" diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index a74906837b5..f058783a0b5 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -58,10 +58,12 @@ void ED_operatortypes_grease_pencil_edit(); void ED_operatortypes_grease_pencil_material(); void ED_operatortypes_grease_pencil_primitives(); void ED_operatortypes_grease_pencil_weight_paint(); +void ED_operatortypes_grease_pencil_interpolate(); void ED_operatormacros_grease_pencil(); void ED_keymap_grease_pencil(wmKeyConfig *keyconf); void ED_primitivetool_modal_keymap(wmKeyConfig *keyconf); void ED_filltool_modal_keymap(wmKeyConfig *keyconf); +void ED_interpolatetool_modal_keymap(wmKeyConfig *keyconf); void GREASE_PENCIL_OT_stroke_cutter(wmOperatorType *ot); @@ -606,6 +608,22 @@ void draw_grease_pencil_strokes(const RegionView3D &rv3d, } // namespace image_render +enum class InterpolateFlipMode : int8_t { + /* No flip. */ + None = 0, + /* Flip always. */ + Flip, + /* Flip if needed. */ + FlipAuto, +}; + +enum class InterpolateLayerMode : int8_t { + /* Only interpolate on the active layer. */ + Active = 0, + /* Interpolate strokes on every layer. */ + All, +}; + /** * Create new strokes tracing the rendered outline of existing strokes. * \param drawing: Drawing with input strokes. diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt index 5bdc4e779bf..b58a93cfb04 100644 --- a/source/blender/editors/sculpt_paint/CMakeLists.txt +++ b/source/blender/editors/sculpt_paint/CMakeLists.txt @@ -50,6 +50,7 @@ set(SRC grease_pencil_draw_ops.cc grease_pencil_erase.cc grease_pencil_fill.cc + grease_pencil_interpolate.cc grease_pencil_paint.cc grease_pencil_sculpt_clone.cc grease_pencil_sculpt_common.cc diff --git a/source/blender/editors/sculpt_paint/grease_pencil_interpolate.cc b/source/blender/editors/sculpt_paint/grease_pencil_interpolate.cc new file mode 100644 index 00000000000..aeb4dc1cce2 --- /dev/null +++ b/source/blender/editors/sculpt_paint/grease_pencil_interpolate.cc @@ -0,0 +1,828 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_context.hh" +#include "BKE_curves.hh" +#include "BKE_grease_pencil.hh" + +#include "BLI_array_utils.hh" +#include "BLI_index_mask.hh" +#include "BLI_math_angle_types.hh" +#include "BLI_math_geom.h" +#include "BLI_math_rotation.h" +#include "BLI_math_rotation.hh" +#include "BLI_math_vector.hh" +#include "BLI_offset_indices.hh" +#include "BLI_task.hh" +#include "BLT_translation.hh" + +#include "DEG_depsgraph.hh" + +#include "DNA_grease_pencil_types.h" + +#include "ED_grease_pencil.hh" +#include "ED_numinput.hh" +#include "ED_screen.hh" + +#include "GEO_interpolate_curves.hh" +#include "GEO_smooth_curves.hh" + +#include "MEM_guardedalloc.h" + +#include "RNA_access.hh" +#include "RNA_define.hh" + +#include + +namespace blender::ed::sculpt_paint::greasepencil { + +using ed::greasepencil::InterpolateFlipMode; +using ed::greasepencil::InterpolateLayerMode; + +/* -------------------------------------------------------------------- */ +/** \name Interpolate Operator + * \{ */ + +constexpr float interpolate_factor_min = -1.0f; +constexpr float interpolate_factor_max = 2.0f; + +/* Pair of curves in a layer that get interpolated. */ +struct InterpolationPairs { + Vector from_frames; + Vector to_frames; + Vector from_curves; + Vector to_curves; +}; + +struct InterpolateOpData { + struct LayerData { + /* Curve pairs to interpolate from this layer. */ + InterpolationPairs curve_pairs; + + /* Geometry of the target frame before interpolation for restoring on cancel. */ + std::optional orig_curves; + }; + + /* Layers to include. */ + IndexMaskMemory layer_mask_memory; + IndexMask layer_mask; + /* Exclude breakdown keyframes when finding intervals. */ + bool exclude_breakdowns; + + /* Interpolation factor bias controlled by the user. */ + float shift; + /* Interpolation base factor for the active layer. */ + float init_factor; + InterpolateFlipMode flipmode; + float smooth_factor; + int smooth_steps; + + NumInput numeric_input; + Array layer_data; + int active_layer_index; +}; + +using FramesMapKeyIntervalT = std::pair; + +static std::optional find_frames_interval( + const bke::greasepencil::Layer &layer, const int frame_number, const bool exclude_breakdowns) +{ + using Layer = bke::greasepencil::Layer; + using bke::greasepencil::FramesMapKeyT; + using SortedKeysIterator = Layer::SortedKeysIterator; + + const Span sorted_keys = layer.sorted_keys(); + SortedKeysIterator prev_key_it = layer.sorted_keys_iterator_at(frame_number); + if (!prev_key_it) { + return std::nullopt; + } + SortedKeysIterator next_key_it = std::next(prev_key_it); + + /* Skip over invalid keyframes on either side. */ + auto is_valid_keyframe = [&](const FramesMapKeyT key) { + const GreasePencilFrame *frame = layer.frame_at(key); + if (!frame || frame->is_end()) { + return false; + } + if (exclude_breakdowns && frame->type == BEZT_KEYTYPE_BREAKDOWN) { + return false; + } + return true; + }; + + for (; next_key_it != sorted_keys.end(); ++next_key_it) { + if (is_valid_keyframe(*next_key_it)) { + break; + } + } + for (; prev_key_it != sorted_keys.begin(); --prev_key_it) { + if (is_valid_keyframe(*prev_key_it)) { + break; + } + } + if (next_key_it == sorted_keys.end() || !is_valid_keyframe(*prev_key_it)) { + return std::nullopt; + } + + return std::make_pair(*prev_key_it, *next_key_it); +} + +/* Build index lists for curve interpolation using index. */ +static void find_curve_mapping_from_index(const GreasePencil &grease_pencil, + const bke::greasepencil::Layer &layer, + const int current_frame, + const bool exclude_breakdowns, + InterpolationPairs &pairs) +{ + using bke::greasepencil::Drawing; + + const std::optional interval = find_frames_interval( + layer, current_frame, exclude_breakdowns); + if (!interval) { + return; + } + + BLI_assert(layer.has_drawing_at(interval->first)); + BLI_assert(layer.has_drawing_at(interval->second)); + const Drawing &from_drawing = *grease_pencil.get_drawing_at(layer, interval->first); + const Drawing &to_drawing = *grease_pencil.get_drawing_at(layer, interval->second); + + const int pairs_num = std::min(from_drawing.strokes().curves_num(), + to_drawing.strokes().curves_num()); + + const int old_pairs_num = pairs.from_frames.size(); + pairs.from_frames.append_n_times(interval->first, pairs_num); + pairs.to_frames.append_n_times(interval->second, pairs_num); + pairs.from_curves.resize(old_pairs_num + pairs_num); + pairs.to_curves.resize(old_pairs_num + pairs_num); + array_utils::fill_index_range( + pairs.from_curves.as_mutable_span().slice(old_pairs_num, pairs_num)); + array_utils::fill_index_range(pairs.to_curves.as_mutable_span().slice(old_pairs_num, pairs_num)); +} + +static bool compute_auto_flip(const Span from_positions, const Span to_positions) +{ + if (from_positions.size() < 2 || to_positions.size() < 2) { + return false; + } + + constexpr float min_angle = DEG2RADF(15); + + const float3 &from_first = from_positions.first(); + const float3 &from_last = from_positions.last(); + const float3 &to_first = to_positions.first(); + const float3 &to_last = to_positions.last(); + + /* If lines intersect at a sharp angle check distances. */ + if (isect_seg_seg_v2(from_first, to_first, from_last, to_last) == ISECT_LINE_LINE_CROSS) { + if (math::angle_between(math::normalize(to_first - from_first), + math::normalize(to_last - from_last)) + .radian() < min_angle) + { + if (math::distance_squared(from_first, to_first) >= + math::distance_squared(from_last, to_first)) + { + return math::distance_squared(from_last, to_first) >= + math::distance_squared(from_last, to_last); + } + + return math::distance_squared(from_first, to_first) < + math::distance_squared(from_first, to_last); + } + + return true; + } + + return math::dot(from_last - from_first, to_last - to_first) < 0.0f; +} + +static bke::CurvesGeometry interpolate_between_curves(const GreasePencil &grease_pencil, + const bke::greasepencil::Layer &layer, + const InterpolationPairs &curve_pairs, + const float mix_factor, + const InterpolateFlipMode flip_mode) +{ + using bke::greasepencil::Drawing; + + const int dst_curve_num = curve_pairs.from_curves.size(); + BLI_assert(curve_pairs.to_curves.size() == dst_curve_num); + BLI_assert(curve_pairs.from_frames.size() == dst_curve_num); + BLI_assert(curve_pairs.to_frames.size() == dst_curve_num); + + /* Sort pairs by unique to/from frame combinations. + * Curves for each frame pair are then interpolated together. + * Map entries are indices into the original curve_pairs array, + * so the order of strokes can be maintained. */ + Array sorted_pairs(dst_curve_num); + array_utils::fill_index_range(sorted_pairs.as_mutable_span()); + std::sort(sorted_pairs.begin(), sorted_pairs.end(), [&](const int a, const int b) { + const int from_frame_a = curve_pairs.from_frames[a]; + const int to_frame_a = curve_pairs.to_frames[a]; + const int from_frame_b = curve_pairs.from_frames[b]; + const int to_frame_b = curve_pairs.to_frames[b]; + return from_frame_a < from_frame_b || + (from_frame_a == from_frame_b && to_frame_a < to_frame_b); + }); + + /* Find ranges of sorted pairs with the same from/to frame intervals. */ + Vector pair_offsets; + const OffsetIndices curves_by_pair = [&]() { + int prev_from_frame = INT_MIN; + int prev_to_frame = INT_MIN; + int current_count = 0; + for (const int sorted_index : IndexRange(dst_curve_num)) { + const int pair_index = sorted_pairs[sorted_index]; + const int from_frame = curve_pairs.from_frames[pair_index]; + const int to_frame = curve_pairs.to_frames[pair_index]; + if (from_frame != prev_from_frame || to_frame != prev_to_frame) { + /* New pair. */ + if (current_count > 0) { + pair_offsets.append(current_count); + } + current_count = 0; + } + ++current_count; + } + if (current_count > 0) { + pair_offsets.append(current_count); + } + + /* Last entry for overall size. */ + if (pair_offsets.is_empty()) { + return OffsetIndices{}; + } + + pair_offsets.append(0); + return offset_indices::accumulate_counts_to_offsets(pair_offsets); + }(); + + /* Compute curve length and flip mode for each pair. */ + Vector dst_curve_offsets; + Vector dst_curve_flip; + const OffsetIndices dst_points_by_curve = [&]() { + for (const int pair_range_i : curves_by_pair.index_range()) { + const IndexRange pair_range = curves_by_pair[pair_range_i]; + BLI_assert(!pair_range.is_empty()); + + const int first_pair_index = sorted_pairs[pair_range.first()]; + const int from_frame = curve_pairs.from_frames[first_pair_index]; + const int to_frame = curve_pairs.to_frames[first_pair_index]; + const Drawing *from_drawing = grease_pencil.get_drawing_at(layer, from_frame); + const Drawing *to_drawing = grease_pencil.get_drawing_at(layer, to_frame); + if (!from_drawing || !to_drawing) { + continue; + } + const OffsetIndices from_points_by_curve = from_drawing->strokes().points_by_curve(); + const OffsetIndices to_points_by_curve = to_drawing->strokes().points_by_curve(); + const Span from_positions = from_drawing->strokes().positions(); + const Span to_positions = to_drawing->strokes().positions(); + + for (const int sorted_index : pair_range) { + const int pair_index = sorted_pairs[sorted_index]; + const int from_curve = curve_pairs.from_curves[pair_index]; + const int to_curve = curve_pairs.to_curves[pair_index]; + const IndexRange from_points = from_points_by_curve[from_curve]; + const IndexRange to_points = to_points_by_curve[to_curve]; + + dst_curve_offsets.append(std::max(from_points.size(), to_points.size())); + switch (flip_mode) { + case InterpolateFlipMode::None: + dst_curve_flip.append(false); + break; + case InterpolateFlipMode::Flip: + dst_curve_flip.append(true); + break; + case InterpolateFlipMode::FlipAuto: { + dst_curve_flip.append(compute_auto_flip(from_positions.slice(from_points), + to_positions.slice(to_points))); + break; + } + } + } + } + /* Last entry for overall size. */ + if (dst_curve_offsets.is_empty()) { + return OffsetIndices{}; + } + + dst_curve_offsets.append(0); + return offset_indices::accumulate_counts_to_offsets(dst_curve_offsets); + }(); + const int dst_point_num = dst_points_by_curve.total_size(); + + bke::CurvesGeometry dst_curves(dst_point_num, dst_curve_num); + /* Offsets are empty when there are no curves. */ + if (dst_curve_num > 0) { + dst_curves.offsets_for_write().copy_from(dst_curve_offsets); + } + + /* Sorted map arrays that can be passed to the interpolation function directly. + * These index maps have the same order as the sorted indices, so slices of indices can be used + * for interpolating all curves of a frame pair at once. */ + Array sorted_from_curve_indices(dst_curve_num); + Array sorted_to_curve_indices(dst_curve_num); + + for (const int pair_range_i : curves_by_pair.index_range()) { + const IndexRange pair_range = curves_by_pair[pair_range_i]; + const int first_pair_index = sorted_pairs[pair_range.first()]; + const int from_frame = curve_pairs.from_frames[first_pair_index]; + const int to_frame = curve_pairs.to_frames[first_pair_index]; + const Drawing *from_drawing = grease_pencil.get_drawing_at(layer, from_frame); + const Drawing *to_drawing = grease_pencil.get_drawing_at(layer, to_frame); + if (!from_drawing || !to_drawing) { + continue; + } + const IndexRange from_curves = from_drawing->strokes().curves_range(); + const IndexRange to_curves = to_drawing->strokes().curves_range(); + + /* Subset of target curves that are filled by this frame pair. */ + IndexMaskMemory selection_memory; + const IndexMask selection = IndexMask::from_indices(sorted_pairs.as_span().slice(pair_range), + selection_memory); + MutableSpan pair_from_indices = sorted_from_curve_indices.as_mutable_span().slice( + pair_range); + MutableSpan pair_to_indices = sorted_to_curve_indices.as_mutable_span().slice(pair_range); + for (const int i : pair_range) { + const int pair_index = sorted_pairs[i]; + sorted_from_curve_indices[i] = std::clamp( + curve_pairs.from_curves[pair_index], 0, int(from_curves.last())); + sorted_to_curve_indices[i] = std::clamp( + curve_pairs.to_curves[pair_index], 0, int(to_curves.last())); + } + geometry::interpolate_curves(from_drawing->strokes(), + to_drawing->strokes(), + pair_from_indices, + pair_to_indices, + selection, + dst_curve_flip, + mix_factor, + dst_curves); + } + + return dst_curves; +} + +static void grease_pencil_interpolate_status_indicators(bContext &C, + const InterpolateOpData &opdata) +{ + Scene &scene = *CTX_data_scene(&C); + ScrArea &area = *CTX_wm_area(&C); + + const StringRef msg = IFACE_("GPencil Interpolation: "); + + std::string status; + if (hasNumInput(&opdata.numeric_input)) { + char str_ofs[NUM_STR_REP_LEN]; + outputNumInput(&const_cast(opdata.numeric_input), str_ofs, &scene.unit); + status = msg + std::string(str_ofs); + } + else { + status = msg + std::to_string(int((opdata.init_factor + opdata.shift) * 100.0f)) + " %"; + } + + ED_area_status_text(&area, status.c_str()); + ED_workspace_status_text( + &C, IFACE_("ESC/RMB to cancel, Enter/LMB to confirm, WHEEL/MOVE to adjust factor")); +} + +/* Utility function to get a drawing at the exact frame number. */ +static bke::greasepencil::Drawing *get_drawing_at_exact_frame(GreasePencil &grease_pencil, + bke::greasepencil::Layer &layer, + const int frame_number) +{ + using bke::greasepencil::Drawing; + + const std::optional start_frame = layer.start_frame_at(frame_number); + if (start_frame && *start_frame == frame_number) { + return grease_pencil.get_editable_drawing_at(layer, frame_number); + } + return nullptr; +} + +static bke::greasepencil::Drawing *ensure_drawing_at_exact_frame( + GreasePencil &grease_pencil, + bke::greasepencil::Layer &layer, + InterpolateOpData::LayerData &layer_data, + const int frame_number) +{ + using bke::greasepencil::Drawing; + + if (Drawing *drawing = get_drawing_at_exact_frame(grease_pencil, layer, frame_number)) { + layer_data.orig_curves = drawing->strokes(); + return drawing; + } + return grease_pencil.insert_frame(layer, frame_number); +} + +static void grease_pencil_interpolate_update(bContext &C, const wmOperator &op) +{ + using bke::greasepencil::Drawing; + using bke::greasepencil::Layer; + + const auto &opdata = *static_cast(op.customdata); + const Scene &scene = *CTX_data_scene(&C); + const int current_frame = scene.r.cfra; + Object &object = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(object.data); + const auto flip_mode = InterpolateFlipMode(RNA_enum_get(op.ptr, "flip")); + + opdata.layer_mask.foreach_index([&](const int layer_index) { + Layer &layer = *grease_pencil.layer(layer_index); + const InterpolateOpData::LayerData &layer_data = opdata.layer_data[layer_index]; + + /* Drawings must be created on operator invoke. */ + Drawing *dst_drawing = get_drawing_at_exact_frame(grease_pencil, layer, current_frame); + if (dst_drawing == nullptr) { + return; + } + + const float mix_factor = opdata.init_factor + opdata.shift; + bke::CurvesGeometry interpolated_curves = interpolate_between_curves( + grease_pencil, layer, layer_data.curve_pairs, mix_factor, flip_mode); + + if (opdata.smooth_factor > 0.0f && opdata.smooth_steps > 0) { + MutableSpan positions = interpolated_curves.positions_for_write(); + geometry::smooth_curve_attribute( + interpolated_curves.curves_range(), + interpolated_curves.points_by_curve(), + VArray::ForSingle(true, interpolated_curves.points_num()), + interpolated_curves.cyclic(), + opdata.smooth_steps, + opdata.smooth_factor, + false, + false, + positions); + interpolated_curves.tag_positions_changed(); + } + + dst_drawing->strokes_for_write() = std::move(interpolated_curves); + dst_drawing->tag_topology_changed(); + }); + + grease_pencil_interpolate_status_indicators(C, opdata); + + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr); +} + +/* Restore timeline changes when cancelled. */ +static void grease_pencil_interpolate_restore(bContext &C, wmOperator &op) +{ + using bke::greasepencil::Drawing; + using bke::greasepencil::Layer; + + if (op.customdata == nullptr) { + return; + } + + const auto &opdata = *static_cast(op.customdata); + const Scene &scene = *CTX_data_scene(&C); + const int current_frame = scene.r.cfra; + Object &object = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(object.data); + + opdata.layer_mask.foreach_index([&](const int layer_index) { + Layer &layer = *grease_pencil.layer(layer_index); + const InterpolateOpData::LayerData &layer_data = opdata.layer_data[layer_index]; + + if (layer_data.orig_curves) { + /* Keyframe existed before the operator, restore geometry. */ + Drawing *drawing = grease_pencil.get_editable_drawing_at(layer, current_frame); + if (drawing) { + drawing->strokes_for_write() = *layer_data.orig_curves; + drawing->tag_topology_changed(); + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr); + } + } + else { + /* Frame was empty, remove the added drawing. */ + grease_pencil.remove_frames(layer, {current_frame}); + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr); + } + }); +} + +static bool grease_pencil_interpolate_init(const bContext &C, wmOperator &op) +{ + using bke::greasepencil::Drawing; + using bke::greasepencil::Layer; + + const Scene &scene = *CTX_data_scene(&C); + const int current_frame = scene.r.cfra; + Object &object = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(object.data); + + BLI_assert(grease_pencil.has_active_layer()); + const Layer &active_layer = *grease_pencil.get_active_layer(); + + op.customdata = MEM_new(__func__); + InterpolateOpData &data = *static_cast(op.customdata); + + data.shift = RNA_float_get(op.ptr, "shift"); + data.exclude_breakdowns = RNA_boolean_get(op.ptr, "exclude_breakdowns"); + data.flipmode = InterpolateFlipMode(RNA_enum_get(op.ptr, "flip")); + data.smooth_factor = RNA_float_get(op.ptr, "smooth_factor"); + data.smooth_steps = RNA_int_get(op.ptr, "smooth_steps"); + data.active_layer_index = *grease_pencil.get_layer_index(active_layer); + + const auto layer_mode = InterpolateLayerMode(RNA_enum_get(op.ptr, "layers")); + switch (layer_mode) { + case InterpolateLayerMode::Active: + data.layer_mask = IndexRange::from_single(data.active_layer_index); + break; + case InterpolateLayerMode::All: + data.layer_mask = IndexMask::from_predicate( + grease_pencil.layers().index_range(), + GrainSize(1024), + data.layer_mask_memory, + [&](const int layer_index) { return grease_pencil.layer(layer_index)->is_editable(); }); + break; + } + + data.layer_data.reinitialize(grease_pencil.layers().size()); + data.layer_mask.foreach_index([&](const int layer_index) { + Layer &layer = *grease_pencil.layer(layer_index); + InterpolateOpData::LayerData &layer_data = data.layer_data[layer_index]; + + /* Pair from/to curves by index. */ + find_curve_mapping_from_index( + grease_pencil, layer, current_frame, data.exclude_breakdowns, layer_data.curve_pairs); + + ensure_drawing_at_exact_frame(grease_pencil, layer, layer_data, current_frame); + }); + + const std::optional active_layer_interval = find_frames_interval( + active_layer, current_frame, data.exclude_breakdowns); + data.init_factor = active_layer_interval ? + float(current_frame - active_layer_interval->first) / + (active_layer_interval->second - active_layer_interval->first + 1) : + 0.5f; + + return true; +} + +/* Exit and free memory. */ +static void grease_pencil_interpolate_exit(bContext &C, wmOperator &op) +{ + ScrArea &area = *CTX_wm_area(&C); + + if (op.customdata == nullptr) { + return; + } + + ED_area_status_text(&area, nullptr); + ED_workspace_status_text(&C, nullptr); + + MEM_delete(static_cast(op.customdata)); + op.customdata = nullptr; +} + +static bool grease_pencil_interpolate_poll(bContext *C) +{ + if (!ed::greasepencil::active_grease_pencil_poll(C)) { + return false; + } + ToolSettings *ts = CTX_data_tool_settings(C); + if (!ts || !ts->gp_paint) { + return false; + } + /* Only 3D view */ + ScrArea *area = CTX_wm_area(C); + if (area && area->spacetype != SPACE_VIEW3D) { + return false; + } + + return true; +} + +/* Invoke handler: Initialize the operator */ +static int grease_pencil_interpolate_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) +{ + wmWindow &win = *CTX_wm_window(C); + + if (!grease_pencil_interpolate_init(*C, *op)) { + grease_pencil_interpolate_exit(*C, *op); + return OPERATOR_CANCELLED; + } + InterpolateOpData &opdata = *static_cast(op->customdata); + + /* Set cursor to indicate modal operator. */ + WM_cursor_modal_set(&win, WM_CURSOR_EW_SCROLL); + + grease_pencil_interpolate_status_indicators(*C, opdata); + + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + + WM_event_add_modal_handler(C, op); + + return OPERATOR_RUNNING_MODAL; +} + +enum class InterpolateToolModalEvent : int8_t { + Cancel = 1, + Confirm, + Increase, + Decrease, +}; + +/* Modal handler: Events handling during interactive part */ +static int grease_pencil_interpolate_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + wmWindow &win = *CTX_wm_window(C); + const ARegion ®ion = *CTX_wm_region(C); + ScrArea &area = *CTX_wm_area(C); + InterpolateOpData &opdata = *static_cast(op->customdata); + const bool has_numinput = hasNumInput(&opdata.numeric_input); + + switch (event->type) { + case EVT_MODAL_MAP: { + switch (InterpolateToolModalEvent(event->val)) { + case InterpolateToolModalEvent::Cancel: + ED_area_status_text(&area, nullptr); + ED_workspace_status_text(C, nullptr); + WM_cursor_modal_restore(&win); + + grease_pencil_interpolate_restore(*C, *op); + grease_pencil_interpolate_exit(*C, *op); + return OPERATOR_CANCELLED; + case InterpolateToolModalEvent::Confirm: + ED_area_status_text(&area, nullptr); + ED_workspace_status_text(C, nullptr); + WM_cursor_modal_restore(&win); + + /* Write current factor to properties for the next execution. */ + RNA_float_set(op->ptr, "shift", opdata.shift); + + grease_pencil_interpolate_exit(*C, *op); + return OPERATOR_FINISHED; + case InterpolateToolModalEvent::Increase: + opdata.shift = std::clamp(opdata.init_factor + opdata.shift + 0.01f, + interpolate_factor_min, + interpolate_factor_max) - + opdata.init_factor; + grease_pencil_interpolate_update(*C, *op); + break; + case InterpolateToolModalEvent::Decrease: + opdata.shift = std::clamp(opdata.init_factor + opdata.shift - 0.01f, + interpolate_factor_min, + interpolate_factor_max) - + opdata.init_factor; + grease_pencil_interpolate_update(*C, *op); + break; + } + break; + } + case MOUSEMOVE: + /* Only handle mouse-move if not doing numeric-input. */ + if (!has_numinput) { + const float mouse_pos = event->mval[0]; + const float factor = std::clamp( + mouse_pos / region.winx, interpolate_factor_min, interpolate_factor_max); + opdata.shift = factor - opdata.init_factor; + + grease_pencil_interpolate_update(*C, *op); + } + break; + default: { + if ((event->val == KM_PRESS) && handleNumInput(C, &opdata.numeric_input, event)) { + float value = (opdata.init_factor + opdata.shift) * 100.0f; + applyNumInput(&opdata.numeric_input, &value); + opdata.shift = std::clamp(value * 0.01f, interpolate_factor_min, interpolate_factor_max) - + opdata.init_factor; + + grease_pencil_interpolate_update(*C, *op); + break; + } + /* Unhandled event, allow to pass through. */ + return OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH; + } + } + + return OPERATOR_RUNNING_MODAL; +} + +static void grease_pencil_interpolate_cancel(bContext *C, wmOperator *op) +{ + grease_pencil_interpolate_restore(*C, *op); + grease_pencil_interpolate_exit(*C, *op); +} + +static void GREASE_PENCIL_OT_interpolate(wmOperatorType *ot) +{ + static const EnumPropertyItem flip_modes[] = { + {int(InterpolateFlipMode::None), "NONE", 0, "No Flip", ""}, + {int(InterpolateFlipMode::Flip), "FLIP", 0, "Flip", ""}, + {int(InterpolateFlipMode::FlipAuto), "AUTO", 0, "Automatic", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + static const EnumPropertyItem gpencil_interpolation_layer_items[] = { + {int(InterpolateLayerMode::Active), "ACTIVE", 0, "Active", ""}, + {int(InterpolateLayerMode::All), "ALL", 0, "All Layers", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + ot->name = "Grease Pencil Interpolation"; + ot->idname = "GREASE_PENCIL_OT_interpolate"; + ot->description = "Interpolate grease pencil strokes between frames"; + + ot->invoke = grease_pencil_interpolate_invoke; + ot->modal = grease_pencil_interpolate_modal; + ot->cancel = grease_pencil_interpolate_cancel; + ot->poll = grease_pencil_interpolate_poll; + + ot->flag = OPTYPE_UNDO | OPTYPE_BLOCKING; + + RNA_def_float_factor( + ot->srna, + "shift", + 0.0f, + -1.0f, + 1.0f, + "Shift", + "Bias factor for which frame has more influence on the interpolated strokes", + -0.9f, + 0.9f); + + RNA_def_enum(ot->srna, + "layers", + gpencil_interpolation_layer_items, + 0, + "Layer", + "Layers included in the interpolation"); + + RNA_def_boolean(ot->srna, + "exclude_breakdowns", + false, + "Exclude Breakdowns", + "Exclude existing Breakdowns keyframes as interpolation extremes"); + + RNA_def_enum(ot->srna, + "flip", + flip_modes, + int(InterpolateFlipMode::FlipAuto), + "Flip Mode", + "Invert destination stroke to match start and end with source stroke"); + + RNA_def_int(ot->srna, + "smooth_steps", + 1, + 1, + 3, + "Iterations", + "Number of times to smooth newly created strokes", + 1, + 3); + + RNA_def_float(ot->srna, + "smooth_factor", + 0.0f, + 0.0f, + 2.0f, + "Smooth", + "Amount of smoothing to apply to interpolated strokes, to reduce jitter/noise", + 0.0f, + 2.0f); +} + +/** \} */ + +} // namespace blender::ed::sculpt_paint::greasepencil + +/* -------------------------------------------------------------------- */ +/** \name Registration + * \{ */ + +void ED_operatortypes_grease_pencil_interpolate() +{ + using namespace blender::ed::sculpt_paint::greasepencil; + WM_operatortype_append(GREASE_PENCIL_OT_interpolate); +} + +void ED_interpolatetool_modal_keymap(wmKeyConfig *keyconf) +{ + using namespace blender::ed::sculpt_paint::greasepencil; + static const EnumPropertyItem modal_items[] = { + {int(InterpolateToolModalEvent::Cancel), "CANCEL", 0, "Cancel", ""}, + {int(InterpolateToolModalEvent::Confirm), "CONFIRM", 0, "Confirm", ""}, + {int(InterpolateToolModalEvent::Increase), "INCREASE", 0, "Increase", ""}, + {int(InterpolateToolModalEvent::Decrease), "DECREASE", 0, "Decrease", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + wmKeyMap *keymap = WM_modalkeymap_find(keyconf, "Interpolate Tool Modal Map"); + + /* This function is called for each space-type, only needs to add map once. */ + if (keymap && keymap->modal_items) { + return; + } + + keymap = WM_modalkeymap_ensure(keyconf, "Interpolate Tool Modal Map", modal_items); + + WM_modalkeymap_assign(keymap, "GREASE_PENCIL_OT_interpolate"); +} + +/** \} */ diff --git a/source/blender/geometry/CMakeLists.txt b/source/blender/geometry/CMakeLists.txt index 37de5a6dd71..c3ca7d73aa9 100644 --- a/source/blender/geometry/CMakeLists.txt +++ b/source/blender/geometry/CMakeLists.txt @@ -20,6 +20,7 @@ set(SRC intern/curve_constraints.cc intern/extend_curves.cc intern/fillet_curves.cc + intern/interpolate_curves.cc intern/join_geometries.cc intern/merge_curves.cc intern/mesh_boolean.cc @@ -57,6 +58,7 @@ set(SRC GEO_curve_constraints.hh GEO_extend_curves.hh GEO_fillet_curves.hh + GEO_interpolate_curves.hh GEO_join_geometries.hh GEO_merge_curves.hh GEO_mesh_boolean.hh diff --git a/source/blender/geometry/GEO_interpolate_curves.hh b/source/blender/geometry/GEO_interpolate_curves.hh new file mode 100644 index 00000000000..b6d3c0e1060 --- /dev/null +++ b/source/blender/geometry/GEO_interpolate_curves.hh @@ -0,0 +1,27 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "FN_field.hh" + +#include "BKE_attribute.hh" +#include "BKE_curves.hh" + +namespace blender::geometry { + +/** + * Create new curves that are interpolated between "from" and "to" curves. + * \param selection: Selection of curves in \a dst_curves that are being filled. + */ +void interpolate_curves(const bke::CurvesGeometry &from_curves, + const bke::CurvesGeometry &to_curves, + Span from_curve_indices, + Span to_curve_indices, + const IndexMask &selection, + Span curve_flip_direction, + const float mix_factor, + bke::CurvesGeometry &dst_curves); + +} // namespace blender::geometry diff --git a/source/blender/geometry/intern/interpolate_curves.cc b/source/blender/geometry/intern/interpolate_curves.cc new file mode 100644 index 00000000000..78997583db1 --- /dev/null +++ b/source/blender/geometry/intern/interpolate_curves.cc @@ -0,0 +1,397 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_attribute_math.hh" +#include "BKE_curves.hh" + +#include "BLI_assert.h" +#include "BLI_length_parameterize.hh" +#include "BLI_math_vector.hh" +#include "BLI_offset_indices.hh" +#include "BLI_task.hh" + +#include "DNA_customdata_types.h" + +#include "GEO_interpolate_curves.hh" +#include "GEO_resample_curves.hh" + +namespace blender::geometry { + +using bke::CurvesGeometry; + +/** + * Return true if the attribute should be copied/interpolated to the result curves. + * Don't output attributes that correspond to curve types that have no curves in the result. + */ +static bool interpolate_attribute_to_curves(const bke::AttributeIDRef &attribute_id, + const std::array &type_counts) +{ + if (attribute_id.is_anonymous()) { + return true; + } + if (ELEM(attribute_id.name(), + "handle_type_left", + "handle_type_right", + "handle_left", + "handle_right")) + { + return type_counts[CURVE_TYPE_BEZIER] != 0; + } + if (ELEM(attribute_id.name(), "nurbs_weight")) { + return type_counts[CURVE_TYPE_NURBS] != 0; + } + return true; +} + +/** + * Return true if the attribute should be copied to poly curves. + */ +static bool interpolate_attribute_to_poly_curve(const bke::AttributeIDRef &attribute_id) +{ + static const Set no_interpolation{{ + "handle_type_left", + "handle_type_right", + "handle_right", + "handle_left", + "nurbs_weight", + }}; + return !no_interpolation.contains(attribute_id.name()); +} + +struct AttributesForInterpolation { + Vector src_from; + Vector src_to; + + Vector dst; +}; + +/** + * Retrieve spans from source and result attributes. + */ +static AttributesForInterpolation retrieve_attribute_spans(const Span ids, + const CurvesGeometry &src_from_curves, + const CurvesGeometry &src_to_curves, + CurvesGeometry &dst_curves) +{ + AttributesForInterpolation result; + + const bke::AttributeAccessor src_from_attributes = src_from_curves.attributes(); + const bke::AttributeAccessor src_to_attributes = src_to_curves.attributes(); + bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write(); + for (const int i : ids.index_range()) { + eCustomDataType data_type; + + const GVArray src_from_attribute = *src_from_attributes.lookup(ids[i], bke::AttrDomain::Point); + if (src_from_attribute) { + data_type = bke::cpp_type_to_custom_data_type(src_from_attribute.type()); + + const GVArray src_to_attribute = *src_to_attributes.lookup( + ids[i], bke::AttrDomain::Point, data_type); + + result.src_from.append(src_from_attribute); + result.src_to.append(src_to_attribute ? src_to_attribute : GVArraySpan{}); + } + else { + const GVArray src_to_attribute = *src_to_attributes.lookup(ids[i], bke::AttrDomain::Point); + /* Attribute should exist on at least one of the geometries. */ + BLI_assert(src_to_attribute); + + data_type = bke::cpp_type_to_custom_data_type(src_to_attribute.type()); + + result.src_from.append(GVArraySpan{}); + result.src_to.append(src_to_attribute); + } + + bke::GSpanAttributeWriter dst_attribute = dst_attributes.lookup_or_add_for_write_only_span( + ids[i], bke::AttrDomain::Point, data_type); + result.dst.append(std::move(dst_attribute)); + } + + return result; +} + +/** + * Gather a set of all generic attribute IDs to copy to the result curves. + */ +static AttributesForInterpolation gather_point_attributes_to_interpolate( + const CurvesGeometry &from_curves, const CurvesGeometry &to_curves, CurvesGeometry &dst_curves) +{ + VectorSet ids; + auto add_attribute = [&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) { + if (meta_data.domain != bke::AttrDomain::Point) { + return true; + } + if (meta_data.data_type == CD_PROP_STRING) { + return true; + } + if (!interpolate_attribute_to_curves(id, dst_curves.curve_type_counts())) { + return true; + } + if (interpolate_attribute_to_poly_curve(id)) { + ids.add(id); + } + return true; + }; + + from_curves.attributes().for_all(add_attribute); + to_curves.attributes().for_all(add_attribute); + + /* Position is handled differently since it has non-generic interpolation for Bezier + * curves and because the evaluated positions are cached for each evaluated point. */ + ids.remove_contained("position"); + + return retrieve_attribute_spans(ids, from_curves, to_curves, dst_curves); +} + +/* Resample a span of attribute values from source curves to a destination buffer. */ +static void sample_curve_attribute(const bke::CurvesGeometry &src_curves, + const OffsetIndices dst_points_by_curve, + const GSpan src_data, + const IndexMask &curve_selection, + const Span sample_indices, + const Span sample_factors, + GMutableSpan dst_data) +{ + const CPPType &type = src_data.type(); + BLI_assert(dst_data.type() == type); + + const OffsetIndices src_points_by_curve = src_curves.points_by_curve(); + const OffsetIndices src_evaluated_points_by_curve = src_curves.evaluated_points_by_curve(); + const VArray curve_types = src_curves.curve_types(); + +#ifndef NDEBUG + const int dst_points_num = dst_data.size(); + BLI_assert(sample_indices.size() == dst_points_num); + BLI_assert(sample_factors.size() == dst_points_num); +#endif + + bke::attribute_math::convert_to_static_type(type, [&](auto dummy) { + using T = decltype(dummy); + Span src = src_data.typed(); + MutableSpan dst = dst_data.typed(); + + Vector evaluated_data; + curve_selection.foreach_index([&](const int i_curve) { + const IndexRange src_points = src_points_by_curve[i_curve]; + const IndexRange dst_points = dst_points_by_curve[i_curve]; + + if (curve_types[i_curve] == CURVE_TYPE_POLY) { + length_parameterize::interpolate(src.slice(src_points), + sample_indices.slice(dst_points), + sample_factors.slice(dst_points), + dst.slice(dst_points)); + } + else { + const IndexRange src_evaluated_points = src_evaluated_points_by_curve[i_curve]; + evaluated_data.reinitialize(src_evaluated_points.size()); + src_curves.interpolate_to_evaluated( + i_curve, src.slice(src_points), evaluated_data.as_mutable_span()); + length_parameterize::interpolate(evaluated_data.as_span(), + sample_indices.slice(dst_points), + sample_factors.slice(dst_points), + dst.slice(dst_points)); + } + }); + }); +} + +template +static void mix_arrays(const Span from, + const Span to, + const float mix_factor, + const MutableSpan dst) +{ + for (const int i : dst.index_range()) { + dst[i] = math::interpolate(from[i], to[i], mix_factor); + } +} + +static void mix_arrays(const GSpan src_from, + const GSpan src_to, + const float mix_factor, + const IndexMask &group_selection, + const OffsetIndices groups, + const GMutableSpan dst) +{ + group_selection.foreach_index(GrainSize(32), [&](const int curve) { + const IndexRange range = groups[curve]; + bke::attribute_math::convert_to_static_type(dst.type(), [&](auto dummy) { + using T = decltype(dummy); + const Span from = src_from.typed(); + const Span to = src_to.typed(); + const MutableSpan dst_typed = dst.typed(); + mix_arrays(from.slice(range), to.slice(range), mix_factor, dst_typed.slice(range)); + }); + }); +} + +void interpolate_curves(const CurvesGeometry &from_curves, + const CurvesGeometry &to_curves, + const Span from_curve_indices, + const Span to_curve_indices, + const IndexMask &selection, + const Span curve_flip_direction, + const float mix_factor, + CurvesGeometry &dst_curves) +{ + BLI_assert(from_curve_indices.size() == selection.size()); + BLI_assert(to_curve_indices.size() == selection.size()); + + if (from_curves.curves_num() == 0 || to_curves.curves_num() == 0) { + return; + } + + const VArray from_curves_cyclic = from_curves.cyclic(); + const VArray to_curves_cyclic = to_curves.cyclic(); + const Span from_evaluated_positions = from_curves.evaluated_positions(); + const Span to_evaluated_positions = to_curves.evaluated_positions(); + + /* All resampled curves are poly curves. */ + dst_curves.fill_curve_types(selection, CURVE_TYPE_POLY); + + MutableSpan dst_positions = dst_curves.positions_for_write(); + + AttributesForInterpolation attributes = gather_point_attributes_to_interpolate( + from_curves, to_curves, dst_curves); + + from_curves.ensure_evaluated_lengths(); + to_curves.ensure_evaluated_lengths(); + + /* Sampling arbitrary attributes works by first interpolating them to the curve's standard + * "evaluated points" and then interpolating that result with the uniform samples. This is + * potentially wasteful when down-sampling a curve to many fewer points. There are two possible + * solutions: only sample the necessary points for interpolation, or first sample curve + * parameter/segment indices and evaluate the curve directly. */ + Array from_sample_indices(dst_curves.points_num()); + Array to_sample_indices(dst_curves.points_num()); + Array from_sample_factors(dst_curves.points_num()); + Array to_sample_factors(dst_curves.points_num()); + + const OffsetIndices dst_points_by_curve = dst_curves.points_by_curve(); + + /* Gather uniform samples based on the accumulated lengths of the original curve. */ + selection.foreach_index(GrainSize(32), [&](const int i_dst_curve, const int pos) { + const int i_from_curve = from_curve_indices[pos]; + const int i_to_curve = to_curve_indices[pos]; + const IndexRange dst_points = dst_points_by_curve[i_dst_curve]; + const Span from_lengths = from_curves.evaluated_lengths_for_curve( + i_from_curve, from_curves_cyclic[i_from_curve]); + const Span to_lengths = to_curves.evaluated_lengths_for_curve( + i_to_curve, to_curves_cyclic[i_to_curve]); + + if (from_lengths.is_empty()) { + /* Handle curves with only one evaluated point. */ + from_sample_indices.as_mutable_span().slice(dst_points).fill(0); + from_sample_factors.as_mutable_span().slice(dst_points).fill(0.0f); + } + else { + length_parameterize::sample_uniform(from_lengths, + !from_curves_cyclic[i_from_curve], + from_sample_indices.as_mutable_span().slice(dst_points), + from_sample_factors.as_mutable_span().slice(dst_points)); + } + if (to_lengths.is_empty()) { + /* Handle curves with only one evaluated point. */ + to_sample_indices.as_mutable_span().slice(dst_points).fill(0); + to_sample_factors.as_mutable_span().slice(dst_points).fill(0.0f); + } + else { + if (curve_flip_direction[i_dst_curve]) { + length_parameterize::sample_uniform_reverse( + to_lengths, + !to_curves_cyclic[i_to_curve], + to_sample_indices.as_mutable_span().slice(dst_points), + to_sample_factors.as_mutable_span().slice(dst_points)); + } + else { + length_parameterize::sample_uniform(to_lengths, + !to_curves_cyclic[i_to_curve], + to_sample_indices.as_mutable_span().slice(dst_points), + to_sample_factors.as_mutable_span().slice(dst_points)); + } + } + }); + + /* For every attribute, evaluate attributes from every curve in the range in the original + * curve's "evaluated points", then use linear interpolation to sample to the result. */ + for (const int i_attribute : attributes.dst.index_range()) { + const GSpan src_from = attributes.src_from[i_attribute]; + const GSpan src_to = attributes.src_to[i_attribute]; + GMutableSpan dst = attributes.dst[i_attribute].span; + + /* Mix factors depend on which of the from/to curves geometries has attribute data. If + * only one geometry has attribute data it gets the full mix weight. */ + if (!src_from.is_empty() && !src_to.is_empty()) { + GArray<> from_samples(dst.type(), dst.size()); + GArray<> to_samples(dst.type(), dst.size()); + sample_curve_attribute(from_curves, + dst_points_by_curve, + src_from, + selection, + from_sample_indices, + from_sample_factors, + from_samples); + sample_curve_attribute(to_curves, + dst_points_by_curve, + src_to, + selection, + to_sample_indices, + to_sample_factors, + to_samples); + mix_arrays(from_samples, to_samples, mix_factor, selection, dst_points_by_curve, dst); + } + else if (!src_from.is_empty()) { + sample_curve_attribute(from_curves, + dst_points_by_curve, + src_from, + selection, + from_sample_indices, + from_sample_factors, + dst); + } + else if (!src_to.is_empty()) { + sample_curve_attribute(to_curves, + dst_points_by_curve, + src_to, + selection, + to_sample_indices, + to_sample_factors, + dst); + } + } + + { + Array from_samples(dst_positions.size()); + Array to_samples(dst_positions.size()); + + /* Interpolate the evaluated positions to the resampled curves. */ + sample_curve_attribute(from_curves, + dst_points_by_curve, + from_evaluated_positions, + selection, + from_sample_indices, + from_sample_factors, + from_samples.as_mutable_span()); + sample_curve_attribute(to_curves, + dst_points_by_curve, + to_evaluated_positions, + selection, + to_sample_indices, + to_sample_factors, + to_samples.as_mutable_span()); + + mix_arrays(from_samples.as_span(), + to_samples.as_span(), + mix_factor, + selection, + dst_points_by_curve, + dst_positions); + } + + for (bke::GSpanAttributeWriter &attribute : attributes.dst) { + attribute.finish(); + } +} + +} // namespace blender::geometry