diff --git a/CMakeLists.txt b/CMakeLists.txt index 4153efc..ce3ce3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,17 @@ project(puzzles include(cmake/setup.cmake) add_library(core_obj OBJECT - combi.c divvy.c drawing.c dsf.c findloop.c grid.c latin.c - laydomino.c loopgen.c malloc.c matching.c midend.c misc.c penrose.c - penrose-legacy.c ps.c random.c sort.c tdq.c tree234.c version.c + combi.c divvy.c draw-poly.c drawing.c dsf.c findloop.c grid.c + latin.c laydomino.c loopgen.c malloc.c matching.c midend.c misc.c + penrose.c penrose-legacy.c ps.c random.c sort.c tdq.c tree234.c + version.c ${platform_common_sources}) add_library(core $) add_library(common $ hat.c spectre.c) +cliprogram(polygon-test draw-poly.c + SDL2_LIB COMPILE_DEFINITIONS STANDALONE_POLYGON) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) puzzle(blackbox diff --git a/cmake/setup.cmake b/cmake/setup.cmake index ffb8019..bfacd73 100644 --- a/cmake/setup.cmake +++ b/cmake/setup.cmake @@ -7,6 +7,7 @@ set(build_cli_programs TRUE) set(build_gui_programs TRUE) set(build_icons FALSE) set(need_c_icons FALSE) +option(BUILD_SDL_PROGRAMS "build test programs requiring SDL" FALSE) # Don't disable assertions, even in release mode. Our assertions # generally aren't expensive and protect against more annoying crashes @@ -148,7 +149,7 @@ endfunction() # a command-line helper tool. function(cliprogram NAME) cmake_parse_arguments(OPT - "CORE_LIB" "" "COMPILE_DEFINITIONS" ${ARGN}) + "CORE_LIB;SDL2_LIB" "" "COMPILE_DEFINITIONS" ${ARGN}) if(OPT_CORE_LIB) set(lib core) @@ -156,13 +157,18 @@ function(cliprogram NAME) set(lib common) endif() - if(build_cli_programs) + if(build_cli_programs AND ((NOT OPT_SDL2_LIB) OR BUILD_SDL_PROGRAMS)) add_executable(${NAME} ${CMAKE_SOURCE_DIR}/nullfe.c ${OPT_UNPARSED_ARGUMENTS}) target_link_libraries(${NAME} ${lib} ${platform_libs}) if(OPT_COMPILE_DEFINITIONS) target_compile_definitions(${NAME} PRIVATE ${OPT_COMPILE_DEFINITIONS}) endif() + if(OPT_SDL2_LIB) + find_package(SDL2 REQUIRED) + include_directories(${NAME} ${SDL2_INCLUDE_DIRS}) + target_link_libraries(${NAME} ${SDL2_LIBRARIES}) + endif() endif() endfunction() diff --git a/devel.but b/devel.but index 3a7c9cd..b5d462a 100644 --- a/devel.but +++ b/devel.but @@ -2752,6 +2752,14 @@ function; see \k{drawing-draw-line}. This function behaves exactly like the back end \cw{draw_polygon()} function; see \k{drawing-draw-polygon}. +An implementation of this API which doesn't have a native polygon fill +primitive is permitted to define this function pointer to point to the +middleware's \c{draw_polygon_fallback()} (see +\k{drawing-draw-polygon-fallback}), which is a fallback polygon +rasterizer that produces a series of \c{draw_line()} calls to fill and +outline the polygon. However, it is explicitly \e{not} permitted for +this function pointer to be \cw{NULL}. + \S{drawingapi-draw-circle} \cw{draw_circle()} \c void (*draw_circle)(drawing *dr, int cx, int cy, int radius, @@ -3084,6 +3092,36 @@ with the RGB values of the desired colour (if printing in colour), or all filled with the grey-scale value (if printing in black and white). +\S{drawing-draw-polygon-fallback} \cw{draw_polygon_fallback()} + +\c void draw_polygon_fallback(drawing *dr, +\c const int *coords, int npoints, +\c int fillcolour, int outlinecolour); + +This function is intended for use by front ends which do not have a +native polygon fill primitive. Its signature and semantics are exactly +the same as \cw{draw_polygon()} (\k{drawing-draw-polygon}); however, +instead of being implemented by a front end, it is implemented as a +piece of middleware that uses a scanline algorithm to produce a series +of calls to \cw{draw_line()} that have the effect of filling and +outlining the desired polygon. + +Although a front end may choose to call this function directly (such +as from a stub implementation of \c{draw_polygon()}), this function is +intended to be \e{pointed to} by the \c{draw_polygon} field of the +front end's \c{drawing_api}; that is, a frontend without a polygon +fill primitive should set \c{drawing_api}'s \c{draw_polygon} field to +\c{draw_polygon_fallback}. + +The motivation for this rather unwieldy method of employing this +fallback function (instead of simply setting \c{draw_polygon} to +\c{NULL} in \c{drawing_api}) is that it allows a link-time optimizing +compiler to prune this function's implementation on platforms that +provide their own \c{draw_polygon()}, since this function would never +be referenced on those platforms. (But this function is still +unconditionally compiled on all platforms, thus protecting it from +bit-rot.) + \C{midend} The API provided by the mid-end This chapter documents the API provided by the mid-end to be called diff --git a/draw-poly.c b/draw-poly.c new file mode 100644 index 0000000..f838996 --- /dev/null +++ b/draw-poly.c @@ -0,0 +1,302 @@ +/* + * draw-poly.c: Fallback polygon drawing function. + */ + +#include + +#include "puzzles.h" + +struct edge { + int x1, y1; + int x2, y2; + bool active; + + /* whether y1 is a closed endpoint (i.e. this edge should be + * active when y == y1) */ + bool closed_y1; + + /* (x2 - x1) / (y2 - y1) as 16.16 signed fixed point; precomputed + * for speed */ + long inverse_slope; +}; + +#define FRACBITS 16 +#define ONEHALF (1 << (FRACBITS-1)) + +void draw_polygon_fallback(drawing *dr, const int *coords, int npoints, + int fillcolour, int outlinecolour) +{ + struct edge *edges; + int min_y = INT_MAX, max_y = INT_MIN, i, y; + int n_edges = 0; + int *intersections; + + if(npoints < 3) + return; + + if(fillcolour < 0) + goto draw_outline; + + /* This uses a basic scanline rasterization algorithm for polygon + * filling. First, an "edge table" is constructed for each pair of + * neighboring points. Horizontal edges are excluded. Then, the + * algorithm iterates a horizontal "scan line" over the vertical + * (Y) extent of the polygon. At each Y level, it maintains a set + * of "active" edges, which are those which intersect the scan + * line at the current Y level. The X coordinates where the scan + * line intersects each active edge are then computed via + * fixed-point arithmetic and stored. Finally, horizontal lines + * are drawn between each successive pair of intersection points, + * in the order of ascending X coordinate. This has the effect of + * "even-odd" filling when the polygon is self-intersecting. + * + * I (vaguely) based this implementation off the slides below: + * + * https://www.khoury.northeastern.edu/home/fell/CS4300/Lectures/CS4300F2012-9-ScanLineFill.pdf + * + * I'm fairly confident that this current implementation is + * correct (i.e. draws the right polygon, free from artifacts), + * but it isn't quite as efficient as it could be. Namely, it + * currently maintains the active edge set by setting the `active` + * flag in the `edge` array, which is quite inefficient. Perhaps + * future optimization could see this replaced with a tree + * set. Additionally, one could perhaps replace the current linear + * search for edge endpoints (i.e. the inner loop over `edges`) by + * sorting the edge table by upper and lower Y coordinate. + * + * A final caveat comes from the use of fixed point arithmetic, + * which is motivated by performance considerations on FPU-less + * platforms (e.g. most Rockbox devices, and maybe others?). I'm + * currently using 16 fractional bits to compute the edge + * intersections, which (in the case of a 32-bit int) limits the + * acceptable range of coordinates to roughly (-2^14, +2^14). This + * ought to be acceptable for the forseeable future, but + * ultra-high DPI screens might one day exceed this. In that case, + * options include switching to int64_t (but that comes with its + * own portability headaches), reducing the number of fractional + * bits, or just giving up and using floating point. + */ + + /* Build edge table from coords. Horizontal edges are filtered + * out, so n_edges <= n_points in general. */ + edges = smalloc(npoints * sizeof(struct edge)); + + for(i = 0; i < npoints; i++) { + int x1, y1, x2, y2; + + x1 = coords[(2*i+0)]; + y1 = coords[(2*i+1)]; + x2 = coords[(2*i+2) % (npoints * 2)]; + y2 = coords[(2*i+3) % (npoints * 2)]; + + if(y1 < min_y) + min_y = y1; + if(y1 > max_y) + max_y = y1; + +#define COORD_LIMIT (1< y2; + int lower_endpoint = swap ? (i + 1) : i; + + /* Compute index of the point adjacent to lower end of + * this edge (which is not the upper end of this edge). */ + int lower_neighbor = swap ? (lower_endpoint + 1) % npoints : (lower_endpoint + npoints - 1) % npoints; + + struct edge *edge = edges + (n_edges++); + + edge->active = false; + edge->x1 = swap ? x2 : x1; + edge->y1 = swap ? y2 : y1; + edge->x2 = swap ? x1 : x2; + edge->y2 = swap ? y1 : y2; + edge->inverse_slope = ((edge->x2 - edge->x1) << FRACBITS) / (edge->y2 - edge->y1); + edge->closed_y1 = edge->y1 < coords[2*lower_neighbor+1]; + } + } + + /* a generous upper bound on number of intersections is n_edges */ + intersections = smalloc(n_edges * sizeof(int)); + + for(y = min_y; y <= max_y; y++) { + int n_intersections = 0; + for(i = 0; i < n_edges; i++) { + struct edge *edge = edges + i; + /* Update active edge set. These conditions are mutually + * exclusive because of the invariant that y1 < y2. */ + if(edge->y1 + (edge->closed_y1 ? 0 : 1) == y) + edge->active = true; + else if(edge->y2 + 1 == y) + edge->active = false; + + if(edge->active) { + int x = edges[i].x1; + x += (edges[i].inverse_slope * (y - edges[i].y1) + ONEHALF) >> FRACBITS; + intersections[n_intersections++] = x; + } + } + + qsort(intersections, n_intersections, sizeof(int), compare_integers); + + assert(n_intersections % 2 == 0); + assert(n_intersections <= n_edges); + + /* Draw horizontal lines between successive pairs of + * intersections of the scanline with active edges. */ + for(i = 0; i + 1 < n_intersections; i += 2) { + draw_line(dr, + intersections[i], y, + intersections[i+1], y, + fillcolour); + } + } + + sfree(intersections); + sfree(edges); + +draw_outline: + assert(outlinecolour >= 0); + for (i = 0; i < 2 * npoints; i += 2) + draw_line(dr, + coords[i], coords[i+1], + coords[(i+2)%(2*npoints)], coords[(i+3)%(2*npoints)], + outlinecolour); +} + +#ifdef STANDALONE_POLYGON + +/* + * Standalone program to test draw_polygon_fallback(). By default, + * creates a window and allows clicking points to build a + * polygon. Optionally, can draw a randomly growing polygon in + * "screensaver" mode. + */ + +#include + +void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour) +{ + SDL_Renderer *renderer = GET_HANDLE_AS_TYPE(dr, SDL_Renderer); + SDL_RenderDrawLine(renderer, x1, y1, x2, y2); +} + +#define WINDOW_WIDTH 800 +#define WINDOW_HEIGHT 600 +#define MAX_SCREENSAVER_POINTS 1000 + +int main(int argc, char *argv[]) { + SDL_Window* window = NULL; + SDL_Event event; + SDL_Renderer *renderer = NULL; + int running = 1; + int i; + drawing dr; + bool screensaver = false; + + if(argc >= 2) { + if(!strcmp(argv[1], "--screensaver")) + screensaver = true; + else + printf("usage: %s [--screensaver]\n", argv[0]); + } + + int *poly = NULL; + int n_points = 0; + + /* Initialize SDL */ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); + return 1; + } + + /* Create window */ + window = SDL_CreateWindow("Polygon Drawing Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN); + if (!window) { + printf("Window could not be created! SDL_Error: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + /* Create renderer */ + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (!renderer) { + printf("Renderer could not be created! SDL_Error: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + dr.handle = renderer; + + if(!screensaver) + printf("Click points in the window to create vertices. Pressing C resets.\n"); + + while (running) { + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_QUIT) { + running = 0; + } + else if (event.type == SDL_MOUSEBUTTONDOWN) { + if (event.button.button == SDL_BUTTON_LEFT) { + int mouseX = event.button.x; + int mouseY = event.button.y; + + poly = realloc(poly, ++n_points * 2 * sizeof(int)); + poly[2 * (n_points - 1)] = mouseX; + poly[2 * (n_points - 1) + 1] = mouseY; + } + } + else if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_c) { + free(poly); + poly = NULL; + n_points = 0; + } + } + } + + if(screensaver) { + poly = realloc(poly, ++n_points * 2 * sizeof(int)); + poly[2 * (n_points - 1)] = rand() % WINDOW_WIDTH; + poly[2 * (n_points - 1) + 1] = rand() % WINDOW_HEIGHT; + + if(n_points >= MAX_SCREENSAVER_POINTS) { + free(poly); + poly = NULL; + n_points = 0; + } + } + + /* Clear the screen with a black color */ + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + /* Set draw color to white */ + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + + draw_polygon_fallback(&dr, poly, n_points, 1, 1); + + SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); + for(i = 0; i < 2*n_points; i+=2) + SDL_RenderDrawPoint(renderer, poly[i], poly[i+1]); + + /* Present the back buffer */ + SDL_RenderPresent(renderer); + } + + /* Clean up and quit SDL */ + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} +#endif diff --git a/nullfe.c b/nullfe.c index b6022d4..42cf532 100644 --- a/nullfe.c +++ b/nullfe.c @@ -19,7 +19,9 @@ void drawing_free(drawing *dr) { sfree(dr); } void draw_text(drawing *dr, int x, int y, int fonttype, int fontsize, int align, int colour, const char *text) {} void draw_rect(drawing *dr, int x, int y, int w, int h, int colour) {} +#ifndef STANDALONE_POLYGON void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour) {} +#endif void draw_thick_line(drawing *dr, float thickness, float x1, float y1, float x2, float y2, int colour) {} void draw_polygon(drawing *dr, const int *coords, int npoints, diff --git a/puzzles.h b/puzzles.h index f04bdbf..cf944eb 100644 --- a/puzzles.h +++ b/puzzles.h @@ -266,6 +266,8 @@ void draw_rect(drawing *dr, int x, int y, int w, int h, int colour); void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour); void draw_polygon(drawing *dr, const int *coords, int npoints, int fillcolour, int outlinecolour); +void draw_polygon_fallback(drawing *dr, const int *coords, int npoints, + int fillcolour, int outlinecolour); void draw_circle(drawing *dr, int cx, int cy, int radius, int fillcolour, int outlinecolour); void draw_thick_line(drawing *dr, float thickness,