Blender V4.3
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
16#include "testing/testing.h"
17
18#include "BLI_array.hh"
19#include "BLI_convexhull_2d.h"
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/* -------------------------------------------------------------------- */
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(
66 reinterpret_cast<const float(*)[2]>(points.data()), points.size(), points_hull_map.data());
67
68 blender::Span<int> points_hull_map_span(points_hull_map.data(), points_hull_map_num);
69 return convexhull_points_from_map(points, points_hull_map_span);
70}
71
74/* -------------------------------------------------------------------- */
79{
80 return BLI_convexhull_aabb_fit_points_2d(reinterpret_cast<const float(*)[2]>(points.data()),
81 points.size());
82}
83
86/* -------------------------------------------------------------------- */
90TEST(convexhull_2d, IsConvex)
91{
94 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
95 for (float2 &p : points) {
96 p = float2(rng.get_float(), rng.get_float());
97 }
99 if (UNLIKELY(points_hull.size() < 3)) {
100 continue;
101 }
102
103 int i_prev = points_hull.size() - 2;
104 int i_curr = points_hull.size() - 1;
105 for (int i_next = 0; i_next < points_hull.size(); i_prev = i_curr, i_curr = i_next++) {
106 EXPECT_GE(cross_tri_v2(points_hull[i_prev], points_hull[i_curr], points_hull[i_next]), 0.0f);
107 }
108 }
109}
110
111TEST(convexhull_2d, IsCCW)
112{
115 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
116 for (float2 &p : points) {
117 p = float2(rng.get_float(), rng.get_float());
118 }
120
121 EXPECT_GE(
122 cross_poly_v2(reinterpret_cast<const float(*)[2]>(points_hull.data()), points_hull.size()),
123 0.0f);
124 }
125}
126
127TEST(convexhull_2d, NOP)
128{
129 { /* Single point. */
130 blender::Array<float2> points = {{0.0f, 0.0f}};
131 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
132 }
133
134 { /* Single point, 2x duplicates. */
135 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 0.0f}};
136 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
137 }
138 { /* Single point, 3x duplicates. */
139 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f}};
140 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
141 }
142}
143
144TEST(convexhull_2d, Lines_AxisAligned)
145{
146 { /* Horizontal line (2 points). */
147 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
148 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 0.0}};
149 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
150 float(math::AngleRadian::from_degree(90.0f)),
152 }
153 }
154 { /* Horizontal line (3 points). */
155 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
156 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 0.0}, {2.0f * sign_x, 0.0}};
157 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
158 float(math::AngleRadian::from_degree(90.0f)),
160 }
161 }
162
163 { /* Vertical line (2 points). */
164 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
165 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 1.0f * sign_y}};
166 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
169 }
170 }
171 { /* Vertical line (3 points). */
172 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
173 blender::Array<float2> points = {{0.0f, 0.0f}, {0.0f, 1.0f * sign_y}, {0.0f, 2.0f * sign_y}};
174 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
177 }
178 }
179
180 { /* Horizontal line (many points). */
181 blender::Array<float2> points(8);
183 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
184 /* Add points, Y is always positive. */
185 for (float2 &p : points) {
186 p = rng.get_unit_float2();
187 p[0] = 0.0;
188 }
189
190 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points), 0.0f, ROTATION_EPS);
191 }
192 }
193
194 { /* Vertical line (many points). */
195 blender::Array<float2> points(8);
197 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
198 /* Add points, Y is always positive. */
199 for (float2 &p : points) {
200 p = rng.get_unit_float2();
201 p[0] = 0.0;
202 }
203
205 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points_hull), 0.0f, ROTATION_EPS);
206 }
207 }
208}
209
210TEST(convexhull_2d, Lines_Diagonal)
211{
212 { /* Diagonal line (2 points). */
213 const float expected[4] = {45, -45, -45, 45};
214 int index = 0;
215 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
216 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
217 blender::Array<float2> points = {{0.0f, 0.0f}, {1.0f * sign_x, 1.0f * sign_y}};
218 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
219 float(math::AngleRadian::from_degree(expected[index])),
221 index++;
222 }
223 }
224 }
225
226 { /* Diagonal line (3 points). */
227 const float expected[4] = {45, -45, -45, 45};
228 int index = 0;
229 for (int sign_x = -1; sign_x <= 2; sign_x += 2) {
230 for (int sign_y = -1; sign_y <= 2; sign_y += 2) {
231 blender::Array<float2> points = {
232 {0.0f, 0.0f},
233 {1.0f * sign_x, 1.0f * sign_y},
234 {2.0f * sign_x, 2.0f * sign_y},
235 };
236 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
237 float(math::AngleRadian::from_degree(expected[index])),
239 index++;
240 }
241 }
242 }
243}
244
245TEST(convexhull_2d, Simple)
246{
247 { /* 45degree rotated square. */
248 blender::Array<float2> points = {
249 {0.0f, -1.0f},
250 {-1.0f, 0.0f},
251 {0.0f, 1.0f},
252 {1.0f, 0.0f},
253 };
254 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
255 float(math::AngleRadian::from_degree(45.0f)),
257 }
258
259 { /* Axis aligned square. */
260 blender::Array<float2> points = {
261 {-1.0f, -1.0f},
262 {-1.0f, 1.0f},
263 {1.0f, 1.0f},
264 {1.0f, -1.0f},
265 };
266 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
267 float(math::AngleRadian::from_degree(90.0f)),
269 }
270}
271
272TEST(convexhull_2d, Octagon)
273{
274 auto shape_octagon_fn = [](RandomNumberGenerator &rng,
275 const int points_num) -> blender::Array<float2> {
276 /* Avoid zero area boxes. */
277 blender::Array<float2> points(points_num);
278 for (int i = 0; i < points_num; i++) {
279 sin_cos_from_fraction(i, points_num, &points[i][0], &points[i][1]);
280 }
281 rng.shuffle<float2>(points);
282 return points;
283 };
284
286 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
287 blender::Array<float2> points = shape_octagon_fn(rng, 8);
288 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
289 float(math::AngleRadian::from_degree(67.5f)),
291 }
292}
293
294TEST(convexhull_2d, OctagonAxisAligned)
295{
296 auto shape_octagon_fn = [](RandomNumberGenerator &rng,
297 const int points_num) -> blender::Array<float2> {
298 /* Avoid zero area boxes. */
299 blender::Array<float2> points(points_num);
300 for (int i = 0; i < points_num; i++) {
301 sin_cos_from_fraction((i * 2) + 1, points_num * 2, &points[i][0], &points[i][1]);
302 }
303 rng.shuffle<float2>(points);
304 return points;
305 };
306
308 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
309 blender::Array<float2> points = shape_octagon_fn(rng, 8);
310 EXPECT_NEAR(convexhull_2d_aabb_fit_points_2d(points),
311 float(math::AngleRadian::from_degree(90.0f)),
313 }
314}
315
321TEST(convexhull_2d, Complex)
322{
323 auto shape_generate_fn = [](RandomNumberGenerator &rng,
324 const float2 &size,
325 const int points_num) -> blender::Array<float2> {
326 /* Avoid zero area boxes. */
327 blender::Array<float2> points(points_num);
328 const int points_num_reserved = 4;
329 BLI_assert(points_num_reserved >= 4);
330
331 /* Ensure there are always points at the bounds. */
332 points[0] = {0.0f, rng.get_float()}; /* Left. */
333 points[1] = {1.0f, rng.get_float()}; /* Right. */
334 points[2] = {rng.get_float(), 0.0f}; /* Bottom. */
335 points[3] = {rng.get_float(), 1.0f}; /* Top. */
336
337 for (int i = points_num_reserved; i < points_num; i++) {
338 points[i] = {rng.get_float(), rng.get_float()};
339 }
340
341 /* Shuffle to ensure the solution is valid no matter the order of the input,
342 * Only the first `points_num_reserved` matter as remaining points are random. */
343 for (int i = 0; i < points_num_reserved; i++) {
344 std::swap(points[i], points[rng.get_int32(points_num)]);
345 }
346
347 /* Map from 0-1 to a random transformation. */
348 const float2 translation = {
349 (rng.get_float() * 2.0f) - 1.0f,
350 (rng.get_float() * 2.0f) - 1.0f,
351 };
352
355 for (float2 &p : points) {
356 BLI_assert(p[0] >= 0.0 && p[0] <= 1.0f);
357 BLI_assert(p[1] >= 0.0 && p[1] <= 1.0f);
358 /* Center from [-0.5..0.5], apply size, rotate & translate. */
359 p = (((p - float2(0.5f, 0.5f)) * size) * rot_mat) + translation;
360 }
361
362 return points;
363 };
364
366 for (int i = 0; i < DEFAULT_TEST_ITER; i++) {
367 constexpr float size_margin = 0.1;
368 /* Random size from `[size_margin..2]`. */
369 float2 size = {
370 math::min((rng.get_float() * 2.0f) + size_margin, 2.0f),
371 math::min((rng.get_float() * 2.0f) + size_margin, 2.0f),
372 };
373
374 blender::Array<float2> points = shape_generate_fn(rng, size, DEFAULT_TEST_POLY_NUM);
375 const float angle = convexhull_2d_aabb_fit_points_2d(points);
376
377 const float2x2 rot_mat = math::from_rotation<float2x2>(-angle);
378 float2 tempmin, tempmax;
379 INIT_MINMAX2(tempmin, tempmax);
380 for (const float2 &p : points) {
381 math::min_max(p * rot_mat, tempmin, tempmax);
382 }
383
384 const float2 size_result = tempmax - tempmin;
385 float area_input = size[0] * size[1];
386 float area_result = size_result[0] * size_result[1];
387 EXPECT_LE(area_result, area_input + 1e-6f);
388 }
389}
390
391/* Keep these as they're handy for generating a lot of random data.
392 * To brute force check results are as expected:
393 * - Increase #DEFAULT_TEST_ITER to a large number (100k or so).
394 * - Uncomment #USE_BRUTE_FORCE_ASSERT define in `convexhull_2d.cc` to ensure results
395 * match a reference implementation.
396 */
397#if 0
398TEST(convexhull_2d, Circle)
399{
400 auto shape_circle_fn = [](RandomNumberGenerator &rng,
401 const int points_num) -> blender::Array<float2> {
402 /* Avoid zero area boxes. */
403 blender::Array<float2> points(points_num);
404
405 /* Going this way ends up with normal(s) upward */
406 for (int i = 0; i < points_num; i++) {
407 sin_cos_from_fraction(i, points_num, &points[i][0], &points[i][1]);
408 }
409 rng.shuffle<float2>(points);
410 return points;
411 };
412
414 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
415 blender::Array<float2> points = shape_circle_fn(rng, DEFAULT_TEST_POLY_NUM);
416 const float angle = convexhull_2d_aabb_fit_points_2d(points);
417 (void)angle;
418 }
419}
420
421TEST(convexhull_2d, Random)
422{
423 auto shape_random_unit_fn = [](RandomNumberGenerator &rng,
424 const int points_num) -> blender::Array<float2> {
425 /* Avoid zero area boxes. */
426 blender::Array<float2> points(points_num);
427
428 /* Going this way ends up with normal(s) upward */
429 for (int i = 0; i < points_num; i++) {
430 points[i] = rng.get_unit_float2();
431 }
432 return points;
433 };
434
436
437 for (int iter = 0; iter < DEFAULT_TEST_ITER; iter++) {
438 blender::Array<float2> points = shape_random_unit_fn(rng, DEFAULT_TEST_POLY_NUM);
439 const float angle = convexhull_2d_aabb_fit_points_2d(points);
440 (void)angle;
441 }
442}
443#endif
444
#define BLI_assert(a)
Definition BLI_assert.h:50
int BLI_convexhull_2d(const float(*points)[2], int points_num, int r_points[])
float BLI_convexhull_aabb_fit_points_2d(const float(*points)[2], int points_num)
static blender::Array< float2 > convexhull_points_from_map(blender::Span< float2 > points, blender::Span< int > points_map)
static blender::Array< float2 > convexhull_2d_as_array(blender::Span< float2 > points)
#define DEFAULT_TEST_RANDOM_SEED
#define DEFAULT_TEST_POLY_NUM
static float convexhull_2d_aabb_fit_points_2d(blender::Span< float2 > points)
#define DEFAULT_TEST_ITER
#define ROTATION_EPS
TEST(convexhull_2d, IsConvex)
#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:147
void sin_cos_from_fraction(int numerator, int denominator, float *r_sin, float *r_cos)
#define INIT_MINMAX2(min, max)
#define UNLIKELY(x)
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
int64_t size() const
Definition BLI_array.hh:245
const T * data() const
Definition BLI_array.hh:301
void shuffle(MutableSpan< T > values)
Definition BLI_rand.hh:89
constexpr int64_t size() const
Definition BLI_span.hh:253
T min(const T &a, const T &b)
void min_max(const T &value, T &min, T &max)
MatT from_rotation(const RotationT &rotation)
VecBase< float, 2 > float2
static AngleRadianBase from_degree(const T &degrees)