GPv3: Automerge feature for joining curve endpoints by distance

This implements the _automerge_ feature which finds nearby end points
during stroke draw and merges the new curve with existing strokes.

New utility functions are added in `geometry` for slightly generalized
functionality. The `curves_merge_endpoints` takes an index map that
describes how to connect curves by index. It performs a topological sort
and reorders connected curves into contiguous ranges. This can be used
with an arbitrary number of connected curves. The tool feature itself
only uses a single curve, based on 2D end point positions.

Unit tests have been added for the curve merge utility function.

Pull Request: https://projects.blender.org/blender/blender/pulls/124459
This commit is contained in:
Lukas Tönne
2024-07-12 11:21:23 +02:00
parent fdf10495d0
commit d0089e6fe1
7 changed files with 675 additions and 0 deletions

View File

@@ -6,18 +6,29 @@
* \ingroup edgreasepencil
*/
#include <algorithm>
#include <limits>
#include "BLI_array_utils.hh"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_kdtree.h"
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_rect.h"
#include "BLI_stack.hh"
#include "BLI_task.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_curves_utils.hh"
#include "BKE_grease_pencil.hh"
#include "DNA_curves_types.h"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "GEO_merge_curves.hh"
extern "C" {
#include "curve_fit_nd.h"
@@ -332,6 +343,98 @@ blender::bke::CurvesGeometry curves_merge_by_distance(
return dst_curves;
}
bke::CurvesGeometry curves_merge_endpoints_by_distance(
const ARegion &region,
const bke::CurvesGeometry &src_curves,
const float4x4 &layer_to_world,
const float merge_distance,
const IndexMask &selection,
const bke::AnonymousAttributePropagationInfo &propagation_info)
{
const OffsetIndices src_points_by_curve = src_curves.points_by_curve();
const Span<float3> src_positions = src_curves.positions();
const float merge_distance_squared = merge_distance * merge_distance;
Array<float2> screen_start_points(src_curves.curves_num());
Array<float2> screen_end_points(src_curves.curves_num());
/* For comparing screen space positions use a 2D KDTree. Each curve adds 2 points. */
KDTree_2d *tree = BLI_kdtree_2d_new(2 * src_curves.curves_num());
threading::parallel_for(src_curves.curves_range(), 1024, [&](const IndexRange range) {
for (const int src_i : range) {
const IndexRange points = src_points_by_curve[src_i];
const float3 start_pos = src_positions[points.first()];
const float3 end_pos = src_positions[points.last()];
const float3 start_world = math::transform_point(layer_to_world, start_pos);
const float3 end_world = math::transform_point(layer_to_world, end_pos);
ED_view3d_project_float_global(
&region, start_world, screen_start_points[src_i], V3D_PROJ_TEST_NOP);
ED_view3d_project_float_global(
&region, end_world, screen_end_points[src_i], V3D_PROJ_TEST_NOP);
}
});
/* Note: KDTree insertion is not thread-safe, don't parallelize this. */
for (const int src_i : src_curves.curves_range()) {
BLI_kdtree_2d_insert(tree, src_i * 2, screen_start_points[src_i]);
BLI_kdtree_2d_insert(tree, src_i * 2 + 1, screen_end_points[src_i]);
}
BLI_kdtree_2d_balance(tree);
Array<int> connect_to_curve(src_curves.curves_num(), -1);
Array<bool> flip_direction(src_curves.curves_num(), false);
selection.foreach_index(GrainSize(512), [&](const int src_i) {
const float2 &start_co = screen_start_points[src_i];
const float2 &end_co = screen_end_points[src_i];
/* Index of KDTree points so they can be ignored. */
const int start_index = src_i * 2;
const int end_index = src_i * 2 + 1;
KDTreeNearest_2d nearest_start, nearest_end;
const bool is_start_ok = (BLI_kdtree_2d_find_nearest_cb_cpp(
tree,
start_co,
&nearest_start,
[&](const int other, const float * /*co*/, const float dist_sq) {
if (start_index == other || dist_sq > merge_distance_squared) {
return 0;
}
return 1;
}) != -1);
const bool is_end_ok = (BLI_kdtree_2d_find_nearest_cb_cpp(
tree,
end_co,
&nearest_end,
[&](const int other, const float * /*co*/, const float dist_sq) {
if (end_index == other || dist_sq > merge_distance_squared) {
return 0;
}
return 1;
}) != -1);
if (is_start_ok) {
const int curve_index = nearest_start.index / 2;
const bool is_end_point = bool(nearest_start.index % 2);
if (connect_to_curve[curve_index] < 0) {
connect_to_curve[curve_index] = src_i;
flip_direction[curve_index] = !is_end_point;
}
}
if (is_end_ok) {
const int curve_index = nearest_end.index / 2;
const bool is_end_point = bool(nearest_end.index % 2);
if (connect_to_curve[src_i] < 0) {
connect_to_curve[src_i] = curve_index;
flip_direction[curve_index] = is_end_point;
}
}
});
BLI_kdtree_2d_free(tree);
return geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, propagation_info);
}
/* Generate points in an counter-clockwise arc between two directions. */
static void generate_arc_from_point_to_point(const float3 &from,
const float3 &to,

View File

@@ -355,17 +355,36 @@ IndexMask polyline_detect_corners(Span<float2> points,
float angle_threshold,
IndexMaskMemory &memory);
/**
* Merge points that are close together on each selected curve.
* Points are not merged across curves.
*/
bke::CurvesGeometry curves_merge_by_distance(
const bke::CurvesGeometry &src_curves,
const float merge_distance,
const IndexMask &selection,
const bke::AnonymousAttributePropagationInfo &propagation_info);
/**
* Merge points on the same curve that are close together.
*/
int curve_merge_by_distance(const IndexRange points,
const Span<float> distances,
const IndexMask &selection,
const float merge_distance,
MutableSpan<int> r_merge_indices);
/**
* Connect selected curve endpoints with the closest endpoints of other curves.
*/
bke::CurvesGeometry curves_merge_endpoints_by_distance(
const ARegion &region,
const bke::CurvesGeometry &src_curves,
const float4x4 &layer_to_world,
const float merge_distance,
const IndexMask &selection,
const bke::AnonymousAttributePropagationInfo &propagation_info);
/**
* Structure describing a point in the destination relatively to the source.
* If a point in the destination \a is_src_point, then it corresponds

View File

@@ -29,6 +29,7 @@
#include "DNA_brush_enums.h"
#include "DNA_material_types.h"
#include "DNA_scene_types.h"
#include "ED_curves.hh"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
@@ -1344,6 +1345,7 @@ void PaintOperation::on_stroke_done(const bContext &C)
Scene *scene = CTX_data_scene(&C);
Object *object = CTX_data_active_object(&C);
RegionView3D *rv3d = CTX_wm_region_view3d(&C);
const ARegion *region = CTX_wm_region(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object->data);
Paint *paint = &scene->toolsettings->gp_paint->paint;
@@ -1351,6 +1353,8 @@ void PaintOperation::on_stroke_done(const bContext &C)
BrushGpencilSettings *settings = brush->gpencil_settings;
const bool on_back = (scene->toolsettings->gpencil_flags & GP_TOOL_FLAG_PAINT_ONBACK) != 0;
const bool do_post_processing = (settings->flag & GP_BRUSH_GROUP_SETTINGS) != 0;
const bool do_automerge_endpoints = (scene->toolsettings->gpencil_flags &
GP_TOOL_FLAG_AUTOMERGE_STROKE) != 0;
/* Grease Pencil should have an active layer. */
BLI_assert(grease_pencil.has_active_layer());
@@ -1413,6 +1417,15 @@ void PaintOperation::on_stroke_done(const bContext &C)
attributes.remove(".draw_tool_screen_space_positions");
drawing.set_texture_matrices({texture_space_}, IndexRange::from_single(active_curve));
if (do_automerge_endpoints) {
constexpr float merge_distance = 20.0f;
const float4x4 layer_to_world = active_layer.to_world_space(*object);
const IndexMask selection = IndexRange::from_single(active_curve);
drawing.strokes_for_write() = ed::greasepencil::curves_merge_endpoints_by_distance(
*region, drawing.strokes(), layer_to_world, merge_distance, selection, {});
}
drawing.tag_topology_changed();
/* Now we're done drawing. */

View File

@@ -21,6 +21,7 @@ set(SRC
intern/extend_curves.cc
intern/fillet_curves.cc
intern/join_geometries.cc
intern/merge_curves.cc
intern/mesh_boolean.cc
intern/mesh_copy_selection.cc
intern/mesh_merge_by_distance.cc
@@ -57,6 +58,7 @@ set(SRC
GEO_extend_curves.hh
GEO_fillet_curves.hh
GEO_join_geometries.hh
GEO_merge_curves.hh
GEO_mesh_boolean.hh
GEO_mesh_copy_selection.hh
GEO_mesh_merge_by_distance.hh
@@ -135,3 +137,14 @@ if(WITH_GMP)
endif()
blender_add_lib(bf_geometry "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_INC
)
set(TEST_SRC
tests/GEO_merge_curves_test.cc
)
set(TEST_LIB
)
blender_add_test_suite_lib(bf_geometry_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,24 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_curves.hh"
namespace blender::geometry {
/**
* Join each selected curve's end point with another curve's start point to form a single curve.
*
* \param connect_to_curve: Index of the curve to connect to, invalid indices are ignored
* (set to -1 to leave a curve disconnected).
* \param flip_direction: Flip direction of input curves.
*/
bke::CurvesGeometry curves_merge_endpoints(
const bke::CurvesGeometry &src_curves,
Span<int> connect_to_curve,
Span<bool> flip_direction,
const bke::AnonymousAttributePropagationInfo &propagation_info);
}; // namespace blender::geometry

View File

@@ -0,0 +1,308 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array_utils.hh"
#include "BLI_stack.hh"
#include "GEO_merge_curves.hh"
namespace blender::geometry {
enum Flag {
OnStack = 1,
Inserted = 2,
};
template<typename Fn>
static void foreach_connected_curve(const Span<int> connect_to_curve,
MutableSpan<uint8_t> flags,
const int start,
Fn fn)
{
const IndexRange range = connect_to_curve.index_range();
Stack<int> stack;
bool has_cycle = false;
auto push_curve = [&](const int curve_i) -> bool {
if ((flags[curve_i] & Inserted) != 0) {
return false;
}
if ((flags[curve_i] & OnStack) != 0) {
has_cycle = true;
return false;
}
stack.push(curve_i);
flags[curve_i] |= OnStack;
fn(curve_i);
return true;
};
push_curve(start);
while (!stack.is_empty()) {
const int current = stack.peek();
const int next = connect_to_curve[current];
if (range.contains(next)) {
if (push_curve(next)) {
continue;
}
}
flags[current] |= Inserted;
stack.pop();
}
UNUSED_VARS(has_cycle);
}
/* Topological sorting that puts connected curves into contiguous ranges. */
static Vector<int> toposort_connected_curves(const Span<int> connect_to_curve)
{
const IndexRange range = connect_to_curve.index_range();
Array<uint8_t> flags(connect_to_curve.size());
/* First add all open chains by finding curves without a connection. */
Array<bool> is_start_curve(range.size(), true);
for (const int curve_i : range) {
const int next = connect_to_curve[curve_i];
if (range.contains(next)) {
is_start_curve[next] = false;
}
}
/* Mark all curves that can be reached from a start curve. These must not be added before the
* start curve, or it can lead to gaps in curve ranges. */
flags.fill(0);
Array<bool> is_reachable(range.size(), false);
for (const int curve_i : range) {
if (is_start_curve[curve_i]) {
foreach_connected_curve(
connect_to_curve, flags, curve_i, [&](const int index) { is_reachable[index] = true; });
}
}
Vector<int> sorted_curves;
sorted_curves.reserve(connect_to_curve.size());
flags.fill(0);
for (const int curve_i : range) {
if (is_start_curve[curve_i] || !is_reachable[curve_i]) {
foreach_connected_curve(
connect_to_curve, flags, curve_i, [&](const int index) { sorted_curves.append(index); });
}
}
BLI_assert(sorted_curves.size() == range.size());
return sorted_curves;
}
/* TODO Add an optimized function for reversing the order of spans. */
static void reverse_order(GMutableSpan span)
{
const CPPType &cpptype = span.type();
BUFFER_FOR_CPP_TYPE_VALUE(cpptype, buffer);
cpptype.default_construct(buffer);
for (const int i : IndexRange(span.size() / 2)) {
const int mirror_i = span.size() - 1 - i;
/* Swap. */
cpptype.move_assign(span[i], buffer);
cpptype.move_assign(span[mirror_i], span[i]);
cpptype.move_assign(buffer, span[mirror_i]);
}
cpptype.destruct(buffer);
}
static void reorder_and_flip_attributes_group_to_group(
const bke::AttributeAccessor src_attributes,
const bke::AttrDomain domain,
const OffsetIndices<int> src_offsets,
const OffsetIndices<int> dst_offsets,
const Span<int> old_by_new_map,
const Span<bool> flip_direction,
bke::MutableAttributeAccessor dst_attributes)
{
src_attributes.for_all(
[&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
if (meta_data.domain != domain) {
return true;
}
if (meta_data.data_type == CD_PROP_STRING) {
return true;
}
const GVArray src = *src_attributes.lookup(id, domain);
bke::GSpanAttributeWriter dst = dst_attributes.lookup_or_add_for_write_only_span(
id, domain, meta_data.data_type);
if (!dst) {
return true;
}
threading::parallel_for(old_by_new_map.index_range(), 1024, [&](const IndexRange range) {
for (const int new_i : range) {
const int old_i = old_by_new_map[new_i];
const bool flip = flip_direction[old_i];
GMutableSpan dst_span = dst.span.slice(dst_offsets[new_i]);
array_utils::copy(src.slice(src_offsets[old_i]), dst_span);
if (flip) {
reverse_order(dst_span);
}
}
});
dst.finish();
return true;
});
}
static bke::CurvesGeometry reorder_and_flip_curves(const bke::CurvesGeometry &src_curves,
const Span<int> old_by_new_map,
const Span<bool> flip_direction)
{
bke::CurvesGeometry dst_curves = bke::CurvesGeometry(src_curves);
bke::gather_attributes(src_curves.attributes(),
bke::AttrDomain::Curve,
{},
{},
old_by_new_map,
dst_curves.attributes_for_write());
const Span<int> old_offsets = src_curves.offsets();
MutableSpan<int> new_offsets = dst_curves.offsets_for_write();
offset_indices::gather_group_sizes(old_offsets, old_by_new_map, new_offsets);
offset_indices::accumulate_counts_to_offsets(new_offsets);
reorder_and_flip_attributes_group_to_group(src_curves.attributes(),
bke::AttrDomain::Point,
old_offsets,
new_offsets.as_span(),
old_by_new_map,
flip_direction,
dst_curves.attributes_for_write());
dst_curves.tag_topology_changed();
return dst_curves;
}
/* Build new offsets array for connected ranges. */
static void find_connected_ranges(const bke::CurvesGeometry &src_curves,
const Span<int> old_by_new_map,
Span<int> connect_to_curve,
Span<bool> cyclic,
Vector<int> &r_joined_curve_offsets,
Vector<bool> &r_joined_cyclic)
{
const IndexRange curves_range = src_curves.curves_range();
Array<int> new_by_old_map(old_by_new_map.size());
for (const int dst_i : old_by_new_map.index_range()) {
const int src_i = old_by_new_map[dst_i];
new_by_old_map[src_i] = dst_i;
}
r_joined_curve_offsets.reserve(curves_range.size() + 1);
r_joined_cyclic.reserve(curves_range.size());
int start_index = -1;
for (const int dst_i : curves_range) {
const int src_i = old_by_new_map[dst_i];
/* Strokes are cyclic if they are not connected and the original stroke is cyclic, or if the
* the last stroke of a chain is merged with the first stroke. */
const bool src_cyclic = cyclic[src_i];
if (start_index < 0) {
r_joined_curve_offsets.append(0);
r_joined_cyclic.append(src_cyclic);
start_index = dst_i;
}
++r_joined_curve_offsets.last();
const int src_connect_to = connect_to_curve[src_i];
const bool is_connected = curves_range.contains(src_connect_to);
const int dst_connect_to = is_connected ? new_by_old_map[src_connect_to] : -1;
/* Check for end of chain. */
if (dst_connect_to != dst_i + 1) {
/* Set cyclic state for connected curves.
* Becomes cyclic if connected to the start. */
const bool is_chain = (is_connected || dst_i != start_index);
if (is_chain) {
r_joined_cyclic.last() = (dst_connect_to == start_index);
}
/* Start new curve. */
start_index = -1;
}
}
/* Offsets has one more entry for the overall size. */
r_joined_curve_offsets.append(0);
offset_indices::accumulate_counts_to_offsets(r_joined_curve_offsets);
}
static bke::CurvesGeometry join_curves_ranges(const bke::CurvesGeometry &src_curves,
const OffsetIndices<int> old_curves_by_new)
{
bke::CurvesGeometry dst_curves = bke::CurvesGeometry(src_curves.points_num(),
old_curves_by_new.size());
/* Note: using the offsets as an index map means the first curve of each range is used for
* attributes. */
const Span<int> old_by_new_map = old_curves_by_new.data().drop_back(1);
bke::gather_attributes(src_curves.attributes(),
bke::AttrDomain::Curve,
{},
{"cyclic"},
old_by_new_map,
dst_curves.attributes_for_write());
const OffsetIndices old_points_by_curve = src_curves.points_by_curve();
MutableSpan<int> new_offsets = dst_curves.offsets_for_write();
new_offsets.fill(0);
for (const int new_i : new_offsets.index_range().drop_back(1)) {
const IndexRange old_curves = old_curves_by_new[new_i];
for (const int old_i : old_curves) {
new_offsets[new_i] += old_points_by_curve[old_i].size();
}
}
offset_indices::accumulate_counts_to_offsets(new_offsets);
/* Point attributes copied without changes. */
bke::copy_attributes(
src_curves.attributes(), bke::AttrDomain::Point, {}, {}, dst_curves.attributes_for_write());
dst_curves.tag_topology_changed();
return dst_curves;
}
bke::CurvesGeometry curves_merge_endpoints(
const bke::CurvesGeometry &src_curves,
Span<int> connect_to_curve,
Span<bool> flip_direction,
const bke::AnonymousAttributePropagationInfo & /*propagation_info*/)
{
BLI_assert(connect_to_curve.size() == src_curves.curves_num());
const VArraySpan<bool> src_cyclic = src_curves.cyclic();
Vector<int> old_by_new_map = toposort_connected_curves(connect_to_curve);
Vector<int> joined_curve_offsets;
Vector<bool> cyclic;
find_connected_ranges(
src_curves, old_by_new_map, connect_to_curve, src_cyclic, joined_curve_offsets, cyclic);
bke::CurvesGeometry ordered_curves = reorder_and_flip_curves(
src_curves, old_by_new_map, flip_direction);
OffsetIndices joined_curves_by_new = OffsetIndices<int>(joined_curve_offsets);
bke::CurvesGeometry merged_curves = join_curves_ranges(ordered_curves, joined_curves_by_new);
merged_curves.cyclic_for_write().copy_from(cyclic);
return merged_curves;
}
} // namespace blender::geometry

View File

@@ -0,0 +1,195 @@
/* SPDX-License-Identifier: Apache-2.0 */
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BLI_array_utils.hh"
#include "GEO_merge_curves.hh"
#include "testing/testing.h"
using namespace blender::bke;
namespace blender::geometry::tests {
static bke::CurvesGeometry create_test_curves(Span<int> offsets, Span<bool> cyclic)
{
BLI_assert(!offsets.is_empty());
const int curves_num = offsets.size() - 1;
BLI_assert(cyclic.size() == curves_num);
const int points_num = offsets.last();
bke::CurvesGeometry curves(points_num, curves_num);
curves.offsets_for_write().copy_from(offsets);
curves.cyclic_for_write().copy_from(cyclic);
/* Attribute storing original indices to test point remapping. */
SpanAttributeWriter<int> test_indices_writer =
curves.attributes_for_write().lookup_or_add_for_write_span<int>(
"test_index", bke::AttrDomain::Point, bke::AttributeInitConstruct());
array_utils::fill_index_range(test_indices_writer.span);
test_indices_writer.finish();
return curves;
}
TEST(merge_curves, NoConnections)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve(4, -1);
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 4);
EXPECT_EQ_ARRAY(Span({0, 3, 6, 9, 12}).data(), dst_curves.offsets().data(), 5);
EXPECT_EQ_ARRAY(Span({false, true, true, false}).data(), cyclic.data(), 4);
}
TEST(merge_curves, ConnectSingleCurve)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve = {-1, -1, -1, 1};
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 3);
EXPECT_EQ_ARRAY(Span({0, 3, 6, 12}).data(), dst_curves.offsets().data(), 4);
EXPECT_EQ_ARRAY(Span({false, true, false}).data(), cyclic.data(), 3);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 6, 7, 8, 9, 10, 11, 3, 4, 5}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, ReverseCurves)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve = {-1, -1, -1, -1};
Array<bool> flip_direction = {false, true, false, true};
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 4);
EXPECT_EQ_ARRAY(Span({0, 3, 6, 9, 12}).data(), dst_curves.offsets().data(), 5);
EXPECT_EQ_ARRAY(Span({false, true, true, false}).data(), cyclic.data(), 3);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 5, 4, 3, 6, 7, 8, 11, 10, 9}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, ConnectAndReverseCurves)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve = {3, 0, -1, -1};
Array<bool> flip_direction = {true, false, true, false};
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 2);
EXPECT_EQ_ARRAY(Span({0, 9, 12}).data(), dst_curves.offsets().data(), 3);
EXPECT_EQ_ARRAY(Span({false, true}).data(), cyclic.data(), 2);
EXPECT_EQ_ARRAY(Span({3, 4, 5, 2, 1, 0, 9, 10, 11, 8, 7, 6}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, CyclicConnection)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve = {-1, 3, -1, 1};
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 3);
EXPECT_EQ_ARRAY(Span({0, 3, 9, 12}).data(), dst_curves.offsets().data(), 4);
EXPECT_EQ_ARRAY(Span({false, true, true}).data(), cyclic.data(), 3);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 3, 4, 5, 9, 10, 11, 6, 7, 8}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, SelfConnectCurve)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, false, false, false});
Array<int> connect_to_curve = {-1, 1, 2, -1};
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 4);
EXPECT_EQ_ARRAY(Span({0, 3, 6, 9, 12}).data(), dst_curves.offsets().data(), 5);
EXPECT_EQ_ARRAY(Span({false, true, true, false}).data(), cyclic.data(), 4);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, MergeAll)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
Array<int> connect_to_curve = {2, 0, 3, 1};
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 1);
EXPECT_EQ_ARRAY(Span({0, 12}).data(), dst_curves.offsets().data(), 2);
EXPECT_EQ_ARRAY(Span({true}).data(), cyclic.data(), 1);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 6, 7, 8, 9, 10, 11, 3, 4, 5}).data(), dst_indices.data(), 12);
}
TEST(merge_curves, Branching)
{
bke::CurvesGeometry src_curves = create_test_curves({0, 3, 6, 9, 12},
{false, true, true, false});
/* Multiple curves connect to curve 2, one connection is ignored. */
Array<int> connect_to_curve = {2, 2, -1, -1};
Array<bool> flip_direction(4, false);
bke::CurvesGeometry dst_curves = geometry::curves_merge_endpoints(
src_curves, connect_to_curve, flip_direction, {});
const VArraySpan<bool> cyclic = dst_curves.cyclic();
const VArraySpan<int> dst_indices = *dst_curves.attributes().lookup<int>("test_index");
EXPECT_EQ(dst_curves.points_num(), 12);
EXPECT_EQ(dst_curves.curves_num(), 3);
EXPECT_EQ_ARRAY(Span({0, 6, 9, 12}).data(), dst_curves.offsets().data(), 4);
EXPECT_EQ_ARRAY(Span({false, false, false}).data(), cyclic.data(), 3);
EXPECT_EQ_ARRAY(Span({0, 1, 2, 6, 7, 8, 3, 4, 5, 9, 10, 11}).data(), dst_indices.data(), 12);
}
} // namespace blender::geometry::tests