Fix: Grease Pencil interpolation on cyclic curves shifts end point
Fix for unreported issue with Grease Pencil interpolation tool: on cyclic curves the last point is interpolated between the end points of the curve, especially noticeable with sequence interpolation. This required handling a corner case in the curve sample mapping function. This function is complex and hard to verify with the operator alone, leading to frequent issues and discovery of yet more corner cases. For this reason i refactored the sampling function and added new unit tests. This should help avoid regressions and make it clear how the function is expected to behave in various corner cases. The `sample_curve_padded` function has been moved into the geometry module, since the `sculpt_paint` module does not have tests yet and is intended mostly for higher-level operator code. The function has been split to separate out the "reverse" sampling mode, which reduces complexity. Reverse sampling is done by first reversing the input curve points, doing regular sampling, and then reversing the resulting samples. The function can now sample to larger or smaller sample arrays: - Larger output arrays have a point aligned with each source point as before, with the rest of the points evenly distributed over the source curve. This ensures that the output curve matches the source as closely as possible, especially for poly curves. - Smaller output arrays are uniformly sampled along the length of the source curve. Pull Request: https://projects.blender.org/blender/blender/pulls/141946
This commit is contained in:
@@ -459,116 +459,6 @@ static bool compute_auto_flip(const Span<float3> from_positions, const Span<floa
|
||||
return math::dot(from_last - from_first, to_last - to_first) < 0.0f;
|
||||
}
|
||||
|
||||
static void assign_samples_to_segments(const int num_dst_points,
|
||||
const Span<float3> src_positions,
|
||||
const bool cyclic,
|
||||
MutableSpan<int> dst_sample_offsets)
|
||||
{
|
||||
const IndexRange src_points = src_positions.index_range();
|
||||
/* Extra segment at the end for cyclic curves. */
|
||||
const int num_src_segments = src_points.size() - 1 + cyclic;
|
||||
/* Extra points of the destination curve that need to be distributed on source segments. */
|
||||
const int num_free_samples = num_dst_points - num_src_segments - 1;
|
||||
BLI_assert(dst_sample_offsets.size() == num_src_segments + 1);
|
||||
|
||||
Array<float> segment_lengths(num_src_segments + 1);
|
||||
segment_lengths[0] = 0.0f;
|
||||
for (const int i : src_points.drop_front(1)) {
|
||||
segment_lengths[i] = segment_lengths[i - 1] + math::distance(src_positions[src_points[i - 1]],
|
||||
src_positions[src_points[i]]);
|
||||
}
|
||||
if (cyclic) {
|
||||
const int i = src_points.size();
|
||||
segment_lengths[i] = segment_lengths[i - 1] + math::distance(src_positions[src_points[i - 1]],
|
||||
src_positions[src_points[0]]);
|
||||
}
|
||||
const float total_length = segment_lengths.last();
|
||||
|
||||
constexpr float length_epsilon = 1e-4f;
|
||||
if (total_length > length_epsilon) {
|
||||
/* Factor for computing the fraction of remaining samples in a segment. */
|
||||
const float length_to_free_sample_count = math::safe_divide(float(num_free_samples),
|
||||
total_length);
|
||||
int samples_start = 0;
|
||||
for (const int segment : IndexRange(num_src_segments)) {
|
||||
const int free_samples_start = math::round(segment_lengths[segment] *
|
||||
length_to_free_sample_count);
|
||||
const int free_samples_end = math::round(segment_lengths[segment + 1] *
|
||||
length_to_free_sample_count);
|
||||
dst_sample_offsets[segment] = samples_start;
|
||||
samples_start += 1 + free_samples_end - free_samples_start;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* If source segment lengths are zero use uniform mapping by index as a fallback. */
|
||||
const float index_to_free_sample_count = math::safe_divide(float(num_free_samples),
|
||||
float(num_src_segments));
|
||||
int samples_start = 0;
|
||||
for (const int segment : IndexRange(num_src_segments)) {
|
||||
dst_sample_offsets[segment] = samples_start;
|
||||
const int free_samples_start = math::round(segment * index_to_free_sample_count);
|
||||
const int free_samples_end = math::round((segment + 1) * index_to_free_sample_count);
|
||||
samples_start += 1 + free_samples_end - free_samples_start;
|
||||
}
|
||||
}
|
||||
/* This also assigns any remaining samples in case of rounding error. */
|
||||
dst_sample_offsets.last() = num_dst_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy existing sample positions and insert new samples in between to reach the final count.
|
||||
*/
|
||||
static void sample_curve_padded(const bke::CurvesGeometry &curves,
|
||||
const int curve_index,
|
||||
const bool cyclic,
|
||||
const bool reverse,
|
||||
MutableSpan<int> r_segment_indices,
|
||||
MutableSpan<float> r_factors)
|
||||
{
|
||||
const int num_dst_points = r_segment_indices.size();
|
||||
const IndexRange src_points = curves.points_by_curve()[curve_index];
|
||||
if (src_points.is_empty()) {
|
||||
return;
|
||||
}
|
||||
if (src_points.size() == 1) {
|
||||
r_segment_indices.fill(0);
|
||||
r_factors.fill(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Extra segment at the end for cyclic curves. */
|
||||
const int num_src_segments = src_points.size() - 1 + cyclic;
|
||||
/* There should be at least one source point for every output sample. */
|
||||
BLI_assert(num_dst_points >= num_src_segments);
|
||||
|
||||
/* First destination point in each source segment. */
|
||||
Array<int> dst_sample_offsets(num_src_segments + 1);
|
||||
assign_samples_to_segments(
|
||||
num_dst_points, curves.positions().slice(src_points), cyclic, dst_sample_offsets);
|
||||
|
||||
OffsetIndices dst_samples_by_src_segment = OffsetIndices<int>(dst_sample_offsets);
|
||||
for (const int segment : IndexRange(num_src_segments)) {
|
||||
const IndexRange samples = dst_samples_by_src_segment[segment];
|
||||
BLI_assert(samples.size() >= 1);
|
||||
|
||||
const int point_index = segment < src_points.size() ? segment : 0;
|
||||
r_segment_indices.slice(samples).fill(point_index);
|
||||
for (const int sample_i : samples.index_range()) {
|
||||
const int sample = reverse ? samples[samples.size() - 1 - sample_i] : samples[sample_i];
|
||||
const float factor = float(sample_i) / samples.size();
|
||||
r_factors[sample] = reverse ? 1.0f - factor : factor;
|
||||
}
|
||||
}
|
||||
if (cyclic) {
|
||||
r_segment_indices.last() = 0;
|
||||
r_factors.last() = 0.0f;
|
||||
}
|
||||
else {
|
||||
r_segment_indices.last() = src_points.size() - 1;
|
||||
r_factors.last() = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static bke::CurvesGeometry interpolate_between_curves(const GreasePencil &grease_pencil,
|
||||
const bke::greasepencil::Layer &layer,
|
||||
const InterpolationPairs &curve_pairs,
|
||||
@@ -748,22 +638,22 @@ static bke::CurvesGeometry interpolate_between_curves(const GreasePencil &grease
|
||||
BLI_assert(from_points.size() == dst_points.size());
|
||||
array_utils::fill_index_range(from_sample_indices.as_mutable_span().slice(dst_points));
|
||||
from_sample_factors.as_mutable_span().slice(dst_points).fill(0.0f);
|
||||
sample_curve_padded(to_drawing->strokes(),
|
||||
to_curve,
|
||||
to_curves_cyclic[to_curve],
|
||||
dst_curve_flip[pair_index],
|
||||
to_sample_indices.as_mutable_span().slice(dst_points),
|
||||
to_sample_factors.as_mutable_span().slice(dst_points));
|
||||
geometry::sample_curve_padded(to_drawing->strokes(),
|
||||
to_curve,
|
||||
to_curves_cyclic[to_curve],
|
||||
dst_curve_flip[pair_index],
|
||||
to_sample_indices.as_mutable_span().slice(dst_points),
|
||||
to_sample_factors.as_mutable_span().slice(dst_points));
|
||||
}
|
||||
else {
|
||||
/* Target curve samples match 'to' points. */
|
||||
BLI_assert(to_points.size() == dst_points.size());
|
||||
sample_curve_padded(from_drawing->strokes(),
|
||||
from_curve,
|
||||
from_curves_cyclic[from_curve],
|
||||
dst_curve_flip[pair_index],
|
||||
from_sample_indices.as_mutable_span().slice(dst_points),
|
||||
from_sample_factors.as_mutable_span().slice(dst_points));
|
||||
geometry::sample_curve_padded(from_drawing->strokes(),
|
||||
from_curve,
|
||||
from_curves_cyclic[from_curve],
|
||||
dst_curve_flip[pair_index],
|
||||
from_sample_indices.as_mutable_span().slice(dst_points),
|
||||
from_sample_factors.as_mutable_span().slice(dst_points));
|
||||
array_utils::fill_index_range(to_sample_indices.as_mutable_span().slice(dst_points));
|
||||
to_sample_factors.fill(0.0f);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ if(WITH_GTESTS)
|
||||
)
|
||||
set(TEST_SRC
|
||||
tests/GEO_merge_curves_test.cc
|
||||
tests/GEO_interpolate_curves_test.cc
|
||||
)
|
||||
set(TEST_LIB
|
||||
)
|
||||
|
||||
@@ -8,6 +8,36 @@
|
||||
|
||||
namespace blender::geometry {
|
||||
|
||||
/**
|
||||
* Assign source point indices and interpolation factors to target points.
|
||||
*
|
||||
* \param positions Source curve positions.
|
||||
* \param cyclic True if the source curve is cyclic.
|
||||
* \param r_indices Output array of point indices of the source curve.
|
||||
* \param r_factors Output array of interpolation factors between a source point and the next.
|
||||
*/
|
||||
void sample_curve_padded(const Span<float3> positions,
|
||||
bool cyclic,
|
||||
MutableSpan<int> r_indices,
|
||||
MutableSpan<float> r_factors);
|
||||
|
||||
/**
|
||||
* Assign source point indices and interpolation factors to target points for a single curve.
|
||||
*
|
||||
* \param curves Source curves geometry to sample.
|
||||
* \param curve_index Index of the source curve to sample.
|
||||
* \param cyclic True if the source curve is cyclic.
|
||||
* \param reverse True if the curve should be sampled in reverse direction.
|
||||
* \param r_indices Output array of point indices of the source curve.
|
||||
* \param r_factors Output array of interpolation factors between a source point and the next.
|
||||
*/
|
||||
void sample_curve_padded(const bke::CurvesGeometry &curves,
|
||||
int curve_index,
|
||||
bool cyclic,
|
||||
bool reverse,
|
||||
MutableSpan<int> r_indices,
|
||||
MutableSpan<float> r_factors);
|
||||
|
||||
/**
|
||||
* Create new curves that are interpolated between "from" and "to" curves.
|
||||
* \param dst_curve_mask: Set of curves in \a dst_curves that are being filled.
|
||||
|
||||
@@ -24,6 +24,211 @@ namespace blender::geometry {
|
||||
|
||||
using bke::CurvesGeometry;
|
||||
|
||||
/* Returns a map that places each point in the sample index space.
|
||||
* The map has one additional point at the end to simplify cyclic curve mapping. */
|
||||
static Array<float> build_point_to_sample_map(const Span<float3> positions,
|
||||
const bool cyclic,
|
||||
const int samples_num)
|
||||
{
|
||||
const IndexRange points = positions.index_range();
|
||||
Array<float> sample_by_point(points.size() + 1);
|
||||
sample_by_point[0] = 0.0f;
|
||||
for (const int i : points.drop_front(1)) {
|
||||
sample_by_point[i] = sample_by_point[i - 1] + math::distance(positions[i - 1], positions[i]);
|
||||
}
|
||||
sample_by_point.last() = cyclic ? sample_by_point[points.size() - 1] +
|
||||
math::distance(positions.last(), positions.first()) :
|
||||
sample_by_point[points.size() - 1];
|
||||
|
||||
/* If source segment lengths are zero use uniform mapping by index as a fallback. */
|
||||
constexpr float length_epsilon = 1e-4f;
|
||||
if (sample_by_point.last() <= length_epsilon) {
|
||||
array_utils::fill_index_range(sample_by_point.as_mutable_span());
|
||||
}
|
||||
|
||||
const float total_length = sample_by_point.last();
|
||||
/* Factor for mapping segment length to sample index space. */
|
||||
const float length_to_sample_count = math::safe_divide(float(samples_num), total_length);
|
||||
for (float &sample_value : sample_by_point) {
|
||||
sample_value *= length_to_sample_count;
|
||||
}
|
||||
|
||||
return sample_by_point;
|
||||
}
|
||||
|
||||
static void assign_samples_to_segments(const int num_dst_points,
|
||||
const Span<float3> src_positions,
|
||||
const bool cyclic,
|
||||
MutableSpan<int> dst_sample_offsets)
|
||||
{
|
||||
const IndexRange src_points = src_positions.index_range();
|
||||
BLI_assert(src_points.size() > 0);
|
||||
BLI_assert(num_dst_points > 0);
|
||||
BLI_assert(num_dst_points >= src_points.size());
|
||||
BLI_assert(dst_sample_offsets.size() == src_points.size() + 1);
|
||||
|
||||
/* Extra points of the destination curve that need to be distributed on source segments. */
|
||||
const int num_free_samples = num_dst_points - int(src_points.size());
|
||||
const Array<float> sample_by_point = build_point_to_sample_map(
|
||||
src_positions, cyclic, num_free_samples);
|
||||
|
||||
int samples_start = 0;
|
||||
for (const int src_point_i : src_points) {
|
||||
dst_sample_offsets[src_point_i] = samples_start;
|
||||
|
||||
/* Use rounding to distribute samples equally over all segments. */
|
||||
const int free_samples = math::round(sample_by_point[src_point_i + 1]) -
|
||||
math::round(sample_by_point[src_point_i]);
|
||||
samples_start += 1 + free_samples;
|
||||
}
|
||||
|
||||
/* This also assigns any remaining samples in case of rounding error. */
|
||||
dst_sample_offsets.last() = num_dst_points;
|
||||
}
|
||||
|
||||
void sample_curve_padded(const Span<float3> positions,
|
||||
const bool cyclic,
|
||||
MutableSpan<int> r_indices,
|
||||
MutableSpan<float> r_factors)
|
||||
{
|
||||
const int num_dst_points = r_indices.size();
|
||||
BLI_assert(r_factors.size() == num_dst_points);
|
||||
const IndexRange src_points = positions.index_range();
|
||||
|
||||
if (num_dst_points == 0) {
|
||||
return;
|
||||
}
|
||||
if (num_dst_points == 1) {
|
||||
r_indices.first() = 0;
|
||||
r_factors.first() = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (src_points.is_empty()) {
|
||||
return;
|
||||
}
|
||||
if (src_points.size() == 1) {
|
||||
r_indices.fill(0);
|
||||
r_factors.fill(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the destination curve has equal or more points then the excess samples are distributed
|
||||
* equally over all the segments.
|
||||
* If the destination curve is shorter the samples are placed equidistant along the source
|
||||
* segments. */
|
||||
if (num_dst_points >= src_points.size()) {
|
||||
/* First destination point in each source segment. */
|
||||
Array<int> dst_sample_offsets(src_points.size() + 1);
|
||||
assign_samples_to_segments(num_dst_points, positions, cyclic, dst_sample_offsets);
|
||||
|
||||
OffsetIndices dst_samples_by_src_point = OffsetIndices<int>(dst_sample_offsets);
|
||||
for (const int src_point_i : src_points.index_range()) {
|
||||
const IndexRange samples = dst_samples_by_src_point[src_point_i];
|
||||
|
||||
r_indices.slice(samples).fill(src_point_i);
|
||||
for (const int sample_i : samples.index_range()) {
|
||||
const int sample = samples[sample_i];
|
||||
const float factor = float(sample_i) / samples.size();
|
||||
r_factors[sample] = factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const Array<float> sample_by_point = build_point_to_sample_map(
|
||||
positions, cyclic, num_dst_points - (cyclic ? 0 : 1));
|
||||
|
||||
for (const int src_point_i : src_points.index_range()) {
|
||||
const float sample_start = sample_by_point[src_point_i];
|
||||
const float sample_end = sample_by_point[src_point_i + 1];
|
||||
const IndexRange samples = IndexRange::from_begin_end(math::ceil(sample_start),
|
||||
math::ceil(sample_end));
|
||||
|
||||
for (const int sample : samples) {
|
||||
r_indices[sample] = src_point_i;
|
||||
r_factors[sample] = math::safe_divide(float(sample) - sample_start,
|
||||
sample_end - sample_start);
|
||||
}
|
||||
}
|
||||
if (!cyclic) {
|
||||
r_indices.last() = src_points.size() - 1;
|
||||
r_factors.last() = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reverse_samples(const int points_num,
|
||||
MutableSpan<int> r_indices,
|
||||
MutableSpan<float> r_factors)
|
||||
{
|
||||
Vector<int> reverse_indices;
|
||||
Vector<float> reverse_factors;
|
||||
reverse_indices.reserve(r_indices.size());
|
||||
reverse_factors.reserve(r_factors.size());
|
||||
/* Indices in the last (cyclic) segment are also in the last segment when reversed. */
|
||||
for (const int i : r_indices.index_range()) {
|
||||
const int index = r_indices[i];
|
||||
const float factor = r_factors[i];
|
||||
const bool is_last_segment = index >= points_num - 1;
|
||||
|
||||
if (is_last_segment && factor > 0.0f) {
|
||||
reverse_indices.append(points_num - 1);
|
||||
reverse_factors.append(1.0f - factor);
|
||||
}
|
||||
}
|
||||
/* Insert reversed indices except the last (cyclic) segment. */
|
||||
for (const int i : r_indices.index_range()) {
|
||||
const int index = r_indices[i];
|
||||
const float factor = r_factors[i];
|
||||
const bool is_last_segment = index >= points_num - 1;
|
||||
|
||||
if (factor > 0.0f) {
|
||||
/* Skip the last (cyclic) segment, handled below. */
|
||||
if (is_last_segment) {
|
||||
continue;
|
||||
}
|
||||
reverse_indices.append(points_num - 2 - index);
|
||||
reverse_factors.append(1.0f - r_factors[i]);
|
||||
}
|
||||
else {
|
||||
/* Move factor 1.0 into the next segment. */
|
||||
reverse_indices.append(points_num - 1 - index);
|
||||
reverse_factors.append(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
r_indices.copy_from(reverse_indices);
|
||||
r_factors.copy_from(reverse_factors);
|
||||
}
|
||||
|
||||
void sample_curve_padded(const bke::CurvesGeometry &curves,
|
||||
const int curve_index,
|
||||
const bool cyclic,
|
||||
const bool reverse,
|
||||
MutableSpan<int> r_indices,
|
||||
MutableSpan<float> r_factors)
|
||||
{
|
||||
BLI_assert(curves.curves_range().contains(curve_index));
|
||||
BLI_assert(r_indices.size() == r_factors.size());
|
||||
const IndexRange points = curves.points_by_curve()[curve_index];
|
||||
const Span<float3> positions = curves.positions().slice(points);
|
||||
|
||||
if (reverse) {
|
||||
const int points_num = positions.size();
|
||||
Array<float3> reverse_positions(points_num);
|
||||
for (const int i : reverse_positions.index_range()) {
|
||||
reverse_positions[i] = positions[points_num - 1 - i];
|
||||
}
|
||||
|
||||
sample_curve_padded(reverse_positions, cyclic, r_indices, r_factors);
|
||||
|
||||
reverse_samples(points_num, r_indices, r_factors);
|
||||
}
|
||||
else {
|
||||
sample_curve_padded(positions, cyclic, r_indices, r_factors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
292
source/blender/geometry/tests/GEO_interpolate_curves_test.cc
Normal file
292
source/blender/geometry/tests/GEO_interpolate_curves_test.cc
Normal file
@@ -0,0 +1,292 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BLI_array_utils.hh"
|
||||
|
||||
#include "BKE_curves.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
|
||||
#include "GEO_interpolate_curves.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::bke::tests {
|
||||
|
||||
class GreasePencilInterpolate : public testing::Test {
|
||||
public:
|
||||
enum class TestCurveShape {
|
||||
Zero,
|
||||
Circle,
|
||||
Eight,
|
||||
Helix,
|
||||
};
|
||||
|
||||
static void create_test_shape(const TestCurveShape shape, MutableSpan<float3> positions)
|
||||
{
|
||||
switch (shape) {
|
||||
case TestCurveShape::Zero:
|
||||
positions.fill(float3(0.0f));
|
||||
break;
|
||||
case TestCurveShape::Circle:
|
||||
for (const int point_i : positions.index_range()) {
|
||||
const float angle = 2.0f * M_PI * float(point_i) / float(positions.size());
|
||||
positions[point_i] = float3(math::cos(angle), math::sin(angle), 0.0f);
|
||||
}
|
||||
break;
|
||||
case TestCurveShape::Eight:
|
||||
for (const int point_i : positions.index_range()) {
|
||||
const float angle = 2.0f * M_PI * float(point_i) / float(positions.size());
|
||||
positions[point_i] = float3(math::cos(angle), math::sin(angle * 2.0f), 0.0f);
|
||||
}
|
||||
break;
|
||||
case TestCurveShape::Helix:
|
||||
const int turns = 3;
|
||||
const float pitch = 0.3f;
|
||||
for (const int point_i : positions.index_range()) {
|
||||
const float factor = float(turns) * float(point_i) / float(positions.size() - 1);
|
||||
const float angle = 2.0f * M_PI * factor;
|
||||
const float height = pitch * factor;
|
||||
positions[point_i] = float3(math::cos(angle), math::sin(angle), height);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bke::CurvesGeometry create_test_curves(Span<int> offsets,
|
||||
Span<bool> cyclic,
|
||||
TestCurveShape shape)
|
||||
{
|
||||
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);
|
||||
|
||||
MutableSpan<float3> positions = curves.positions_for_write();
|
||||
for (const int curve_i : curves.curves_range()) {
|
||||
const IndexRange points = curves.points_by_curve()[curve_i];
|
||||
create_test_shape(shape, positions.slice(points));
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
void test_sample_curve(const bke::CurvesGeometry &curves,
|
||||
const int curve_index,
|
||||
const bool reverse,
|
||||
const Span<int> expected_indices,
|
||||
const Span<float> expected_factors,
|
||||
const float threshold = 1e-4f)
|
||||
{
|
||||
const int num_dst_points = expected_indices.size();
|
||||
BLI_assert(expected_factors.size() == num_dst_points);
|
||||
|
||||
const bool cyclic = curves.cyclic()[curve_index];
|
||||
|
||||
Array<int> indices(num_dst_points, -9999);
|
||||
Array<float> factors(num_dst_points, -12345.6f);
|
||||
geometry::sample_curve_padded(curves, curve_index, cyclic, reverse, indices, factors);
|
||||
|
||||
EXPECT_EQ_SPAN(expected_indices, indices.as_span());
|
||||
|
||||
EXPECT_EQ(expected_factors.size(), factors.size());
|
||||
if (expected_factors.size() == factors.size()) {
|
||||
for (const int i : expected_factors.index_range()) {
|
||||
EXPECT_NEAR(expected_factors[i], factors[i], threshold)
|
||||
<< "Element mismatch at index " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GreasePencilInterpolate, sample_curve_empty_output)
|
||||
{
|
||||
bke::CurvesGeometry curves = create_test_curves(
|
||||
{0, 1, 3}, {false, false}, TestCurveShape::Eight);
|
||||
|
||||
test_sample_curve(curves, 0, false, {}, {});
|
||||
test_sample_curve(curves, 1, false, {}, {});
|
||||
}
|
||||
|
||||
TEST_F(GreasePencilInterpolate, sample_curve_same_length)
|
||||
{
|
||||
bke::CurvesGeometry curves = create_test_curves(
|
||||
{0, 1, 3, 13, 14, 16, 26}, {false, false, false, true, true, true}, TestCurveShape::Eight);
|
||||
|
||||
test_sample_curve(curves, 0, false, {0}, {0.0f});
|
||||
test_sample_curve(curves, 0, true, {0}, {0.0f});
|
||||
|
||||
test_sample_curve(curves, 1, false, {0, 1}, {0.0f, 0.0f});
|
||||
test_sample_curve(curves, 1, true, {1, 0}, {0.0f, 0.0f});
|
||||
|
||||
test_sample_curve(curves,
|
||||
2,
|
||||
false,
|
||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
test_sample_curve(curves,
|
||||
2,
|
||||
true,
|
||||
{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
|
||||
test_sample_curve(curves, 3, false, {0}, {0.0f});
|
||||
test_sample_curve(curves, 3, true, {0}, {0.0f});
|
||||
|
||||
test_sample_curve(curves, 4, false, {0, 1}, {0.0f, 0.0f});
|
||||
test_sample_curve(curves, 4, true, {1, 0}, {0.0f, 0.0f});
|
||||
|
||||
test_sample_curve(curves,
|
||||
5,
|
||||
false,
|
||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
test_sample_curve(curves,
|
||||
5,
|
||||
true,
|
||||
{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
}
|
||||
|
||||
TEST_F(GreasePencilInterpolate, sample_curve_shorter)
|
||||
{
|
||||
bke::CurvesGeometry curves = create_test_curves(
|
||||
{0, 1, 3, 13, 14, 16, 26}, {false, false, false, true, true, true}, TestCurveShape::Eight);
|
||||
|
||||
test_sample_curve(curves, 1, false, {0}, {0.0f});
|
||||
test_sample_curve(curves, 1, true, {1}, {0.0f});
|
||||
|
||||
test_sample_curve(curves, 2, false, {0, 2, 5, 9}, {0.0f, 0.82178f, 0.88113f, 0.0f});
|
||||
test_sample_curve(curves, 2, true, {9, 5, 2, 0}, {0.0f, 0.88113f, 0.82178f, 0.0f});
|
||||
|
||||
test_sample_curve(curves, 4, false, {0}, {0.0f});
|
||||
test_sample_curve(curves, 4, true, {1}, {0.0f});
|
||||
|
||||
test_sample_curve(curves, 5, false, {0, 2, 5, 7}, {0.0f, 0.5f, 0.0f, 0.5f});
|
||||
test_sample_curve(curves, 5, true, {9, 6, 4, 1}, {0.0f, 0.50492f, 0.0f, 0.50492f});
|
||||
}
|
||||
|
||||
TEST_F(GreasePencilInterpolate, sample_curve_longer)
|
||||
{
|
||||
bke::CurvesGeometry curves = create_test_curves(
|
||||
{0, 1, 3, 13, 14, 16, 26}, {false, false, false, true, true, true}, TestCurveShape::Eight);
|
||||
|
||||
test_sample_curve(curves,
|
||||
1,
|
||||
false,
|
||||
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||
{0.0f,
|
||||
0.09091f,
|
||||
0.18182f,
|
||||
0.27273f,
|
||||
0.36364f,
|
||||
0.45455f,
|
||||
0.54545f,
|
||||
0.63636f,
|
||||
0.72727f,
|
||||
0.81818f,
|
||||
0.90909f,
|
||||
0.0f});
|
||||
test_sample_curve(curves,
|
||||
1,
|
||||
true,
|
||||
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
{0.0f,
|
||||
0.90909f,
|
||||
0.81818f,
|
||||
0.72727f,
|
||||
0.63636f,
|
||||
0.54545f,
|
||||
0.45455f,
|
||||
0.36364f,
|
||||
0.27273f,
|
||||
0.18182f,
|
||||
0.09091f,
|
||||
0.0f});
|
||||
|
||||
test_sample_curve(curves,
|
||||
2,
|
||||
false,
|
||||
{0, 1, 2, 2, 3, 4, 5, 6, 6, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f});
|
||||
test_sample_curve(curves,
|
||||
2,
|
||||
true,
|
||||
{9, 8, 7, 6, 6, 5, 4, 3, 2, 2, 1, 0},
|
||||
{0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f});
|
||||
|
||||
test_sample_curve(curves,
|
||||
4,
|
||||
false,
|
||||
{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1},
|
||||
{0.0f,
|
||||
0.16667f,
|
||||
0.33333f,
|
||||
0.5f,
|
||||
0.66667f,
|
||||
0.83333f,
|
||||
0.0f,
|
||||
0.16667f,
|
||||
0.33333f,
|
||||
0.5f,
|
||||
0.66667f,
|
||||
0.83333f});
|
||||
test_sample_curve(curves,
|
||||
4,
|
||||
true,
|
||||
{1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},
|
||||
{0.83333f,
|
||||
0.66667f,
|
||||
0.5f,
|
||||
0.33333f,
|
||||
0.16667f,
|
||||
0.0f,
|
||||
0.83333f,
|
||||
0.66667f,
|
||||
0.5f,
|
||||
0.33333f,
|
||||
0.16667f,
|
||||
0.0f});
|
||||
|
||||
test_sample_curve(curves,
|
||||
5,
|
||||
false,
|
||||
{0, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f});
|
||||
test_sample_curve(curves,
|
||||
5,
|
||||
true,
|
||||
{9, 8, 7, 6, 6, 5, 4, 3, 2, 1, 1, 0},
|
||||
{0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f});
|
||||
}
|
||||
|
||||
TEST_F(GreasePencilInterpolate, sample_zero_length_curve)
|
||||
{
|
||||
bke::CurvesGeometry curves = create_test_curves(
|
||||
{0, 10, 20}, {false, true}, TestCurveShape::Zero);
|
||||
|
||||
test_sample_curve(curves,
|
||||
0,
|
||||
false,
|
||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
test_sample_curve(curves,
|
||||
1,
|
||||
false,
|
||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||
}
|
||||
|
||||
} // namespace blender::bke::tests
|
||||
Reference in New Issue
Block a user