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:
@@ -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 ®ion,
|
||||
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(
|
||||
®ion, start_world, screen_start_points[src_i], V3D_PROJ_TEST_NOP);
|
||||
ED_view3d_project_float_global(
|
||||
®ion, 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,
|
||||
|
||||
@@ -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 ®ion,
|
||||
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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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()
|
||||
|
||||
24
source/blender/geometry/GEO_merge_curves.hh
Normal file
24
source/blender/geometry/GEO_merge_curves.hh
Normal 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
|
||||
308
source/blender/geometry/intern/merge_curves.cc
Normal file
308
source/blender/geometry/intern/merge_curves.cc
Normal 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
|
||||
195
source/blender/geometry/tests/GEO_merge_curves_test.cc
Normal file
195
source/blender/geometry/tests/GEO_merge_curves_test.cc
Normal 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
|
||||
Reference in New Issue
Block a user