2023-05-06 16:37:48 +02:00
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
|
|
|
#include <catch2/generators/catch_generators_adapters.hpp>
|
|
|
|
|
#include <catch2/generators/catch_generators_random.hpp>
|
2023-05-07 16:25:48 +02:00
|
|
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
2023-05-13 21:34:49 +02:00
|
|
|
|
#include <iostream>
|
2023-05-06 16:37:48 +02:00
|
|
|
|
|
2023-05-07 13:25:57 +02:00
|
|
|
|
#include "bresenham_circle_tool.h"
|
2023-05-06 18:41:11 +02:00
|
|
|
|
#include "bresenham_line_tool.h"
|
2023-05-06 17:39:18 +02:00
|
|
|
|
#include "canvas_buffer.h"
|
|
|
|
|
#include "dda_line_tool.h"
|
2023-05-06 22:33:42 +02:00
|
|
|
|
#include "non_recursive_fill_tool.h"
|
2023-05-07 10:03:41 +02:00
|
|
|
|
#include "rectangle_tool.h"
|
2023-05-06 21:09:59 +02:00
|
|
|
|
#include "recursive_fill_tool.h"
|
2023-05-10 20:45:19 +02:00
|
|
|
|
#include "star_tool.h"
|
2023-05-09 22:06:18 +02:00
|
|
|
|
#include "sweep_line_tool.h"
|
2023-05-06 16:37:48 +02:00
|
|
|
|
#include "util.h"
|
|
|
|
|
|
2023-05-13 21:28:40 +02:00
|
|
|
|
using Catch::Matchers::WithinAbs;
|
2023-05-07 16:25:48 +02:00
|
|
|
|
|
2023-05-06 16:37:48 +02:00
|
|
|
|
TEST_CASE("Transform Mirror") {
|
|
|
|
|
// elementary operations
|
|
|
|
|
REQUIRE(transform(TRANSFORM_MIRROR_X, 10, 20) == std::make_pair(10, -20));
|
|
|
|
|
REQUIRE(transform(TRANSFORM_MIRROR_Y, 10, 20) == std::make_pair(-10, 20));
|
|
|
|
|
|
|
|
|
|
// composite operations
|
|
|
|
|
REQUIRE(transform(TRANSFORM_MIRROR_X | TRANSFORM_MIRROR_Y, 10, 20) ==
|
|
|
|
|
std::make_pair(-10, -20));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Transform Rotate") {
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW, 10, 20) ==
|
|
|
|
|
std::make_pair(-20, 10)); // 4th quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW, -20, 10) ==
|
|
|
|
|
std::make_pair(-10, -20)); // 3rd quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW, -10, -20) ==
|
|
|
|
|
std::make_pair(20, -10)); // 2nd quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW, 20, -10) ==
|
|
|
|
|
std::make_pair(10, 20)); // 1st quadrant
|
|
|
|
|
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW, 20, -10) ==
|
|
|
|
|
std::make_pair(-10, -20)); // 1nd quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW, -10, -20) ==
|
|
|
|
|
std::make_pair(-20, 10)); // 2rd quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW, -20, 10) ==
|
|
|
|
|
std::make_pair(10, 20)); // 3th quadrant
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW, 10, 20) ==
|
|
|
|
|
std::make_pair(20, -10)); // 4st quadrant
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Transform Rotate + Mirror") {
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW | TRANSFORM_MIRROR_X, 10, 20) ==
|
|
|
|
|
std::make_pair(-20, -10));
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CW | TRANSFORM_MIRROR_Y, 10, 20) ==
|
|
|
|
|
std::make_pair(20, 10));
|
|
|
|
|
REQUIRE(
|
|
|
|
|
transform(TRANSFORM_ROTATE_CW | TRANSFORM_MIRROR_X | TRANSFORM_MIRROR_Y,
|
|
|
|
|
10, 20) == std::make_pair(20, -10));
|
|
|
|
|
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW | TRANSFORM_MIRROR_X, 10, 20) ==
|
|
|
|
|
std::make_pair(20, 10));
|
|
|
|
|
REQUIRE(transform(TRANSFORM_ROTATE_CCW | TRANSFORM_MIRROR_Y, 10, 20) ==
|
|
|
|
|
std::make_pair(-20, -10));
|
|
|
|
|
REQUIRE(
|
|
|
|
|
transform(TRANSFORM_ROTATE_CCW | TRANSFORM_MIRROR_X | TRANSFORM_MIRROR_Y,
|
|
|
|
|
10, 20) == std::make_pair(-20, 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Transform = Inverse Transform ○ Transform") {
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int x = GENERATE(take(10, random(-100, 100)));
|
|
|
|
|
const int y = GENERATE(take(10, random(-100, 100)));
|
2023-05-06 16:37:48 +02:00
|
|
|
|
// this iterates over all possible transformations,
|
|
|
|
|
// even bogus ones (like rotating cw and ccw)
|
|
|
|
|
for (Transformation transformation = 0; transformation < 0b10000;
|
|
|
|
|
transformation++) {
|
|
|
|
|
int xt, yt;
|
|
|
|
|
std::tie(xt, yt) = transform(transformation, x, y);
|
|
|
|
|
int xti, yti;
|
|
|
|
|
std::tie(xti, yti) = transform_inv(transformation, xt, yt);
|
|
|
|
|
REQUIRE(x == xti);
|
|
|
|
|
REQUIRE(y == yti);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-06 16:40:17 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Transformation to standard case") {
|
|
|
|
|
REQUIRE(transformation_to_standard_case(5, 20, 20, 10) == 0);
|
|
|
|
|
REQUIRE(transformation_to_standard_case(5, 5, 20, 15) == TRANSFORM_MIRROR_X);
|
|
|
|
|
REQUIRE(transformation_to_standard_case(20, 15, 5, 5) == TRANSFORM_MIRROR_Y);
|
|
|
|
|
REQUIRE(transformation_to_standard_case(20, 10, 5, 20) ==
|
|
|
|
|
(TRANSFORM_MIRROR_X | TRANSFORM_MIRROR_Y));
|
|
|
|
|
|
|
|
|
|
REQUIRE(transformation_to_standard_case(5, 20, 15, 5) ==
|
|
|
|
|
(TRANSFORM_ROTATE_CW | TRANSFORM_MIRROR_X));
|
|
|
|
|
REQUIRE(transformation_to_standard_case(5, 5, 15, 20) ==
|
|
|
|
|
TRANSFORM_ROTATE_CCW);
|
|
|
|
|
REQUIRE(transformation_to_standard_case(15, 5, 5, 20) ==
|
|
|
|
|
(TRANSFORM_ROTATE_CCW | TRANSFORM_MIRROR_X));
|
|
|
|
|
REQUIRE(transformation_to_standard_case(15, 20, 5, 5) == TRANSFORM_ROTATE_CW);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Transformation to standard case (prop)") {
|
|
|
|
|
int x0 = GENERATE(take(10, random(-100, 100)));
|
|
|
|
|
int y0 = GENERATE(take(10, random(-100, 100)));
|
|
|
|
|
int x1 = GENERATE(take(10, random(-100, 100)));
|
|
|
|
|
int y1 = GENERATE(take(10, random(-100, 100)));
|
|
|
|
|
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const Transformation transformation =
|
2023-05-06 16:40:17 +02:00
|
|
|
|
transformation_to_standard_case(x0, y0, x1, y1);
|
|
|
|
|
|
|
|
|
|
transform_mut(transformation, x0, y0);
|
|
|
|
|
transform_mut(transformation, x1, y1);
|
|
|
|
|
|
|
|
|
|
REQUIRE(x0 <= x1);
|
|
|
|
|
REQUIRE(y0 >= y1);
|
|
|
|
|
}
|
2023-05-06 17:39:18 +02:00
|
|
|
|
|
2023-05-06 18:41:11 +02:00
|
|
|
|
TEST_CASE("Bresenham/DDA line tool (prop: for every row/column, only one pixel "
|
|
|
|
|
"is set)") {
|
2023-05-06 17:39:18 +02:00
|
|
|
|
const int size = 100;
|
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(size, size);
|
2023-05-06 18:41:11 +02:00
|
|
|
|
|
|
|
|
|
bresenham_line_tool *tool_bresenham = new bresenham_line_tool(*canvas);
|
|
|
|
|
dda_line_tool *tool_dda = new dda_line_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool_base *tool;
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int tool_idx = GENERATE(0, 1);
|
2023-05-06 18:41:11 +02:00
|
|
|
|
switch (tool_idx) {
|
|
|
|
|
case 0:
|
2023-05-20 11:39:26 +02:00
|
|
|
|
default: // required to make static compiler warnings happy
|
2023-05-06 18:41:11 +02:00
|
|
|
|
tool = tool_bresenham;
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
tool = tool_dda;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-05-06 17:39:18 +02:00
|
|
|
|
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int x0 = GENERATE(take(10, random(0, size - 1)));
|
|
|
|
|
const int y0 = GENERATE(take(10, random(0, size - 1)));
|
|
|
|
|
const int x1 = GENERATE(take(10, random(0, size - 1)));
|
|
|
|
|
const int y1 = GENERATE(take(10, random(0, size - 1)));
|
2023-05-06 17:39:18 +02:00
|
|
|
|
|
|
|
|
|
tool->draw(x0, y0, x1, y1);
|
|
|
|
|
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int x_min = std::min(x0, x1);
|
|
|
|
|
const int x_max = std::max(x0, x1);
|
|
|
|
|
const int y_min = std::min(y0, y1);
|
|
|
|
|
const int y_max = std::max(y0, y1);
|
2023-05-06 17:39:18 +02:00
|
|
|
|
|
|
|
|
|
// Depending on what the direction of the line (rounded to the next 90°) is,
|
|
|
|
|
// either every row or column has only one pixel set.
|
|
|
|
|
bool vertical = false;
|
|
|
|
|
int draw_direction_min;
|
|
|
|
|
int draw_direction_max;
|
|
|
|
|
int unique_direction_min;
|
|
|
|
|
int unique_direction_max;
|
|
|
|
|
if (abs(y1 - y0) > abs(x1 - x0)) {
|
|
|
|
|
vertical = true;
|
|
|
|
|
draw_direction_min = y_min;
|
|
|
|
|
draw_direction_max = y_max;
|
|
|
|
|
unique_direction_min = x_min;
|
|
|
|
|
unique_direction_max = x_max;
|
|
|
|
|
} else {
|
|
|
|
|
draw_direction_min = x_min;
|
|
|
|
|
draw_direction_max = x_max;
|
|
|
|
|
unique_direction_min = y_min;
|
|
|
|
|
unique_direction_max = y_max;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-06 18:38:47 +02:00
|
|
|
|
bool all_sums_are_one = true;
|
2023-05-06 17:39:18 +02:00
|
|
|
|
int sum;
|
|
|
|
|
for (int dd = draw_direction_min; dd <= draw_direction_max; dd++) {
|
|
|
|
|
sum = 0;
|
|
|
|
|
for (int ud = unique_direction_min; ud <= unique_direction_max; ud++) {
|
|
|
|
|
int x, y;
|
|
|
|
|
if (vertical) {
|
|
|
|
|
x = ud;
|
|
|
|
|
y = dd;
|
|
|
|
|
} else {
|
|
|
|
|
x = dd;
|
|
|
|
|
y = ud;
|
|
|
|
|
}
|
|
|
|
|
if (canvas->get_pixel(x, y))
|
|
|
|
|
sum++;
|
|
|
|
|
}
|
2023-05-06 18:38:47 +02:00
|
|
|
|
if (sum != 1)
|
|
|
|
|
all_sums_are_one = false;
|
2023-05-06 17:39:18 +02:00
|
|
|
|
}
|
2023-05-06 18:38:47 +02:00
|
|
|
|
|
|
|
|
|
REQUIRE(all_sums_are_one);
|
2023-05-06 17:39:18 +02:00
|
|
|
|
}
|
2023-05-06 21:09:59 +02:00
|
|
|
|
|
2023-05-06 22:56:17 +02:00
|
|
|
|
TEST_CASE("Fill (recursive and non recursive) test shape") {
|
2023-05-06 21:09:59 +02:00
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(100, 100);
|
2023-05-06 22:33:42 +02:00
|
|
|
|
recursive_fill_tool *tool_fill_recursive = new recursive_fill_tool(*canvas);
|
|
|
|
|
non_recursive_fill_tool *tool_fill_non_recursive =
|
|
|
|
|
new non_recursive_fill_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool_base *tool_fill;
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int tool_fill_idx = GENERATE(0, 1);
|
2023-05-06 22:33:42 +02:00
|
|
|
|
switch (tool_fill_idx) {
|
|
|
|
|
case 0:
|
2023-05-20 11:39:26 +02:00
|
|
|
|
default: // required to make static compiler warnings happy
|
2023-05-06 22:33:42 +02:00
|
|
|
|
tool_fill = tool_fill_recursive;
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
tool_fill = tool_fill_non_recursive;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-05-06 21:09:59 +02:00
|
|
|
|
|
2023-05-06 22:56:17 +02:00
|
|
|
|
canvas->draw_test_shape();
|
|
|
|
|
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(50, 49));
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(50, 25));
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(50, 75));
|
|
|
|
|
tool_fill->draw(50, 25);
|
|
|
|
|
REQUIRE(canvas->get_pixel(50, 49));
|
|
|
|
|
REQUIRE(canvas->get_pixel(50, 25));
|
|
|
|
|
REQUIRE(canvas->get_pixel(50, 75));
|
|
|
|
|
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(75, 40));
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(75, 60));
|
|
|
|
|
tool_fill->draw(75, 50);
|
|
|
|
|
REQUIRE(canvas->get_pixel(75, 40));
|
|
|
|
|
REQUIRE(canvas->get_pixel(75, 60));
|
|
|
|
|
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(0, 0));
|
|
|
|
|
REQUIRE_FALSE(canvas->get_pixel(99, 99));
|
|
|
|
|
tool_fill->draw(25, 50);
|
|
|
|
|
REQUIRE(canvas->get_pixel(0, 0));
|
|
|
|
|
REQUIRE(canvas->get_pixel(99, 99));
|
2023-05-06 21:09:59 +02:00
|
|
|
|
}
|
2023-05-06 22:47:04 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Fill recursive == Fill non recursive (prop, 5 random lines)") {
|
|
|
|
|
const int size = 100;
|
|
|
|
|
|
|
|
|
|
canvas_buffer *canvas_recursive = new canvas_buffer(size, size);
|
|
|
|
|
canvas_buffer *canvas_non_recursive = new canvas_buffer(size, size);
|
|
|
|
|
|
|
|
|
|
bresenham_line_tool *tool_line_recursive =
|
|
|
|
|
new bresenham_line_tool(*canvas_recursive);
|
|
|
|
|
bresenham_line_tool *tool_line_non_recursive =
|
|
|
|
|
new bresenham_line_tool(*canvas_non_recursive);
|
|
|
|
|
|
|
|
|
|
recursive_fill_tool *tool_fill_recursive =
|
|
|
|
|
new recursive_fill_tool(*canvas_recursive);
|
|
|
|
|
non_recursive_fill_tool *tool_fill_non_recursive =
|
|
|
|
|
new non_recursive_fill_tool(*canvas_non_recursive);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int x0 = GENERATE(take(1, random(0, size - 1)));
|
|
|
|
|
const int y0 = GENERATE(take(1, random(0, size - 1)));
|
|
|
|
|
const int x1 = GENERATE(take(1, random(0, size - 1)));
|
|
|
|
|
const int y1 = GENERATE(take(1, random(0, size - 1)));
|
2023-05-06 22:47:04 +02:00
|
|
|
|
tool_line_recursive->draw(x0, y0, x1, y1);
|
|
|
|
|
tool_line_non_recursive->draw(x0, y0, x1, y1);
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-07 10:05:43 +02:00
|
|
|
|
const int x = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
const int y = GENERATE(take(3, random(0, size - 1)));
|
2023-05-06 22:47:04 +02:00
|
|
|
|
tool_fill_recursive->draw(x, y);
|
|
|
|
|
tool_fill_non_recursive->draw(x, y);
|
|
|
|
|
|
|
|
|
|
bool equal = true;
|
|
|
|
|
for (int x = 0; x < size; x++) {
|
|
|
|
|
for (int y = 0; y < size; y++) {
|
|
|
|
|
if (canvas_recursive->get_pixel(x, y) !=
|
|
|
|
|
canvas_non_recursive->get_pixel(x, y))
|
|
|
|
|
equal = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(equal);
|
|
|
|
|
}
|
2023-05-07 10:03:41 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Rectangle (prop)") {
|
|
|
|
|
const int size = 100;
|
|
|
|
|
|
|
|
|
|
const int x0 = GENERATE(take(5, random(0, size - 1)));
|
|
|
|
|
const int y0 = GENERATE(take(5, random(0, size - 1)));
|
|
|
|
|
const int x1 = GENERATE(take(5, random(0, size - 1)));
|
|
|
|
|
const int y1 = GENERATE(take(5, random(0, size - 1)));
|
|
|
|
|
|
|
|
|
|
const int x_min = std::min(x0, x1);
|
|
|
|
|
const int x_max = std::max(x0, x1);
|
|
|
|
|
const int y_min = std::min(y0, y1);
|
|
|
|
|
const int y_max = std::max(y0, y1);
|
|
|
|
|
|
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(size, size);
|
|
|
|
|
rectangle_tool *tool = new rectangle_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool->draw(x0, y0, x1, y1);
|
|
|
|
|
|
|
|
|
|
bool pass = true;
|
|
|
|
|
for (int x = 0; x < size; x++) {
|
|
|
|
|
for (int y = 0; y < size; y++) {
|
|
|
|
|
if (((x == x0 || x == x1) && (y >= y_min && y <= y_max)) ||
|
|
|
|
|
((y == y0 || y == y1) && (x >= x_min && x <= x_max))) {
|
|
|
|
|
if (!canvas->get_pixel(x, y))
|
|
|
|
|
pass = false;
|
|
|
|
|
} else {
|
|
|
|
|
if (canvas->get_pixel(x, y))
|
|
|
|
|
pass = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(pass);
|
|
|
|
|
}
|
2023-05-07 13:25:57 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Bresenham circle (prop: √(x²+y²)-r<ε)") {
|
|
|
|
|
// Let s be the size of the canvas (s,s).
|
|
|
|
|
// Let m be the smallest coordinate (x and y) for random points
|
|
|
|
|
// and M be the largest coordinate (x and y) for random points.
|
|
|
|
|
//
|
|
|
|
|
// The largest radius that can fit on canvas (for arbitrary centres) is m.
|
|
|
|
|
// The largest radius that is possible to create is √(2)(M-m).
|
|
|
|
|
//
|
|
|
|
|
// ⇒ √(2)(M-m) ≥ m
|
|
|
|
|
// ⇔ √(2)M-√(2)m ≥ m
|
|
|
|
|
// ⇔ √(2)M ≥ (1+√(2))m
|
|
|
|
|
// ⇔ m ≤ (√(2)/(1+√(2)))m
|
|
|
|
|
// ⇔ m ≤ (2-√(2))M (1)
|
|
|
|
|
//
|
|
|
|
|
// Additionally, to have a centered point field,
|
|
|
|
|
// s-M=m must hold
|
|
|
|
|
//
|
|
|
|
|
// s-M = m
|
|
|
|
|
// ⇒ s-M ≤ (2-√(2))M
|
|
|
|
|
// ⇔ s ≤ (3-√(2))M
|
|
|
|
|
// ⇔ M ≥ s/(3-√(2)) (2)
|
|
|
|
|
//
|
|
|
|
|
// With this, it now is possible to express M and m in terms of s:
|
|
|
|
|
//
|
|
|
|
|
// m ≤ (2-√(2))M (1)
|
|
|
|
|
// ⇔ m ≤ ((2-√(2))/(3-√(2)))s
|
|
|
|
|
// ⇔ m ≤ ((4-√(2))/7)s (3)
|
|
|
|
|
//
|
|
|
|
|
// When the points are rounded to the nearest integer,
|
|
|
|
|
// M must be rounded down and m rounded down.
|
2023-05-07 14:02:45 +02:00
|
|
|
|
//
|
|
|
|
|
// An interactive version of this can be found here:
|
|
|
|
|
// https://www.desmos.com/calculator/kn19qhue20
|
2023-05-07 13:25:57 +02:00
|
|
|
|
const int size = 100; // s
|
|
|
|
|
const int max_c = std::floor(size / (3 - std::sqrt(2))); // M (2)
|
|
|
|
|
const int min_c = std::ceil(((4 - std::sqrt(2)) / 7) * size); // m (3)
|
|
|
|
|
|
|
|
|
|
const int x0 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int y0 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int x1 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int y1 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
|
|
|
|
|
if ((x0 == min_c || x0 == max_c) && (x1 == min_c || x1 == max_c) &&
|
|
|
|
|
(y0 == min_c || y0 == max_c) && (y1 == min_c || y1 == max_c)) {
|
2023-05-13 21:34:49 +02:00
|
|
|
|
// catch2’s SKIP macro does not exactly do what I want here,
|
|
|
|
|
// so this just returns from the function and prints a message.
|
|
|
|
|
std::cerr
|
|
|
|
|
<< "All coordinates have extreme value, skipping (avoid rounding error)"
|
|
|
|
|
<< std::endl;
|
|
|
|
|
return;
|
2023-05-07 13:25:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 20:47:08 +02:00
|
|
|
|
const float r =
|
2023-05-07 13:25:57 +02:00
|
|
|
|
round(std::sqrt(std::pow((x1 - x0), 2) + std::pow((y1 - y0), 2)));
|
|
|
|
|
|
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(size, size);
|
|
|
|
|
bresenham_circle_tool *tool = new bresenham_circle_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool->draw(x0, y0, x1, y1);
|
|
|
|
|
|
|
|
|
|
bool pass = true;
|
|
|
|
|
for (int x = 0; x < size; x++) {
|
|
|
|
|
for (int y = 0; y < size; y++) {
|
2023-05-10 20:47:08 +02:00
|
|
|
|
float distance =
|
2023-05-07 13:25:57 +02:00
|
|
|
|
std::abs(std::sqrt(std::pow(x0 - x, 2) + std::pow(y0 - y, 2)) - r);
|
|
|
|
|
// Because of rounding errors, an exact test (for all pixels) is not
|
|
|
|
|
// feasible.
|
|
|
|
|
// Therefore, it is only tested if set pixels have a distance <= 1.
|
|
|
|
|
if (canvas->get_pixel(x, y) && distance > 1) {
|
|
|
|
|
pass = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(pass);
|
|
|
|
|
}
|
2023-05-07 16:25:48 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Barycentric coordinates: Edge cases") {
|
|
|
|
|
int x0 = 0, y0 = 0, x1 = 0, y1 = 10, x2 = 10, y2 = 0;
|
|
|
|
|
float b1, b2, b3;
|
|
|
|
|
|
|
|
|
|
// point on vertex
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 0, 0);
|
|
|
|
|
REQUIRE(b1 == 1);
|
|
|
|
|
REQUIRE(b2 == 0);
|
|
|
|
|
REQUIRE(b3 == 0);
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 0, 10);
|
|
|
|
|
REQUIRE(b1 == 0);
|
|
|
|
|
REQUIRE(b2 == 1);
|
|
|
|
|
REQUIRE(b3 == 0);
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 10, 0);
|
|
|
|
|
REQUIRE(b1 == 0);
|
|
|
|
|
REQUIRE(b2 == 0);
|
|
|
|
|
REQUIRE(b3 == 1);
|
|
|
|
|
|
|
|
|
|
// point on edge
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 0, 5);
|
|
|
|
|
REQUIRE(b1 == 0.5);
|
|
|
|
|
REQUIRE(b2 == 0.5);
|
|
|
|
|
REQUIRE(b3 == 0);
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 5, 0);
|
|
|
|
|
REQUIRE(b1 == 0.5);
|
|
|
|
|
REQUIRE(b2 == 0);
|
|
|
|
|
REQUIRE(b3 == 0.5);
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, 5, 5);
|
|
|
|
|
REQUIRE(b1 == 0);
|
|
|
|
|
REQUIRE(b2 == 0.5);
|
|
|
|
|
REQUIRE(b3 == 0.5);
|
2023-05-09 22:02:59 +02:00
|
|
|
|
|
|
|
|
|
// All points on straight line
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(0, y0, 0, y1, 0, y2, 0, 0);
|
|
|
|
|
REQUIRE(std::isnan(b1));
|
|
|
|
|
REQUIRE(std::isnan(b2));
|
|
|
|
|
REQUIRE(std::isnan(b3));
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, 0, x1, 0, x2, 0, 0, 0);
|
|
|
|
|
REQUIRE(std::isnan(b1));
|
|
|
|
|
REQUIRE(std::isnan(b2));
|
|
|
|
|
REQUIRE(std::isnan(b3));
|
2023-05-07 16:25:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Barycentric coordinates (prop: Σ = 1)") {
|
|
|
|
|
int x0 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
int y0 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
int x1 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
int y1 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
int x2 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
int y2 = GENERATE(take(2, random(-100, 100)));
|
|
|
|
|
|
2023-05-13 22:06:37 +02:00
|
|
|
|
// If all points are on a straight line, the property does not hold.
|
|
|
|
|
// Checking this is equivalent to checking if the area of the triangle is 0.
|
|
|
|
|
if ((x0 - x1) * (y0 - y2) - (y0 - y1) * (x0 - x2) == 0) {
|
|
|
|
|
// see above
|
|
|
|
|
std::cerr << "Points do not form reasonable triangle, skipping"
|
|
|
|
|
<< std::endl;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-07 16:25:48 +02:00
|
|
|
|
int x = GENERATE(take(5, random(-100, 100)));
|
|
|
|
|
int y = GENERATE(take(5, random(-100, 100)));
|
|
|
|
|
float b1, b2, b3;
|
|
|
|
|
|
|
|
|
|
std::tie(b1, b2, b3) = barycentric_coordinates(x0, y0, x1, y1, x2, y2, x, y);
|
|
|
|
|
|
2023-05-13 22:06:37 +02:00
|
|
|
|
REQUIRE_THAT(b1 + b2 + b3, WithinAbs(1.0, 0.01));
|
2023-05-07 16:25:48 +02:00
|
|
|
|
}
|
2023-05-07 19:24:48 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Sort triangle points") {
|
|
|
|
|
int x0 = 10, y0 = 60, x1 = 50, y1 = 90, x2 = 40, y2 = 30;
|
|
|
|
|
sort_triangle_points(x0, y0, x1, y1, x2, y2);
|
|
|
|
|
REQUIRE(x0 == 40);
|
|
|
|
|
REQUIRE(y0 == 30);
|
|
|
|
|
REQUIRE(x1 == 10);
|
|
|
|
|
REQUIRE(y1 == 60);
|
|
|
|
|
REQUIRE(x2 == 50);
|
|
|
|
|
REQUIRE(y2 == 90);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE("Sort triangle points (prop: y0 < y1 < y2)") {
|
|
|
|
|
int x0 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
int y0 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
int x1 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
int y1 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
int x2 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
int y2 = GENERATE(take(3, random(-100, 100)));
|
|
|
|
|
|
|
|
|
|
sort_triangle_points(x0, y0, x1, y1, x2, y2);
|
|
|
|
|
|
|
|
|
|
REQUIRE(y0 <= y1);
|
|
|
|
|
REQUIRE(y1 <= y2);
|
|
|
|
|
}
|
2023-05-09 20:39:24 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Slope") {
|
|
|
|
|
REQUIRE(slope(5, 10, 20, 10) == 0.0);
|
|
|
|
|
REQUIRE(slope(0, 0, 10, 10) == 1.0);
|
|
|
|
|
REQUIRE(slope(0, 0, 10, -10) == -1.0);
|
|
|
|
|
REQUIRE(slope(0, 0, 10, 5) == 0.5);
|
|
|
|
|
REQUIRE(slope(0, 0, 10, -5) == -0.5);
|
|
|
|
|
REQUIRE(slope(0, 10, 10, 40) == 3.0);
|
|
|
|
|
REQUIRE(slope(0, 10, 10, -40) == -5.0);
|
|
|
|
|
|
|
|
|
|
// Special case: Infinite slope, must be normalized
|
|
|
|
|
REQUIRE(slope(10, 10, 10, 40) == 0.0);
|
|
|
|
|
}
|
2023-05-09 22:06:18 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Sweep line (prop: Barycentric coordinates)") {
|
|
|
|
|
const int size = 100;
|
|
|
|
|
int x0 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
int y0 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
int x1 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
int y1 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
int x2 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
int y2 = GENERATE(take(3, random(0, size - 1)));
|
|
|
|
|
|
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(size, size);
|
|
|
|
|
sweep_line_tool *tool = new sweep_line_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool->draw(x0, y0, x1, y1, x2, y2);
|
|
|
|
|
|
|
|
|
|
int deviating = 0;
|
|
|
|
|
bool pass = true;
|
|
|
|
|
for (int x = 0; x < size; x++) {
|
|
|
|
|
for (int y = 0; y < size; y++) {
|
|
|
|
|
if (point_in_triangle(x0, y0, x1, y1, x2, y2, x, y)) {
|
|
|
|
|
if (!canvas->get_pixel(x, y)) {
|
2023-05-13 22:55:17 +02:00
|
|
|
|
// Barycentric coordinates say, point is in triangle,
|
2023-05-09 22:06:18 +02:00
|
|
|
|
// but point is not set.
|
|
|
|
|
// This must not happen → fail test.
|
|
|
|
|
pass = false;
|
|
|
|
|
}
|
|
|
|
|
} else if (canvas->get_pixel(x, y)) {
|
|
|
|
|
// Barycentric coordinates say, point is not in triangle,
|
|
|
|
|
// but point is set.
|
|
|
|
|
// The point is most likely on edge → mark it as deviating.
|
|
|
|
|
deviating++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(pass);
|
|
|
|
|
// Crude heuristic:
|
|
|
|
|
// No more than differences of all edge point coordinates can deviate.
|
|
|
|
|
// This ist not accurate (false negatives possible) on small/spiky triangles,
|
|
|
|
|
// but overall it gives an okayish result.
|
|
|
|
|
REQUIRE(deviating < abs(y1 - y0) + abs(y2 - y1) + abs(y0 - y2) +
|
|
|
|
|
abs(x1 - x0) + abs(x2 - x1) + abs(x0 - x2));
|
|
|
|
|
}
|
2023-05-10 20:45:19 +02:00
|
|
|
|
|
|
|
|
|
TEST_CASE("Star (prop: all points inside circle)") {
|
|
|
|
|
// See test “Bresenham circle (prop: √(x²+y²)-r<ε)” for what this does.
|
|
|
|
|
const int size = 100;
|
|
|
|
|
const int max_c = std::floor(size / (3 - std::sqrt(2)));
|
|
|
|
|
const int min_c = std::ceil(((4 - std::sqrt(2)) / 7) * size);
|
|
|
|
|
|
|
|
|
|
const int x0 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int y0 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int x1 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
const int y1 = GENERATE_COPY(take(10, random(min_c, max_c)));
|
|
|
|
|
|
|
|
|
|
if ((x0 == min_c || x0 == max_c) && (x1 == min_c || x1 == max_c) &&
|
|
|
|
|
(y0 == min_c || y0 == max_c) && (y1 == min_c || y1 == max_c)) {
|
2023-05-13 21:34:49 +02:00
|
|
|
|
// see above
|
|
|
|
|
std::cerr
|
|
|
|
|
<< "All coordinates have extreme value, skipping (avoid rounding error)"
|
|
|
|
|
<< std::endl;
|
|
|
|
|
return;
|
2023-05-10 20:45:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float r = std::sqrt(std::pow((x1 - x0), 2) + std::pow((y1 - y0), 2));
|
|
|
|
|
|
|
|
|
|
// correction factor for very small stars
|
|
|
|
|
const float correction = r < 2 ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
canvas_buffer *canvas = new canvas_buffer(size, size);
|
|
|
|
|
star_tool *tool = new star_tool(*canvas);
|
|
|
|
|
|
|
|
|
|
tool->draw(x0, y0, x1, y1);
|
|
|
|
|
|
|
|
|
|
bool pass = true;
|
|
|
|
|
for (int x = 0; x < size; x++) {
|
|
|
|
|
for (int y = 0; y < size; y++) {
|
|
|
|
|
if (canvas->get_pixel(x, y) &&
|
|
|
|
|
(std::sqrt(std::pow(x0 - x, 2) + std::pow(y0 - y, 2)) >
|
|
|
|
|
r * r + correction)) {
|
|
|
|
|
pass = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(pass);
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 22:01:15 +02:00
|
|
|
|
TEST_CASE("Star (prop: average of all points is centre)") {
|
|
|
|
|
const int x0 = GENERATE(take(5, random(-100, 100)));
|
|
|
|
|
const int y0 = GENERATE(take(5, random(-100, 100)));
|
|
|
|
|
const int n = GENERATE(take(5, random(3, 1000)));
|
|
|
|
|
const float r2 = GENERATE(take(5, random(0, 100)));
|
|
|
|
|
const float r_factor = GENERATE(take(5, random(0, 1)));
|
|
|
|
|
const float r1 = r_factor * r2;
|
|
|
|
|
const float angle = GENERATE(take(5, random(-16.0 * atan(1), 16 * atan(1))));
|
|
|
|
|
|
|
|
|
|
std::vector<std::pair<float, float>> points = star(n, r1, r2, x0, y0, angle);
|
|
|
|
|
|
|
|
|
|
float x = 0, y = 0;
|
|
|
|
|
for (std::pair<float, float> point : points) {
|
|
|
|
|
x += point.first;
|
|
|
|
|
y += point.second;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-13 21:28:40 +02:00
|
|
|
|
REQUIRE_THAT(x / points.size(), WithinAbs(x0, 0.5));
|
|
|
|
|
REQUIRE_THAT(y / points.size(), WithinAbs(y0, 0.5));
|
2023-05-10 22:01:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The Haskell implementation of regular_polygon_mod has many more tests,
|
2023-05-10 20:45:19 +02:00
|
|
|
|
// but they are not implemented here for brevity.
|