From fc9ed1ce6661ccc73783ee9193fa77e8a673b839 Mon Sep 17 00:00:00 2001 From: Mathias Westerdahl Date: Sat, 9 Nov 2019 15:06:43 +0100 Subject: [PATCH] Restructured the bounding box generation (#46) * Restructured the code for handling the bounding box generation Added unit tests for the point pruning functions * Win32 compile fix --- src/jc_voronoi.h | 100 +++++++++++++++++++++++-------------------- test/jc_test.h | 15 ++++--- test/test.c | 108 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 169 insertions(+), 54 deletions(-) diff --git a/src/jc_voronoi.h b/src/jc_voronoi.h index 58670c5..6fd1dd7 100644 --- a/src/jc_voronoi.h +++ b/src/jc_voronoi.h @@ -1234,7 +1234,7 @@ static inline void jcv_rect_inflate(jcv_rect* rect, jcv_real amount) rect->max.y += amount; } -static int jcv_prune_duplicates(jcv_context_internal* internal, const jcv_rect* rect) +static int jcv_prune_duplicates(jcv_context_internal* internal, jcv_rect* rect) { int num_sites = internal->numsites; jcv_site* sites = internal->sites; @@ -1257,25 +1257,16 @@ static int jcv_prune_duplicates(jcv_context_internal* internal, const jcv_rect* sites[i - offset] = sites[i]; - if( rect == 0 ) - { - jcv_rect_union(&r, &s->p); - } + jcv_rect_union(&r, &s->p); } - - if( rect == 0 ) - { - jcv_rect_round(&r); - jcv_rect_inflate(&r, 10); - internal->rect = r; - } else { - internal->rect = *rect; + internal->numsites -= offset; + if (rect) { + *rect = r; } - return offset; } -static int jcv_prune_not_in_shape(jcv_context_internal* internal, const jcv_rect* rect) +static int jcv_prune_not_in_shape(jcv_context_internal* internal, jcv_rect* rect) { int num_sites = internal->numsites; jcv_site* sites = internal->sites; @@ -1297,20 +1288,17 @@ static int jcv_prune_not_in_shape(jcv_context_internal* internal, const jcv_rect sites[i - offset] = sites[i]; - if( rect == 0 ) - { - jcv_rect_union(&r, &s->p); - } + jcv_rect_union(&r, &s->p); + } + internal->numsites -= offset; + if (rect) { + *rect = r; } - return offset; } -void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn, jcv_diagram* d ) +static jcv_context_internal* jcv_alloc_internal(int num_points, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn) { - if( d->internal ) - jcv_diagram_free( d ); - // Interesting limits from Euler's equation // Slide 81: https://courses.cs.washington.edu/courses/csep521/01au/lectures/lecture10slides.pdf // Page 3: https://sites.cs.ucsb.edu/~suri/cs235/Voronoi.pdf @@ -1332,16 +1320,6 @@ void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, co internal->alloc = allocfn; internal->free = freefn; - internal->beachline_start = jcv_halfedge_new(internal, 0, 0); - internal->beachline_end = jcv_halfedge_new(internal, 0, 0); - - internal->beachline_start->left = 0; - internal->beachline_start->right = internal->beachline_end; - internal->beachline_end->left = internal->beachline_start; - internal->beachline_end->right = 0; - - internal->last_inserted = 0; - internal->sites = (jcv_site*) mem; mem += sitessize; @@ -1352,6 +1330,27 @@ void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, co tmp.charp = mem; internal->eventmem = tmp.voidpp; + return internal; +} + +void jcv_diagram_generate_useralloc(int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn, jcv_diagram* d) +{ + if( d->internal ) + jcv_diagram_free( d ); + + jcv_context_internal* internal = jcv_alloc_internal(num_points, userallocctx, allocfn, freefn); + + internal->beachline_start = jcv_halfedge_new(internal, 0, 0); + internal->beachline_end = jcv_halfedge_new(internal, 0, 0); + + internal->beachline_start->left = 0; + internal->beachline_start->right = internal->beachline_end; + internal->beachline_end->left = internal->beachline_start; + internal->beachline_end->right = 0; + + internal->last_inserted = 0; + + int max_num_events = num_points*2; // beachline can have max 2*n-5 parabolas jcv_pq_create(internal->eventqueue, max_num_events, (void**)internal->eventmem); internal->numsites = num_points; @@ -1375,29 +1374,38 @@ void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, co } internal->clipper = *clipper; - int offset = jcv_prune_duplicates(internal, rect); - num_points -= offset; + jcv_rect tmp_rect; + tmp_rect.min.x = tmp_rect.min.y = JCV_FLT_MAX; + tmp_rect.max.x = tmp_rect.max.y = -JCV_FLT_MAX; + jcv_prune_duplicates(internal, &tmp_rect); // Prune using the test second if (internal->clipper.test_fn) { - internal->clipper.min = internal->rect.min; - internal->clipper.max = internal->rect.max; + // e.g. used by the box clipper in the test_fn + internal->clipper.min = rect ? rect->min : tmp_rect.min; + internal->clipper.max = rect ? rect->max : tmp_rect.max; + + jcv_prune_not_in_shape(internal, &tmp_rect); - offset = jcv_prune_not_in_shape(internal, rect); - num_points -= offset; + // The pruning might have made the bounding box smaller + if (!rect) { + // In the case of all sites being all on a horizontal or vertical line, the + // rect area will be zero, and the diagram generation will most likely fail + jcv_rect_round(&tmp_rect); + jcv_rect_inflate(&tmp_rect, 10); - internal->clipper.min = internal->rect.min; - internal->clipper.max = internal->rect.max; + internal->clipper.min = tmp_rect.min; + internal->clipper.max = tmp_rect.max; + } } + internal->rect = rect ? *rect : tmp_rect; + d->min = internal->rect.min; d->max = internal->rect.max; + d->numsites = internal->numsites; d->internal = internal; - d->numsites = num_points; - - internal->numsites = num_points; - internal->currentsite = 0; internal->bottomsite = jcv_nextsite(internal); diff --git a/test/jc_test.h b/test/jc_test.h index 68bc8e8..13ea247 100644 --- a/test/jc_test.h +++ b/test/jc_test.h @@ -142,8 +142,8 @@ extern jc_test_state jc_test_global_state; #define TEST(_TEST_) { #_TEST_, JC_TEST_CAST(jc_test_func, (_TEST_)) }, -extern void jc_test_run_test_fixture(jc_test_fixture* fixture); -extern void jc_test_run_all_tests(jc_test_state* state); +extern int jc_test_run_test_fixture(jc_test_fixture* fixture); +extern int jc_test_run_all_tests(jc_test_state* state); extern void jc_test_assert(jc_test_fixture* fixture, bool cond, const char* msg); extern unsigned long long jc_test_get_time(void); @@ -232,7 +232,7 @@ static void jc_test_report_time(unsigned long long t) // Micro seconds JC_TEST_PRINTF("%g %s", t / 1000000.0, "s"); } -void jc_test_run_test_fixture(jc_test_fixture* fixture) +int jc_test_run_test_fixture(jc_test_fixture* fixture) { jc_test_global_state.current_fixture = fixture; @@ -311,16 +311,19 @@ void jc_test_run_test_fixture(jc_test_fixture* fixture) JC_TEST_PRINTF("\n"); jc_test_global_state.current_fixture = 0; + + return fixture->stats.num_fail; } -void jc_test_run_all_tests(jc_test_state* state) +int jc_test_run_all_tests(jc_test_state* state) { + int num_fail = 0; state->stats.totaltime = 0; for( int i = 0; i < state->num_fixtures; ++i ) { if( state->fixtures[i] ) { - jc_test_run_test_fixture( state->fixtures[i] ); + num_fail += jc_test_run_test_fixture( state->fixtures[i] ); state->stats.num_assertions += state->fixtures[i]->stats.num_assertions; state->stats.num_pass += state->fixtures[i]->stats.num_pass; @@ -337,6 +340,8 @@ void jc_test_run_all_tests(jc_test_state* state) JC_TEST_PRINTF("%d tests passed, and %d tests %sFAILED%s\n", state->stats.num_pass, state->stats.num_fail, JC_TEST_CLR_RED, JC_TEST_CLR_DEFAULT); else JC_TEST_PRINTF("%d tests %sPASSED%s\n", state->stats.num_pass, JC_TEST_CLR_GREEN, JC_TEST_CLR_DEFAULT); + + return num_fail; } #if defined(_MSC_VER) diff --git a/test/test.c b/test/test.c index 82e9069..214559e 100644 --- a/test/test.c +++ b/test/test.c @@ -34,7 +34,9 @@ static void test_setup(Context* ctx) static void test_teardown(Context* ctx) { - jcv_diagram_free(&ctx->diagram); + if (ctx->diagram.internal) { + jcv_diagram_free(&ctx->diagram); + } } static void debug_points(int num, const jcv_point* points) @@ -217,6 +219,105 @@ static void voronoi_test_culling(Context* ctx) check_edges( sites[0].edges, 4, expected_edges_0, expected_neighbors_0 ); } + +static jcv_context_internal* setup_test_context_internal(int num_points, jcv_point* points, void* ctx) +{ + jcv_context_internal* internal = jcv_alloc_internal(num_points, ctx, jcv_alloc_fn, jcv_free_fn); + internal->numsites = num_points; + jcv_site* sites = internal->sites; + + for( int i = 0; i < num_points; ++i ) + { + sites[i].p = points[i]; + sites[i].edges = 0; + sites[i].index = i; + } + qsort(sites, (size_t)num_points, sizeof(jcv_site), jcv_point_cmp); + + return internal; +} + +static void setup_clip_shape_box(jcv_context_internal* internal, jcv_rect rect) +{ + jcv_clipper box_clipper; + box_clipper.test_fn = jcv_boxshape_test; + box_clipper.clip_fn = jcv_boxshape_clip; + box_clipper.fill_fn = jcv_boxshape_fillgaps; + internal->clipper = box_clipper; + + internal->clipper.min = rect.min; + internal->clipper.max = rect.max; +} + +static void teardown_test_context_internal(jcv_context_internal* internal) +{ + jcv_free_fn(0, internal->mem); +} + +static void voronoi_test_prune_duplicates(Context* ctx) +{ + (void)ctx; + jcv_point duplicate = {1,2}; + jcv_point points[] = { {1,2}, {2,2}, {1,2}, {3,3}}; + int num_points = sizeof(points) / sizeof(points[0]); + + jcv_context_internal* internal = setup_test_context_internal(num_points, points, 0); + ASSERT_EQ( 4, internal->numsites ); + + jcv_rect rect; + int num_removed = jcv_prune_duplicates(internal, &rect); + + ASSERT_EQ( 1, num_removed ); + ASSERT_EQ( 3, internal->numsites ); + + int count = 0; + for (int i = 0; i < internal->numsites; ++i) + { + if (internal->sites[i].p.x == duplicate.x && + internal->sites[i].p.y == duplicate.y) { + count++; + } + } + ASSERT_EQ( 1, count ); + + ASSERT_EQ( 1, rect.min.x ); + ASSERT_EQ( 2, rect.min.y ); + ASSERT_EQ( 3, rect.max.x ); + ASSERT_EQ( 3, rect.max.y ); + + teardown_test_context_internal(internal); +} + +static void voronoi_test_prune_not_in_shape(Context* ctx) +{ + (void)ctx; + jcv_point points[] = { {0,0}, {1,9}, {2,8}, {5,5}, {8,2}, {9,1}, {10,10}}; + int num_points = sizeof(points) / sizeof(points[0]); + + jcv_context_internal* internal = setup_test_context_internal(num_points, points, 0); + ASSERT_EQ( num_points, internal->numsites ); + + jcv_rect smaller_rect; + smaller_rect.min.x = 2; + smaller_rect.min.y = 2; + smaller_rect.max.x = 8; + smaller_rect.max.y = 8; + setup_clip_shape_box(internal, smaller_rect); + + jcv_rect rect; + int num_removed = jcv_prune_not_in_shape(internal, &rect); + + ASSERT_EQ( 4, num_removed ); + ASSERT_EQ( 3, internal->numsites ); + + ASSERT_EQ( 2, rect.min.x ); + ASSERT_EQ( 2, rect.min.y ); + ASSERT_EQ( 8, rect.max.x ); + ASSERT_EQ( 8, rect.max.y ); + + teardown_test_context_internal(internal); +} + // for debugging // static void write_points(const char* name, int num_points, const jcv_point* points) // { @@ -438,6 +539,8 @@ static void voronoi_test_issue38_numsites_equals_one_assert(Context* ctx) } TEST_BEGIN(voronoi_test, voronoi_main_setup, voronoi_main_teardown, test_setup, test_teardown) + TEST(voronoi_test_prune_duplicates) + TEST(voronoi_test_prune_not_in_shape) TEST(voronoi_test_parallel_horiz_2) TEST(voronoi_test_parallel_vert_2) TEST(voronoi_test_one_site) @@ -461,6 +564,5 @@ int main(int argc, const char** argv) (void)debug_edges; (void)debug_points; - RUN_ALL(); - return 0; + return RUN_ALL(); }