Blender V5.0
BLI_convexhull_2d_test.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: Apache-2.0 */
4
15
16#include "testing/testing.h"
17
18#include "BLI_array.hh"
19#include "BLI_convexhull_2d.hh"
21#include "BLI_math_geom.h"
22#include "BLI_math_matrix.h"
24#include "BLI_math_rotation.h"
25#include "BLI_math_rotation.hh"
26#include "BLI_math_vector.h"
27#include "BLI_math_vector.hh"
29#include "BLI_rand.hh"
30
31using namespace blender;
32
37#define DEFAULT_TEST_ITER 8
38
40#define DEFAULT_TEST_POLY_NUM 12
41
42#define DEFAULT_TEST_RANDOM_SEED 123
43
45#define ROTATION_EPS 1e-6
46
47/* -------------------------------------------------------------------- */
50
52 blender::Span<int> points_map)
53{
54 blender::Array<float2> points_hull(points_map.size());
55 int index = 0;
56 for (int p_index : points_map) {
57 points_hull[index++] = points[p_index];
58 }
59 return points_hull;
60}
61
63{
64 blender::Array<int> points_hull_map(points.size());
65 int points_hull_map_num = BLI_convexhull_2d(points, points_hull_map.data());
66
67 blender::Span<int> points_hull_map_span(points_hull_map.data(), points_hull_map_num);
68 return convexhull_points_from_map(points, points_hull_map_span);
69}
70
71static float mod_inline(float a, float b)
72{
73 return a - (b * floorf(a / b));
74}
75
81{
82 return mod_inline(angle, float(M_PI / 2.0f));
83}
84
86
87/* -------------------------------------------------------------------- */
90
91TEST(convexhull_2d, IsConvex)
92{
95 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
96 for (float2 &p : points) {
97 p = float2(rng.get_float(), rng.get_float());
98 }
100 if (UNLIKELY(points_hull.size() < 3)) {
101 continue;
102 }
103
104 int i_prev = points_hull.size() - 2;
105 int i_curr = points_hull.size() - 1;
106 for (int i_next = 0; i_next < points_hull.size(); i_prev = i_curr, i_curr = i_next++) {
107 EXPECT_GE(cross_tri_v2(points_hull[i_prev], points_hull[i_curr], points_hull[i_next]), 0.0f);
108 }
109 }
110}
111
112TEST(convexhull_2d, IsCCW)
113{
116 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
117 for (float2 &p : points) {
118 p = float2(rng.get_float(), rng.get_float());
119 }
121
122 EXPECT_GE(cross_poly_v2(reinterpret_cast<const float (*)[2]>(points_hull.data()),
123 points_hull.size()),
124 0.0f);
125 }
126}
127
128TEST(convexhull_2d, NOP)
129{
130 { /* Single point. */
131 blender::Array<float2> points = {{0.0f, 0.0f}};
132 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
133 }
134
135 { /* Single point, 2x duplicates. */
136 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 0.0f}};
137 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
138 }
139 { /* Single point, 3x duplicates. */
140 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f}};
141 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
142 }
143}
144
145TEST(convexhull_2d, Lines_AxisAligned)
146{
147 { /* Horizontal line (2 points). */
148 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
149 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 0.0}};
150 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
151 float(math::AngleRadian::from_degree(90.0f)),
153 }
154 }
155 { /* Horizontal line (3 points). */
156 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
157 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 0.0}, {2.0f * sign_x, 0.0}};
158 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
159 float(math::AngleRadian::from_degree(90.0f)),
161 }
162 }
163
164 { /* Vertical line (2 points). */
165 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
166 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 1.0f * sign_y}};
167 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
170 }
171 }
172 { /* Vertical line (3 points). */
173 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
174 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 1.0f * sign_y}, {0.0f, 2.0f * sign_y}};
175 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
178 }
179 }
180
181 { /* Horizontal line (many points). */
182 blender::Array<float2> points(8);
184 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
185 /* Add points, Y is always positive. */
186 for (float2 &p : points) {
187 p = rng.get_unit_float2();
188 p[0] = 0.0;
189 }
190
191 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
192 }
193 }
194
195 { /* Vertical line (many points). */
196 blender::Array<float2> points(8);
198 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
199 /* Add points, Y is always positive. */
200 for (float2 &p : points) {
201 p = rng.get_unit_float2();
202 p[0] = 0.0;
203 }
204
206 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points_hull), 0.0f, ROTATION_EPS);
207 }
208 }
209}
210
211TEST(convexhull_2d, Lines_Diagonal)
212{
213 { /* Diagonal line (2 points). */
214 const float expected[4] = {45, -45, -45, 45};
215 int index = 0;
216 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
217 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
218 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 1.0f * sign_y}};
219 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
220 float(math::AngleRadian::from_degree(expected[index])),
222 index++;
223 }
224 }
225 }
226
227 { /* Diagonal line (3 points). */
228 const float expected[4] = {45, -45, -45, 45};
229 int index = 0;
230 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
231 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
232 blender::Array<float2> points = {
233 {0.0f, 0.0f},
234 {1.0f * sign_x, 1.0f * sign_y},
235 {2.0f * sign_x, 2.0f * sign_y},
236 };
237 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
238 float(math::AngleRadian::from_degree(expected[index])),
240 index++;
241 }
242 }
243 }
244}
245
246TEST(convexhull_2d, Simple)
247{
248
249 /* 45degree rotated square. */
250 const blender::Array<float2> points_square_diagonal = {
251 {0.0f, -1.0f},
252 {-1.0f, 0.0f},
253 {0.0f, 1.0f},
254 {1.0f, 0.0f},
255 };
256
257 /* Axis aligned square. */
258 const blender::Array<float2> points_square_aligned = {
259 {-1.0f, -1.0f},
260 {-1.0f, 1.0f},
261 {1.0f, 1.0f},
262 {1.0f, -1.0f},
263 };
264
265 /* 45degree rotated square. */
266 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points_square_diagonal),
267 float(math::AngleRadian::from_degree(45.0f)),
269
270 /* Axis aligned square. */
271 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points_square_aligned),
272 float(math::AngleRadian::from_degree(90.0f)),
274
275 for (const blender::Array<float2> &points_orig : {
276 points_square_diagonal,
277 points_square_aligned,
278 })
279 {
280 for (int x = -1; x <= 1; x += 2) {
281 for (int y = -1; y <= 1; y += 2) {
282 blender::Array<float2> points = points_orig;
283 const float2 xy_flip = {float(x), float(y)};
284 for (int i = 0; i < points.size(); i++) {
285 points[i] *= xy_flip;
286 }
287
288 blender::Array<int> points_indices(points.size());
289 int points_indices_num = BLI_convexhull_2d(Span(points.data(), points.size()),
290 points_indices.data());
291
292 blender::Array<float2> points_hull(points_indices_num);
293
294 for (int i = 0; i < points_indices_num; i++) {
295 points_hull[i] = points[points_indices[i]];
296 }
297
298 /* The cross product must be positive or zero. */
299 EXPECT_GE(
300 cross_poly_v2(reinterpret_cast<float (*)[2]>(points_hull.data()), points_indices_num),
301 0.0f);
302
303 /* The first point is documented to be the lowest, check this is so. */
304 for (int i = 1; i < points_indices_num; i++) {
305 EXPECT_TRUE((points_hull[0][1] == points_hull[i][1]) ?
306 /* Equal Y therefor X must be less. */
307 (points_hull[0][0] < points_hull[i][0]) :
308 /* When Y isn't equal, Y must be less. */
309 (points_hull[0][1] < points_hull[i][1]));
310 }
311 }
312 }
313 }
314}
315
316TEST(convexhull_2d, Octagon)
317{
318 auto shape_octagon_fn = [](RandomNumberGenerator &rng,
319 const int points_num) -> blender::Array<float2> {
320 /* Avoid zero area boxes. */
321 blender::Array<float2> points(points_num);
322 for (int i = 0; i < points_num; i++) {
323 sin_cos_from_fraction(i, points_num, &points[i][0], &points[i][1]);
324 }
325 rng.shuffle<float2>(points);
326 return points;
327 };
328
330 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
331 blender::Array<float2> points = shape_octagon_fn(rng, 8);
332 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
333 float(math::AngleRadian::from_degree(67.5f)),
335 }
336}
337
338TEST(convexhull_2d, OctagonAxisAligned)
339{
340 auto shape_octagon_fn = [](RandomNumberGenerator &rng,
341 const int points_num) -> blender::Array<float2> {
342 /* Avoid zero area boxes. */
343 blender::Array<float2> points(points_num);
344 for (int i = 0; i < points_num; i++) {
345 sin_cos_from_fraction((i * 2) + 1, points_num * 2, &points[i][0], &points[i][1]);
346 }
347 rng.shuffle<float2>(points);
348 return points;
349 };
350
352 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
353 blender::Array<float2> points = shape_octagon_fn(rng, 8);
354 EXPECT_NEAR(BLI_convexhull_aabb_fit_points_2d(points),
355 float(math::AngleRadian::from_degree(90.0f)),
357 }
358}
359
360TEST(convexhull_2d, OctagonNearDuplicates)
361{
362 /* A large rotated octagon that contains two points which are *almost* duplicates.
363 * Calculating the best fit AABB returns different angles depending on the scale.
364 * This isn't something that needs *fixing* since the exact edge used may
365 * reasonably differ when scaling orders of magnate up or down.
366 * In this test don't check for the exact angle instead check the wrapped (canonical)
367 * angle matches at every scale, see: #143390. */
368 blender::Array<float2> points = {
369 {-128.28127, -311.8105},
370 {-98.5207, -288.1762},
371 {-96.177475, -267.75345},
372 {-119.81172, -237.99284},
373 {-140.23453, -235.64966},
374 {-140.23453, -235.64963}, /* Close to the previous. */
375 {-169.99509, -259.28387},
376 {-172.33832, -279.7067},
377 {-148.70407, -309.46725},
378 {-128.28127, -311.81046}, /* Close to the first. */
379 };
380
381 for (int scale_step = -15; scale_step <= 15; scale_step += 1) {
382 /* Test orders of magnitude from `1 / (10 ** 15)` to `10 ** 15` */
383 float scale;
384 if (scale_step == 0) {
385 scale = 1.0f;
386 }
387 else if (scale_step < 0) {
388 scale = float(1.0 / pow(10.0, double(-scale_step)));
389 }
390 else {
391 scale = float(pow(10.0, double(scale_step)));
392 }
393
394 blender::Array<float2> points_copy = points;
395 for (float2 &p : points_copy) {
396 p *= scale;
397 }
398
399 /* NOTE: #ROTATION_EPS epsilon fails on MacOS,
400 * use a slightly larger epsilon so tests pass on all systems. */
401 const float abs_error = scale < 10.0f ? ROTATION_EPS : 1e-5f;
403 float(math::AngleRadian::from_degree(51.5453016381f)),
404 abs_error);
405 }
406}
407
413TEST(convexhull_2d, Complex)
414{
415 auto shape_generate_fn = [](RandomNumberGenerator &rng,
416 const float2 &size,
417 const int points_num) -> blender::Array<float2> {
418 /* Avoid zero area boxes. */
419 blender::Array<float2> points(points_num);
420 const int points_num_reserved = 4;
421 BLI_assert(points_num_reserved >= 4);
422
423 /* Ensure there are always points at the bounds. */
424 points[0] = {0.0f, rng.get_float()}; /* Left. */
425 points[1] = {1.0f, rng.get_float()}; /* Right. */
426 points[2] = {rng.get_float(), 0.0f}; /* Bottom. */
427 points[3] = {rng.get_float(), 1.0f}; /* Top. */
428
429 for (int i = points_num_reserved; i < points_num; i++) {
430 points[i] = {rng.get_float(), rng.get_float()};
431 }
432
433 /* Shuffle to ensure the solution is valid no matter the order of the input,
434 * Only the first `points_num_reserved` matter as remaining points are random. */
435 for (int i = 0; i < points_num_reserved; i++) {
436 std::swap(points[i], points[rng.get_int32(points_num)]);
437 }
438
439 /* Map from 0-1 to a random transformation. */
440 const float2 translation = {
441 (rng.get_float() * 2.0f) - 1.0f,
442 (rng.get_float() * 2.0f) - 1.0f,
443 };
444
447 for (float2 &p : points) {
448 BLI_assert(p[0] >= 0.0 && p[0] <= 1.0f);
449 BLI_assert(p[1] >= 0.0 && p[1] <= 1.0f);
450 /* Center from [-0.5..0.5], apply size, rotate & translate. */
451 p = (((p - float2(0.5f, 0.5f)) * size) * rot_mat) + translation;
452 }
453
454 return points;
455 };
456
458 for (int i = 0; i < DEFAULT_TEST_ITER; i++) {
459 constexpr float size_margin = 0.1;
460 /* Random size from `[size_margin..2]`. */
461 float2 size = {
462 math::min((rng.get_float() * 2.0f) + size_margin, 2.0f),
463 math::min((rng.get_float() * 2.0f) + size_margin, 2.0f),
464 };
465
466 blender::Array<float2> points = shape_generate_fn(rng, size, DEFAULT_TEST_POLY_NUM);
467 const float angle = BLI_convexhull_aabb_fit_points_2d(points);
468
470 float2 tempmin, tempmax;
471 INIT_MINMAX2(tempmin, tempmax);
472 for (const float2 &p : points) {
473 math::min_max(p * rot_mat, tempmin, tempmax);
474 }
475
476 const float2 size_result = tempmax - tempmin;
477 float area_input = size[0] * size[1];
478 float area_result = size_result[0] * size_result[1];
479 EXPECT_LE(area_result, area_input + 1e-6f);
480 }
481}
482
483/* Keep these as they're handy for generating a lot of random data.
484 * To brute force check results are as expected:
485 * - Increase #DEFAULT_TEST_ITER to a large number (100k or so).
486 * - Uncomment #USE_BRUTE_FORCE_ASSERT define in `convexhull_2d.cc` to ensure results
487 * match a reference implementation.
488 */
489#if 0
490TEST(convexhull_2d, Circle)
491{
492 auto shape_circle_fn = [](RandomNumberGenerator &rng,
493 const int points_num) -> blender::Array<float2> {
494 /* Avoid zero area boxes. */
495 blender::Array<float2> points(points_num);
496
497 /* Going this way ends up with normal(s) upward */
498 for (int i = 0; i < points_num; i++) {
499 sin_cos_from_fraction(i, points_num, &points[i][0], &points[i][1]);
500 }
501 rng.shuffle<float2>(points);
502 return points;
503 };
504
506 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
507 blender::Array<float2> points = shape_circle_fn(rng, DEFAULT_TEST_POLY_NUM);
508 const float angle = BLI_convexhull_aabb_fit_points_2d(points);
509 (void)angle;
510 }
511}
512
513TEST(convexhull_2d, Random)
514{
515 auto shape_random_unit_fn = [](RandomNumberGenerator &rng,
516 const int points_num) -> blender::Array<float2> {
517 /* Avoid zero area boxes. */
518 blender::Array<float2> points(points_num);
519
520 /* Going this way ends up with normal(s) upward */
521 for (int i = 0; i < points_num; i++) {
522 points[i] = rng.get_unit_float2();
523 }
524 return points;
525 };
526
528
529 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
530 blender::Array<float2> points = shape_random_unit_fn(rng, DEFAULT_TEST_POLY_NUM);
531 const float angle = BLI_convexhull_aabb_fit_points_2d(points);
532 (void)angle;
533 }
534}
535#endif
536
#define BLI_assert(a)
Definition BLI_assert.h:46
float BLI_convexhull_aabb_fit_points_2d(blender::Span< blender::float2 > points)
int BLI_convexhull_2d(blender::Span< blender::float2 > points, int r_points[])
static blender::Array< float2 > convexhull_points_from_map(blender::Span< float2 > points, blender::Span< int > points_map)
static float mod_inline(float a, float b)
static blender::Array< float2 > convexhull_2d_as_array(blender::Span< float2 > points)
#define DEFAULT_TEST_RANDOM_SEED
#define DEFAULT_TEST_POLY_NUM
#define DEFAULT_TEST_ITER
#define ROTATION_EPS
static float convexhull_aabb_canonical_angle(float angle)
#define M_PI
MINLINE float cross_tri_v2(const float v1[2], const float v2[2], const float v3[2])
float cross_poly_v2(const float verts[][2], unsigned int nr)
Definition math_geom.cc:149
void sin_cos_from_fraction(int numerator, int denominator, float *r_sin, float *r_cos)
#define INIT_MINMAX2(min, max)
#define UNLIKELY(x)
static double angle(const Eigen::Vector3d &v1, const Eigen::Vector3d &v2)
Definition IK_Math.h:117
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
int64_t size() const
Definition BLI_array.hh:256
const T * data() const
Definition BLI_array.hh:312
void shuffle(MutableSpan< T > values)
Definition BLI_rand.hh:88
constexpr int64_t size() const
Definition BLI_span.hh:252
nullptr float
#define pow
AngleRadianBase< float > AngleRadian
T min(const T &a, const T &b)
void min_max(const T &value, T &min, T &max)
MatT from_rotation(const RotationT &rotation)
MatBase< float, 2, 2 > float2x2
VecBase< float, 2 > float2
TEST(compression, filter_transpose_delta)
#define floorf
static AngleRadianBase from_degree(const float &degrees)
i
Definition text_draw.cc:230