Blender V4.3
curves_geometry_test.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
9#include "BKE_curves.hh"
10
11#include "testing/testing.h"
12
13namespace blender::bke::tests {
14
15static CurvesGeometry create_basic_curves(const int points_size, const int curves_size)
16{
17 CurvesGeometry curves(points_size, curves_size);
18
19 const int curve_length = points_size / curves_size;
20 for (const int i : curves.curves_range()) {
21 curves.offsets_for_write()[i] = curve_length * i;
22 }
23 curves.offsets_for_write().last() = points_size;
24
25 for (const int i : curves.points_range()) {
26 curves.positions_for_write()[i] = {float(i), float(i % curve_length), 0.0f};
27 }
28
29 return curves;
30}
31
32TEST(curves_geometry, Empty)
33{
34 CurvesGeometry empty(0, 0);
35 empty.cyclic();
36 EXPECT_FALSE(empty.bounds_min_max());
37}
38
39TEST(curves_geometry, Move)
40{
41 CurvesGeometry curves = create_basic_curves(100, 10);
42
43 const int *offsets_data = curves.offsets().data();
44 const float3 *positions_data = curves.positions().data();
45
46 CurvesGeometry other = std::move(curves);
47
48 /* The old curves should be empty, and the offsets are expected to be null. */
49 EXPECT_EQ(curves.points_num(), 0); /* NOLINT: bugprone-use-after-move */
50 EXPECT_EQ(curves.curve_offsets, nullptr); /* NOLINT: bugprone-use-after-move */
51
52 /* Just a basic check that the new curves work okay. */
53 EXPECT_TRUE(other.bounds_min_max());
54
55 curves = std::move(other);
56
57 CurvesGeometry second_other(std::move(curves));
58
59 /* The data should not have been reallocated ever. */
60 EXPECT_EQ(second_other.positions().data(), positions_data);
61 EXPECT_EQ(second_other.offsets().data(), offsets_data);
62}
63
64TEST(curves_geometry, TypeCount)
65{
66 CurvesGeometry curves = create_basic_curves(100, 10);
67 curves.curve_types_for_write().copy_from({
78 });
79 curves.update_curve_types();
80 const std::array<int, CURVE_TYPES_NUM> &counts = curves.curve_type_counts();
82 EXPECT_EQ(counts[CURVE_TYPE_POLY], 3);
83 EXPECT_EQ(counts[CURVE_TYPE_BEZIER], 1);
84 EXPECT_EQ(counts[CURVE_TYPE_NURBS], 3);
85}
86
87TEST(curves_geometry, CatmullRomEvaluation)
88{
89 CurvesGeometry curves(4, 1);
90 curves.fill_curve_types(CURVE_TYPE_CATMULL_ROM);
91 curves.resolution_for_write().fill(12);
92 curves.offsets_for_write().last() = 4;
93 curves.cyclic_for_write().fill(false);
94
95 MutableSpan<float3> positions = curves.positions_for_write();
96 positions[0] = {1, 1, 0};
97 positions[1] = {0, 1, 0};
98 positions[2] = {0, 0, 0};
99 positions[3] = {-1, 0, 0};
100
101 Span<float3> evaluated_positions = curves.evaluated_positions();
102 static const Array<float3> result_1{{
103 {1, 1, 0},
104 {0.948495, 1.00318, 0},
105 {0.87963, 1.01157, 0},
106 {0.796875, 1.02344, 0},
107 {0.703704, 1.03704, 0},
108 {0.603588, 1.05064, 0},
109 {0.5, 1.0625, 0},
110 {0.396412, 1.07089, 0},
111 {0.296296, 1.07407, 0},
112 {0.203125, 1.07031, 0},
113 {0.12037, 1.05787, 0},
114 {0.0515046, 1.03501, 0},
115 {0, 1, 0},
116 {-0.0318287, 0.948495, 0},
117 {-0.0462963, 0.87963, 0},
118 {-0.046875, 0.796875, 0},
119 {-0.037037, 0.703704, 0},
120 {-0.0202546, 0.603588, 0},
121 {0, 0.5, 0},
122 {0.0202546, 0.396412, 0},
123 {0.037037, 0.296296, 0},
124 {0.046875, 0.203125, 0},
125 {0.0462963, 0.12037, 0},
126 {0.0318287, 0.0515046, 0},
127 {0, 0, 0},
128 {-0.0515046, -0.0350116, 0},
129 {-0.12037, -0.0578704, 0},
130 {-0.203125, -0.0703125, 0},
131 {-0.296296, -0.0740741, 0},
132 {-0.396412, -0.0708912, 0},
133 {-0.5, -0.0625, 0},
134 {-0.603588, -0.0506366, 0},
135 {-0.703704, -0.037037, 0},
136 {-0.796875, -0.0234375, 0},
137 {-0.87963, -0.0115741, 0},
138 {-0.948495, -0.00318287, 0},
139 {-1, 0, 0},
140 }};
141 for (const int i : evaluated_positions.index_range()) {
142 EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f);
143 }
144
145 /* Changing the positions shouldn't cause the evaluated positions array to be reallocated. */
146 curves.tag_positions_changed();
147 curves.evaluated_positions();
148 EXPECT_EQ(curves.evaluated_positions().data(), evaluated_positions.data());
149
150 /* Call recalculation (which shouldn't happen because low-level accessors don't tag caches). */
151 EXPECT_EQ(evaluated_positions[12].x, 0.0f);
152 EXPECT_EQ(evaluated_positions[12].y, 1.0f);
153
154 positions[0] = {1, 0, 0};
155 positions[1] = {1, 1, 0};
156 positions[2] = {0, 1, 0};
157 positions[3] = {0, 0, 0};
158 curves.cyclic_for_write().fill(true);
159
160 /* Tag topology changed because the new cyclic value is different. */
161 curves.tag_topology_changed();
162
163 /* Retrieve the data again since the size should be larger than last time (one more segment). */
164 evaluated_positions = curves.evaluated_positions();
165 static const Array<float3> result_2{{
166 {1, 0, 0},
167 {1.03819, 0.0515046, 0},
168 {1.06944, 0.12037, 0},
169 {1.09375, 0.203125, 0},
170 {1.11111, 0.296296, 0},
171 {1.12153, 0.396412, 0},
172 {1.125, 0.5, 0},
173 {1.12153, 0.603588, 0},
174 {1.11111, 0.703704, 0},
175 {1.09375, 0.796875, 0},
176 {1.06944, 0.87963, 0},
177 {1.03819, 0.948495, 0},
178 {1, 1, 0},
179 {0.948495, 1.03819, 0},
180 {0.87963, 1.06944, 0},
181 {0.796875, 1.09375, 0},
182 {0.703704, 1.11111, 0},
183 {0.603588, 1.12153, 0},
184 {0.5, 1.125, 0},
185 {0.396412, 1.12153, 0},
186 {0.296296, 1.11111, 0},
187 {0.203125, 1.09375, 0},
188 {0.12037, 1.06944, 0},
189 {0.0515046, 1.03819, 0},
190 {0, 1, 0},
191 {-0.0381944, 0.948495, 0},
192 {-0.0694444, 0.87963, 0},
193 {-0.09375, 0.796875, 0},
194 {-0.111111, 0.703704, 0},
195 {-0.121528, 0.603588, 0},
196 {-0.125, 0.5, 0},
197 {-0.121528, 0.396412, 0},
198 {-0.111111, 0.296296, 0},
199 {-0.09375, 0.203125, 0},
200 {-0.0694444, 0.12037, 0},
201 {-0.0381944, 0.0515046, 0},
202 {0, 0, 0},
203 {0.0515046, -0.0381944, 0},
204 {0.12037, -0.0694444, 0},
205 {0.203125, -0.09375, 0},
206 {0.296296, -0.111111, 0},
207 {0.396412, -0.121528, 0},
208 {0.5, -0.125, 0},
209 {0.603588, -0.121528, 0},
210 {0.703704, -0.111111, 0},
211 {0.796875, -0.09375, 0},
212 {0.87963, -0.0694444, 0},
213 {0.948495, -0.0381944, 0},
214 }};
215 for (const int i : evaluated_positions.index_range()) {
216 EXPECT_V3_NEAR(evaluated_positions[i], result_2[i], 1e-5f);
217 }
218}
219
220TEST(curves_geometry, CatmullRomTwoPointCyclic)
221{
222 CurvesGeometry curves(2, 1);
223 curves.fill_curve_types(CURVE_TYPE_CATMULL_ROM);
224 curves.resolution_for_write().fill(12);
225 curves.offsets_for_write().last() = 2;
226 curves.cyclic_for_write().fill(true);
227
228 /* The curve should still be cyclic when there are only two control points. */
229 EXPECT_EQ(curves.evaluated_points_num(), 24);
230}
231
232TEST(curves_geometry, BezierPositionEvaluation)
233{
234 CurvesGeometry curves(2, 1);
235 curves.fill_curve_types(CURVE_TYPE_BEZIER);
236 curves.resolution_for_write().fill(12);
237 curves.offsets_for_write().last() = 2;
238
239 MutableSpan<float3> handles_left = curves.handle_positions_left_for_write();
240 MutableSpan<float3> handles_right = curves.handle_positions_right_for_write();
241 MutableSpan<float3> positions = curves.positions_for_write();
242 positions.first() = {-1, 0, 0};
243 positions.last() = {1, 0, 0};
244 handles_right.first() = {-0.5f, 0.5f, 0.0f};
245 handles_left.last() = {0, 0, 0};
246
247 /* Dangling handles shouldn't be used in a non-cyclic curve. */
248 handles_left.first() = {100, 100, 100};
249 handles_right.last() = {100, 100, 100};
250
251 Span<float3> evaluated_positions = curves.evaluated_positions();
252 static const Array<float3> result_1{{
253 {-1, 0, 0},
254 {-0.874711, 0.105035, 0},
255 {-0.747685, 0.173611, 0},
256 {-0.617188, 0.210937, 0},
257 {-0.481481, 0.222222, 0},
258 {-0.338831, 0.212674, 0},
259 {-0.1875, 0.1875, 0},
260 {-0.0257524, 0.15191, 0},
261 {0.148148, 0.111111, 0},
262 {0.335937, 0.0703125, 0},
263 {0.539352, 0.0347222, 0},
264 {0.760127, 0.00954859, 0},
265 {1, 0, 0},
266 }};
267 for (const int i : evaluated_positions.index_range()) {
268 EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f);
269 }
270
271 curves.resize(4, 2);
272 curves.fill_curve_types(CURVE_TYPE_BEZIER);
273 curves.resolution_for_write().fill(9);
274 curves.offsets_for_write().last() = 4;
275 handles_left = curves.handle_positions_left_for_write();
276 handles_right = curves.handle_positions_right_for_write();
277 positions = curves.positions_for_write();
278 positions[2] = {-1, 1, 0};
279 positions[3] = {1, 1, 0};
280 handles_right[2] = {-0.5f, 1.5f, 0.0f};
281 handles_left[3] = {0, 1, 0};
282
283 /* Dangling handles shouldn't be used in a non-cyclic curve. */
284 handles_left[2] = {-100, -100, -100};
285 handles_right[3] = {-100, -100, -100};
286
287 evaluated_positions = curves.evaluated_positions();
288 EXPECT_EQ(evaluated_positions.size(), 20);
289 static const Array<float3> result_2{{
290 {-1, 0, 0},
291 {-0.832647, 0.131687, 0},
292 {-0.66118, 0.201646, 0},
293 {-0.481481, 0.222222, 0},
294 {-0.289438, 0.205761, 0},
295 {-0.0809327, 0.164609, 0},
296 {0.148148, 0.111111, 0},
297 {0.40192, 0.0576133, 0},
298 {0.684499, 0.016461, 0},
299 {1, 0, 0},
300 {-1, 1, 0},
301 {-0.832647, 1.13169, 0},
302 {-0.66118, 1.20165, 0},
303 {-0.481481, 1.22222, 0},
304 {-0.289438, 1.20576, 0},
305 {-0.0809327, 1.16461, 0},
306 {0.148148, 1.11111, 0},
307 {0.40192, 1.05761, 0},
308 {0.684499, 1.01646, 0},
309 {1, 1, 0},
310 }};
311 for (const int i : evaluated_positions.index_range()) {
312 EXPECT_V3_NEAR(evaluated_positions[i], result_2[i], 1e-5f);
313 }
314}
315
316TEST(curves_geometry, NURBSEvaluation)
317{
318 CurvesGeometry curves(4, 1);
319 curves.fill_curve_types(CURVE_TYPE_NURBS);
320 curves.resolution_for_write().fill(10);
321 curves.offsets_for_write().last() = 4;
322
323 MutableSpan<float3> positions = curves.positions_for_write();
324 positions[0] = {1, 1, 0};
325 positions[1] = {0, 1, 0};
326 positions[2] = {0, 0, 0};
327 positions[3] = {-1, 0, 0};
328
329 Span<float3> evaluated_positions = curves.evaluated_positions();
330 static const Array<float3> result_1{{
331 {0.166667, 0.833333, 0}, {0.150006, 0.815511, 0}, {0.134453, 0.796582, 0},
332 {0.119924, 0.776627, 0}, {0.106339, 0.75573, 0}, {0.0936146, 0.733972, 0},
333 {0.0816693, 0.711434, 0}, {0.0704211, 0.6882, 0}, {0.0597879, 0.66435, 0},
334 {0.0496877, 0.639968, 0}, {0.0400385, 0.615134, 0}, {0.0307584, 0.589931, 0},
335 {0.0217653, 0.564442, 0}, {0.0129772, 0.538747, 0}, {0.00431208, 0.512929, 0},
336 {-0.00431208, 0.487071, 0}, {-0.0129772, 0.461253, 0}, {-0.0217653, 0.435558, 0},
337 {-0.0307584, 0.410069, 0}, {-0.0400385, 0.384866, 0}, {-0.0496877, 0.360032, 0},
338 {-0.0597878, 0.33565, 0}, {-0.0704211, 0.3118, 0}, {-0.0816693, 0.288566, 0},
339 {-0.0936146, 0.266028, 0}, {-0.106339, 0.24427, 0}, {-0.119924, 0.223373, 0},
340 {-0.134453, 0.203418, 0}, {-0.150006, 0.184489, 0}, {-0.166667, 0.166667, 0},
341 }};
342 for (const int i : evaluated_positions.index_range()) {
343 EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f);
344 }
345
346 /* Test a cyclic curve. */
347 curves.cyclic_for_write().fill(true);
348 curves.tag_topology_changed();
349 evaluated_positions = curves.evaluated_positions();
350 static const Array<float3> result_2{{
351 {0.166667, 0.833333, 0}, {0.121333, 0.778667, 0},
352 {0.084, 0.716, 0}, {0.0526667, 0.647333, 0},
353 {0.0253333, 0.574667, 0}, {0, 0.5, 0},
354 {-0.0253333, 0.425333, 0}, {-0.0526667, 0.352667, 0},
355 {-0.084, 0.284, 0}, {-0.121333, 0.221333, 0},
356 {-0.166667, 0.166667, 0}, {-0.221, 0.121667, 0},
357 {-0.281333, 0.0866667, 0}, {-0.343667, 0.0616666, 0},
358 {-0.404, 0.0466667, 0}, {-0.458333, 0.0416667, 0},
359 {-0.502667, 0.0466667, 0}, {-0.533, 0.0616666, 0},
360 {-0.545333, 0.0866667, 0}, {-0.535667, 0.121667, 0},
361 {-0.5, 0.166667, 0}, {-0.436, 0.221334, 0},
362 {-0.348, 0.284, 0}, {-0.242, 0.352667, 0},
363 {-0.124, 0.425333, 0}, {0, 0.5, 0},
364 {0.124, 0.574667, 0}, {0.242, 0.647333, 0},
365 {0.348, 0.716, 0}, {0.436, 0.778667, 0},
366 {0.5, 0.833333, 0}, {0.535667, 0.878334, 0},
367 {0.545333, 0.913333, 0}, {0.533, 0.938333, 0},
368 {0.502667, 0.953333, 0}, {0.458333, 0.958333, 0},
369 {0.404, 0.953333, 0}, {0.343667, 0.938333, 0},
370 {0.281333, 0.913333, 0}, {0.221, 0.878333, 0},
371 }};
372 for (const int i : evaluated_positions.index_range()) {
373 EXPECT_V3_NEAR(evaluated_positions[i], result_2[i], 1e-5f);
374 }
375
376 /* Test a circular cyclic curve with weights. */
377 positions[0] = {1, 0, 0};
378 positions[1] = {1, 1, 0};
379 positions[2] = {0, 1, 0};
380 positions[3] = {0, 0, 0};
381 curves.nurbs_weights_for_write().fill(1.0f);
382 curves.nurbs_weights_for_write()[0] = 4.0f;
383 curves.tag_positions_changed();
384 static const Array<float3> result_3{{
385 {0.888889, 0.555556, 0}, {0.837792, 0.643703, 0}, {0.773885, 0.727176, 0},
386 {0.698961, 0.800967, 0}, {0.616125, 0.860409, 0}, {0.529412, 0.901961, 0},
387 {0.443152, 0.923773, 0}, {0.361289, 0.925835, 0}, {0.286853, 0.909695, 0},
388 {0.221722, 0.877894, 0}, {0.166667, 0.833333, 0}, {0.122106, 0.778278, 0},
389 {0.0903055, 0.713148, 0}, {0.0741654, 0.638711, 0}, {0.0762274, 0.556847, 0},
390 {0.0980392, 0.470588, 0}, {0.139591, 0.383875, 0}, {0.199032, 0.301039, 0},
391 {0.272824, 0.226114, 0}, {0.356297, 0.162208, 0}, {0.444444, 0.111111, 0},
392 {0.531911, 0.0731388, 0}, {0.612554, 0.0468976, 0}, {0.683378, 0.0301622, 0},
393 {0.74391, 0.0207962, 0}, {0.794872, 0.017094, 0}, {0.837411, 0.017839, 0},
394 {0.872706, 0.0222583, 0}, {0.901798, 0.0299677, 0}, {0.925515, 0.0409445, 0},
395 {0.944444, 0.0555556, 0}, {0.959056, 0.0744855, 0}, {0.970032, 0.0982019, 0},
396 {0.977742, 0.127294, 0}, {0.982161, 0.162589, 0}, {0.982906, 0.205128, 0},
397 {0.979204, 0.256091, 0}, {0.969838, 0.316622, 0}, {0.953102, 0.387446, 0},
398 {0.926861, 0.468089, 0},
399 }};
400 evaluated_positions = curves.evaluated_positions();
401 for (const int i : evaluated_positions.index_range()) {
402 EXPECT_V3_NEAR(evaluated_positions[i], result_3[i], 1e-5f);
403 }
404}
405
406TEST(curves_geometry, BezierGenericEvaluation)
407{
408 CurvesGeometry curves(3, 1);
409 curves.fill_curve_types(CURVE_TYPE_BEZIER);
410 curves.resolution_for_write().fill(8);
411 curves.offsets_for_write().last() = 3;
412
413 MutableSpan<float3> handles_left = curves.handle_positions_left_for_write();
414 MutableSpan<float3> handles_right = curves.handle_positions_right_for_write();
415 MutableSpan<float3> positions = curves.positions_for_write();
416 positions.first() = {-1, 0, 0};
417 handles_right.first() = {-1, 1, 0};
418 handles_left[1] = {0, 0, 0};
419 positions[1] = {1, 0, 0};
420 handles_right[1] = {2, 0, 0};
421 handles_left.last() = {1, 1, 0};
422 positions.last() = {2, 1, 0};
423
424 /* Dangling handles shouldn't be used in a non-cyclic curve. */
425 handles_left.first() = {100, 100, 100};
426 handles_right.last() = {100, 100, 100};
427
428 Span<float3> evaluated_positions = curves.evaluated_positions();
429 static const Array<float3> result_1{{
430 {-1.0f, 0.0f, 0.0f},
431 {-0.955078f, 0.287109f, 0.0f},
432 {-0.828125f, 0.421875f, 0.0f},
433 {-0.630859f, 0.439453f, 0.0f},
434 {-0.375f, 0.375f, 0.0f},
435 {-0.0722656f, 0.263672f, 0.0f},
436 {0.265625f, 0.140625f, 0.0f},
437 {0.626953f, 0.0410156f, 0.0f},
438 {1.0f, 0.0f, 0.0f},
439 {1.28906f, 0.0429688f, 0.0f},
440 {1.4375f, 0.15625f, 0.0f},
441 {1.49219f, 0.316406f, 0.0f},
442 {1.5f, 0.5f, 0.0f},
443 {1.50781f, 0.683594f, 0.0f},
444 {1.5625f, 0.84375f, 0.0f},
445 {1.71094f, 0.957031f, 0.0f},
446 {2.0f, 1.0f, 0.0f},
447 }};
448 for (const int i : evaluated_positions.index_range()) {
449 EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f);
450 }
451
452 Array<float> radii{{0.0f, 1.0f, 2.0f}};
453 Array<float> evaluated_radii(17);
454 curves.interpolate_to_evaluated(0, radii.as_span(), evaluated_radii.as_mutable_span());
455 static const Array<float> result_2{{
456 0.0f,
457 0.125f,
458 0.25f,
459 0.375f,
460 0.5f,
461 0.625f,
462 0.75f,
463 0.875f,
464 1.0f,
465 1.125f,
466 1.25f,
467 1.375f,
468 1.5f,
469 1.625f,
470 1.75f,
471 1.875f,
472 2.0f,
473 }};
474 for (const int i : evaluated_radii.index_range()) {
475 EXPECT_NEAR(evaluated_radii[i], result_2[i], 1e-6f);
476 }
477}
478
479} // namespace blender::bke::tests
Low-level operations for curves.
EXPECT_EQ(BLI_expr_pylike_eval(expr, nullptr, 0, &result), EXPR_PYLIKE_INVALID)
@ CURVE_TYPE_BEZIER
@ CURVE_TYPE_NURBS
@ CURVE_TYPE_POLY
@ CURVE_TYPE_CATMULL_ROM
MutableSpan< T > as_mutable_span()
Definition BLI_array.hh:237
IndexRange index_range() const
Definition BLI_array.hh:349
constexpr T & first() const
Definition BLI_span.hh:680
constexpr T & last(const int64_t n=0) const
Definition BLI_span.hh:690
constexpr const T * data() const
Definition BLI_span.hh:216
constexpr int64_t size() const
Definition BLI_span.hh:253
constexpr IndexRange index_range() const
Definition BLI_span.hh:402
std::optional< Bounds< float3 > > bounds_min_max() const
Span< float3 > positions() const
VArray< bool > cyclic() const
draw_view in_light_buf[] float
TEST(action_groups, ReconstructGroupsWithReordering)
static CurvesGeometry create_basic_curves(const int points_size, const int curves_size)