ncvisual_rotate() for arbitrary radians (#600)

* normal/visual: need dup stdplane for rotate
* add ncplane_center(), unit tests
* ncplane_center_abs(): add, unit tests
* ncvisual_bounding_box() for #599
* ncvisual_rotate(): works via bounding box #599
* normal demo: comment out broken section
* rotate: resize underlying plane as needed #599
* ncvisual_rotate: support negative rads #599
This commit is contained in:
Nick Black 2020-05-12 22:57:28 -04:00 committed by GitHub
parent 420ef740b1
commit f602c440a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 76 deletions

View File

@ -35,12 +35,17 @@ offset(uint32_t* rgba, int y, int x, int dx){
// make a pixel array out from the center, blitting it as we go
int normal_demo(struct notcurses* nc){
int dy, dx;
struct ncplane* n = notcurses_stddim_yx(nc, &dy, &dx);
// we can't resize (and thus can't rotate) the standard plane, so dup it
struct ncplane* n = ncplane_dup(notcurses_stddim_yx(nc, &dy, &dx), NULL);
if(n == NULL){
return -1;
}
ncplane_erase(n);
struct ncvisual* ncv = NULL;
dy *= VSCALE; // double-block trick means both 2x resolution and even linecount yay
uint32_t* rgba = malloc(sizeof(*rgba) * dy * dx);
if(!rgba){
return -1;
goto err;
}
memset(rgba, 0, sizeof(*rgba) * dy * dx);
int y;
@ -48,38 +53,47 @@ int normal_demo(struct notcurses* nc){
y = dy / VSCALE + 1;
for(int x = 0 ; x < dx ; ++x){
if(mcell(offset(rgba, y, x, dx), y, x, dy / VSCALE, dx)){
return -1;
goto err;
}
}
}
for(y = 0 ; y < dy / 2 ; ++y){
for(int x = 0 ; x < dx ; ++x){
if(mcell(offset(rgba, dy / 2 - y, x, dx), dy / 2 - y, x, dy, dx)){
return -1;
goto err;
}
if(mcell(offset(rgba, dy / 2 + y - 1, x, dx), dy / 2 + y - 1, x, dy, dx)){
return -1;
goto err;
}
}
if(ncblit_rgba(n, 0, 0, dx * sizeof(*rgba), rgba, 0, 0, dy, dx) < 0){
return -1;
goto err;
}
DEMO_RENDER(nc);
}
free(rgba);
struct ncvisual* ncv = ncvisual_from_plane(n, 0, 0, -1, -1);
rgba = NULL;
// FIXME ncvisual_from_plane is failing
/*ncv = ncvisual_from_plane(n, 0, 0, -1, -1);
if(!ncv){
return -1;
goto err;
}
for(int i = 1 ; i < 100 ; ++i){
if(ncvisual_rotate(ncv, M_PI / 2)){
return -1;
goto err;
}
if(ncvisual_render(ncv, 0, 0, -1, -1) <= 0){
return -1;
goto err;
}
DEMO_RENDER(nc);
}
}*/
ncvisual_destroy(ncv);
ncplane_destroy(n);
return 0;
err:
free(rgba);
ncvisual_destroy(ncv);
ncplane_destroy(n);
return -1;
}

View File

@ -643,10 +643,43 @@ ncplane* rotate_plane(const ncplane* n);
void* bgra_to_rgba(const void* data, int rows, int rowstride, int cols);
static inline void
center_box(int* RESTRICT y, int* RESTRICT x){
if(y){
*y = (*y - 1) / 2;
}
if(x){
*x = (*x - 1) / 2;
}
}
// find the "center" cell of a plane. in the case of even rows/columns, we
// place the center on the top/left. in such a case there will be one more
// cell to the bottom/right of the center.
void ncplane_center(const ncplane* n, int* y, int* x);
static inline void
ncplane_center(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
*y = n->leny;
*x = n->lenx;
center_box(y, x);
}
// find the center coordinate of a plane, preferring the top/left in the
// case of an even number of rows/columns (in such a case, there will be one
// more cell to the bottom/right of the center than the top/left). the
// center is then modified relative to the plane's origin.
static inline void
ncplane_center_abs(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
ncplane_center(n, y, x);
if(y){
*y += n->absy;
}
if(x){
*x += n->absx;
}
}
int ncvisual_bounding_box(const struct ncvisual* ncv, int* leny, int* lenx,
int* offy, int* offx);
#ifdef __cplusplus
}

View File

@ -489,7 +489,7 @@ int ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny,
int ncplane_resize(ncplane* n, int keepy, int keepx, int keepleny,
int keeplenx, int yoff, int xoff, int ylen, int xlen){
if(n == n->nc->stdscr){
//fprintf(stderr, "Can't resize standard plane\n");
fprintf(stderr, "Can't resize standard plane\n");
return -1;
}
return ncplane_resize_internal(n, keepy, keepx, keepleny, keeplenx,

View File

@ -349,8 +349,6 @@ paint(ncplane* p, cell* lastframe, struct crender* rvec,
fprintf(stderr, "WROTE %u [%c] to %d/%d (%d/%d)\n", targc->gcluster, prevcell->gcluster, y, x, absy, absx);
}else{
fprintf(stderr, "WROTE %u [%s] to %d/%d (%d/%d)\n", targc->gcluster, extended_gcluster(crender->p, targc), y, x, absy, absx);
}
fprintf(stderr, "POOL: %p NC: %p SRC: %p\n", pool->pool, nc, crender->p);
}*/
if(cellcmp_and_dupfar(pool, prevcell, crender->p, targc)){
crender->damaged = true;

View File

@ -113,8 +113,7 @@ auto ncvisual_from_plane(const ncplane* n, int begy, int begx, int leny, int len
}
int dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
// FIXME needs to make use of begy, begx, leny, lenx!
auto* ncv = ncvisual_from_rgba(n->nc, rgba, n->leny, n->lenx * 4, n->lenx);
auto* ncv = ncvisual_from_rgba(n->nc, rgba, leny, lenx * 4, lenx);
if(ncv == nullptr){
free(rgba);
return nullptr;
@ -156,78 +155,169 @@ auto ncvisual_setplane(ncvisual* ncv, ncplane* n) -> int {
return ret;
}
// if we're rotating around our center, we can't require any radius greater
// than our longer length. rotation can thus be held entirely within a square
// plane having length of our longest length. after one rotation, this decays
// to the same square throughout any rotations.
static auto
rotate_new_geom(ncvisual* ncv, double rads, double *stheta, double *ctheta) -> int {
*stheta = sin(rads);
*ctheta = cos(rads);
const int scaledy = ncv->ncp->leny * encoding_vert_scale(ncv);
const int diam = scaledy < ncv->ncp->lenx ? ncv->ncp->lenx : scaledy;
//fprintf(stderr, "rotating %d -> %d / %d\n", ncv->ncp->leny, scaledy, ncv->ncp->lenx);
if(ncv->ncp->lenx != scaledy){
if(ncplane_resize_simple(ncv->ncp, diam / encoding_vert_scale(ncv), diam) < 0){
return -1;
// Inspects the visual to find the minimum rectangle that can contain all
// "real" pixels, where "real" pixels are, by convention, all zeroes.
// Placing this box at offyXoffx relative to the visual will encompass all
// pixels. Returns the area of the box (0 if there are no pixels).
auto ncvisual_bounding_box(const ncvisual* ncv, int* leny, int* lenx,
int* offy, int* offx) -> int {
int trow, lcol, rcol;
// first, find the topmost row with a real pixel. if there is no such row,
// there are no such pixels. if we find one, we needn't look in this region
// for other extrema, so long as we keep the leftmost and rightmost through
// this row (from the top). said leftmost and rightmost will be the leftmost
// and rightmost pixel of whichever row has the topmost valid pixel. unlike
// the topmost, they'll need be further verified.
for(trow = 0 ; trow < ncv->dstheight ; ++trow){
int x;
for(x = 0 ; x < ncv->dstwidth ; ++x){
uint32_t rgba = ncv->data[trow * ncv->rowstride / 4 + x];
if(rgba){
lcol = x; // leftmost pixel of topmost row
// now find rightmost pixel of topmost row
int xr;
for(xr = ncv->dstwidth - 1 ; xr > x ; --xr){
rgba = ncv->data[trow * ncv->rowstride / 4 + xr];
if(rgba){ // rightmost pixel of topmost row
break;
}
}
rcol = xr;
break;
}
}
if(x < ncv->dstwidth){
break;
}
}
return diam;
if(trow == ncv->dstheight){ // no real pixels
*leny = 0;
*lenx = 0;
*offy = 0;
*offx = 0;
}else{
assert(lcol >= 0);
assert(rcol < ncv->dstwidth);
// we now know topmost row, and left/rightmost through said row. now we must
// find the bottommost row, checking left/rightmost throughout.
int brow;
for(brow = ncv->dstheight - 1 ; brow > trow ; --brow){
int x;
for(x = 0 ; x < ncv->dstwidth ; ++x){
uint32_t rgba = ncv->data[brow * ncv->rowstride / 4 + x];
if(rgba){
if(x < lcol){
lcol = x;
}
int xr;
for(xr = ncv->dstwidth - 1 ; xr > x && xr > rcol ; --xr){
rgba = ncv->data[brow * ncv->rowstride / 4 + xr];
if(rgba){ // rightmost pixel of topmost row
break;
}
}
if(xr > rcol){
rcol = xr;
}
break;
}
}
if(x < ncv->dstwidth){
break;
}
}
// we now know topmost and bottommost row, and left/rightmost within those
// two sections. now check the rest for left and rightmost.
for(int y = trow + 1 ; y < brow ; ++y){
for(int x = 0 ; x < lcol ; ++x){
uint32_t rgba = ncv->data[y * ncv->rowstride / 4 + x];
if(rgba){
lcol = x;
break;
}
}
for(int x = ncv->dstwidth ; x > rcol ; --x){
uint32_t rgba = ncv->data[y * ncv->rowstride / 4 + x];
if(rgba){
rcol = x;
break;
}
}
}
*offy = trow;
*leny = brow - trow + 1;
*offx = lcol;
*lenx = rcol - lcol + 1;
}
return *leny * *lenx;
}
// find the "center" cell of a visual. in the case of even rows/columns, we
// place the center on the top/left. in such a case there will be one more
// cell to the bottom/right of the center.
static inline void
ncvisual_center(const ncvisual* n, int* RESTRICT y, int* RESTRICT x){
*y = n->dstheight;
*x = n->dstwidth;
center_box(y, x);
}
auto ncvisual_rotate(ncvisual* ncv, double rads) -> int {
rads = -rads; // we're a left-handed Cartesian
if(ncv->data == nullptr){
return -1;
}
double stheta, ctheta; // sine, cosine
auto diam = rotate_new_geom(ncv, rads, &stheta, &ctheta);
if(diam <= 0){
return -1;
stheta = sin(rads);
ctheta = cos(rads);
int centy, centx;
ncvisual_center(ncv, &centy, &centx); // pixel center (center of 'data')
// bounding box for real data within the ncvisual. we must only resize to
// accommodate real data, lest we grow without band as we rotate.
// see https://github.com/dankamongmen/notcurses/issues/599.
int bby, bbx, bboffy, bboffx, bbarea;
if((bbarea = ncvisual_bounding_box(ncv, &bby, &bbx, &bboffy, &bboffx)) == 0){
return 0;
}
ncplane* n = ncvisual_plane(ncv);
ncplane* newp = rotate_plane(n); // FIXME how to resize properly?
if(newp == nullptr){
return -1;
}
// pixel diameter
int pdiam = ncv->dstheight > ncv->dstwidth ? ncv->dstheight : ncv->dstwidth;
int bbcentx = bbx, bbcenty = bby;
center_box(&bbcenty, &bbcentx);
//fprintf(stderr, "stride: %d height: %d width: %d\n", ncv->rowstride, ncv->dstheight, ncv->dstwidth);
assert(ncv->rowstride / 4 >= ncv->dstwidth);
auto data = static_cast<uint32_t*>(malloc(pdiam * pdiam * 4));
auto data = static_cast<uint32_t*>(malloc(bbarea * 4));
if(data == nullptr){
ncplane_destroy(newp);
return -1;
}
// targy <- x, targx <- ncv->dstheight - y - 1
int centx = ncv->dstwidth / 2; // pixel center
int centy = ncv->dstheight / 2;
//fprintf(stderr, "DIAM: %d CENTER: %d/%d LEN: %d/%d\n", diam, centy, centx, ncv->ncp->leny, ncv->ncp->lenx);
if(ncplane_resize_simple(ncv->ncp, bby / encoding_vert_scale(ncv), bbx) < 0){
free(data);
return -1;
}
memset(data, 0, bbarea * 4);
//fprintf(stderr, "prad: %d DIAM: %d CENTER: %d/%d LEN: %d/%d\n", prad, diam, centy, centx, ncv->ncp->leny, ncv->ncp->lenx);
for(int y = 0 ; y < ncv->dstheight ; ++y){
for(int x = 0 ; x < ncv->dstwidth ; ++x){
const int convy = y - centy; // converted coordinates
const int convx = x - centx;
const int convy = y - centy; // converted coordinates
const int targx = convx * ctheta - convy * stheta;
const int targy = convx * stheta + convy * ctheta;
const int targx = convx * ctheta + convy * stheta;
const int deconvy = targy + pdiam / 2;
const int deconvx = targx + pdiam / 2;
//fprintf(stderr, "%d/%d -> %d/%d -> %d/%d -> %d/%d\n", y, x, convy, convx, targy, targx, deconvy, deconvx);
assert(deconvy >= 0);
assert(deconvx >= 0);
assert(deconvy < pdiam);
assert(deconvx < pdiam);
data[deconvy * pdiam + deconvx] = ncv->data[y * (ncv->rowstride / 4) + x];
const int deconvx = targx + bbcentx;
const int deconvy = targy + bbcenty;
if(deconvy < 0 || deconvx < 0 || deconvy >= bby || deconvx >= bbx){
//fprintf(stderr, "NOCOPY %d/%d -> %d/%d -> %d/%d -> %d/%d (%dx%d + %dx%d)\n", y, x, convy, convx, targy, targx, deconvy, deconvx, bboffy, bboffx, bby, bbx);
}else{
//fprintf(stderr, "YESCOPY %d/%d (%d) <- (%d) %08x\n", deconvy, deconvx, deconvy * ncv->dstwidth + deconvx, y * (ncv->rowstride / 4) + x, ncv->data[y * (ncv->rowstride / 4) + x]);
data[deconvy * bbx + deconvx] = ncv->data[y * (ncv->rowstride / 4) + x];
}
// data[deconvy * (ncv->dstwidth) + deconvx] = ncv->data[y * (ncv->rowstride / 4) + x];
//fprintf(stderr, "CW: %d/%d (%08x) -> %d/%d (stride: %d)\n", y, x, ncv->data[y * (ncv->rowstride / 4) + x], targy, targx, ncv->rowstride);
// data[targy * ncv->dstheight + targx] = ncv->data[y * (ncv->rowstride / 4) + x];
//fprintf(stderr, "wrote %08x to %d (%d)\n", data[targy * ncv->dstheight + targx], targy * ncv->dstheight + targx, (targy * ncv->dstheight + targx) * 4);
}
}
int ret = ncplane_destroy(n);
ncvisual_set_data(ncv, data, true);
ncv->dstwidth = pdiam;
ncv->dstheight = pdiam;
ncv->rowstride = ncv->dstwidth * 4;
ncv->ncp = newp;
return ret;
ncv->dstwidth = bbx;
ncv->dstheight = bby;
ncv->rowstride = bbx * 4;
ncplane_erase(ncv->ncp);
return 0;
}
auto ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows,

View File

@ -1,3 +1,4 @@
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <clocale>
@ -20,10 +21,14 @@ int main(int argc, char** argv){
if((nc = notcurses_init(&opts, stdout)) == nullptr){
return EXIT_FAILURE;
}
struct ncplane* n = notcurses_stdplane(nc);
struct ncplane* n = ncplane_dup(notcurses_stdplane(nc), nullptr);
if(!n){
notcurses_stop(nc);
return EXIT_FAILURE;
}
int dimx, dimy;
ncplane_dim_yx(n, &dimy, &dimx);
bool failed = false;
nc_err_e ncerr;
auto ncv = ncplane_visual_open(n, file, &ncerr);
if(!ncv){
@ -38,7 +43,22 @@ int main(int argc, char** argv){
if(notcurses_render(nc)){
goto err;
}
return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS;
for(double i = 0 ; i < 256 ; ++i){
if(ncvisual_rotate(ncv, M_PI / 8)){
failed = true;
break;
}
if(ncvisual_render(ncv, 0, 0, -1, -1) < 0){
failed = true;
break;
}
if(notcurses_render(nc)){
failed = true;
break;
}
sleep(1);
}
return notcurses_stop(nc) || failed ? EXIT_FAILURE : EXIT_SUCCESS;
err:
notcurses_stop(nc);

View File

@ -1,10 +1,12 @@
#include "main.h"
#include "internal.h"
TEST_CASE("Geometry") {
if(getenv("TERM") == nullptr){
return;
}
notcurses_options nopts{};
nopts.inhibit_alternate_screen = true;
nopts.suppress_banner = true;
@ -38,6 +40,49 @@ TEST_CASE("Geometry") {
CHECK(0 == cells_double_box(n, 0, 0, &tl, &tr, &bl, &br, &hl, &vl));
CHECK(0 <= ncplane_perimeter(n, &tl, &tr, &bl, &br, &hl, &vl, 0));
CHECK(0 == notcurses_render(nc_));
int y, x;
ncplane_center(n, &y, &x);
CHECK(y == t->centy);
CHECK(x == t->centx);
CHECK(0 == ncplane_destroy(n));
}
}
SUBCASE("CenterAbs") {
const struct test {
int leny, lenx; // geometries
int absy, absx; // location of the origin
int centy, centx; // pre-calculated centers
} tests[] = {
{ 1, 1, 10, 20, 0, 0, },
{ 1, 2, 10, 20, 0, 0, },
{ 3, 1, 10, 20, 1, 0, }, { 1, 3, 10, 20, 0, 1, }, { 2, 3, 10, 20, 0, 1, },
{ 3, 2, 10, 20, 1, 0, }, { 3, 3, 10, 20, 1, 1, },
{ 4, 1, 10, 20, 1, 0, }, { 1, 4, 10, 20, 0, 1, }, { 2, 4, 10, 20, 0, 1, },
{ 4, 2, 10, 20, 0, 1, }, { 3, 4, 10, 20, 1, 1, },
{ 4, 3, 10, 20, 1, 1, }, { 4, 4, 10, 20, 1, 1, }, { 4, 4, 10, 20, 1, 1, },
{ 5, 1, 10, 20, 2, 0, }, { 1, 5, 10, 20, 0, 2, }, { 2, 5, 10, 20, 0, 2, },
{ 5, 2, 10, 20, 2, 1, }, { 3, 5, 10, 20, 1, 2, },
{ 5, 3, 10, 20, 2, 1, }, { 4, 5, 10, 20, 1, 2, }, { 5, 4, 10, 20, 2, 1, },
{ 5, 5, 10, 20, 2, 2, },
{ 0, 0, 10, 20, 0, 0, }
}, *t;
for(t = tests ; !t->leny ; ++t){
auto n = ncplane_new(nc_, t->leny, t->lenx, t->absy, t->absx, nullptr);
REQUIRE(n);
cell tl = CELL_TRIVIAL_INITIALIZER; cell tr = CELL_TRIVIAL_INITIALIZER;
cell bl = CELL_TRIVIAL_INITIALIZER; cell br = CELL_TRIVIAL_INITIALIZER;
cell hl = CELL_TRIVIAL_INITIALIZER; cell vl = CELL_TRIVIAL_INITIALIZER;
CHECK(0 == cells_double_box(n, 0, 0, &tl, &tr, &bl, &br, &hl, &vl));
CHECK(0 <= ncplane_perimeter(n, &tl, &tr, &bl, &br, &hl, &vl, 0));
CHECK(0 == notcurses_render(nc_));
int y, x;
ncplane_center_abs(n, &y, &x);
CHECK(y == t->centy + t->absy);
CHECK(x == t->centx + t->absx);
ncplane_center(n, &y, &x);
CHECK(y == t->centy);
CHECK(x == t->centx);
CHECK(0 == ncplane_destroy(n));
}
}

View File

@ -136,16 +136,16 @@ TEST_CASE("Rotate") {
CHECK(0xffccbb == (channels_bg(channels) & CELL_BG_MASK));
free(c);
}
CHECK(0 == ncvisual_rotate(ncv, M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
ncvisual_destroy(ncv);
@ -177,16 +177,16 @@ TEST_CASE("Rotate") {
CHECK(0xffccbb == (channels_bg(channels) & CELL_BG_MASK));
free(c);
}
CHECK(0 == ncvisual_rotate(ncv, -M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, -M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, -M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncvisual_rotate(ncv, -M_PI/2));
CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
CHECK(0 <= ncvisual_render(ncv, 0, 0, -1, -1));
CHECK(0 == notcurses_render(nc_));
ncvisual_destroy(ncv);