mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-19 23:21:31 -07:00
Add draw_polygon_fallback() for platforms without a native polygon fill.
This adds a portable, scanline-based polygon filling algorithm, which fills a polygon by drawing a collection of adjacent horizontal lines. This change is motivated by the Rockbox port's current lack of a true polygon fill capability. Until now, it attempted to approximate a polygon fill by performing a series of triangle fills, but this worked reliably only for convex polygons. I originally considered making this new rasterizer part of the Rockbox front end itself, but I ultimately decided that it made more sense to include it here, in the Puzzles distribution, where other platforms may benefit from it in the future. No in-tree front ends use this new function quite yet, but I plan to follow this commit with a compile-time option to force front ends to use it for testing. This new polygon drawing code also comes with its own standalone driver code to test it out in isolation. This code currently relies on SDL 2.0 to present a GUI window to the user, which unfortunately adds a build-time dependency. To lessen the impact of this change, this program is gated behind a CMake build option. To use it, run: $ cmake -DBUILD_SDL_PROGRAMS=true
This commit is contained in:

committed by
Simon Tatham

parent
4149f2cb9c
commit
989df5d2bf
@ -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 $<TARGET_OBJECTS:core_obj>)
|
||||
add_library(common $<TARGET_OBJECTS:core_obj> hat.c spectre.c)
|
||||
|
||||
cliprogram(polygon-test draw-poly.c
|
||||
SDL2_LIB COMPILE_DEFINITIONS STANDALONE_POLYGON)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
puzzle(blackbox
|
||||
|
@ -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()
|
||||
|
||||
|
38
devel.but
38
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
|
||||
|
302
draw-poly.c
Normal file
302
draw-poly.c
Normal file
@ -0,0 +1,302 @@
|
||||
/*
|
||||
* draw-poly.c: Fallback polygon drawing function.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#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<<sizeof(int)*CHAR_BIT-2 - FRACBITS)
|
||||
/* Prevent integer overflow when computing `inverse_slope',
|
||||
* which shifts the coordinates left by FRACBITS, and for
|
||||
* which we'd like to avoid relying on `long long'. */
|
||||
/* If this ever causes issues, see the above comment about
|
||||
possible solutions. */
|
||||
assert(x1 < COORD_LIMIT && y1 < COORD_LIMIT);
|
||||
|
||||
/* Only create non-horizontal edges, and require y1 < y2. */
|
||||
if(y1 != y2) {
|
||||
bool swap = y1 > 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 <SDL.h>
|
||||
|
||||
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
|
2
nullfe.c
2
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,
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user