diff --git a/u02/include/sweep_line_tool.h b/u02/include/sweep_line_tool.h new file mode 100644 index 0000000..7acfa05 --- /dev/null +++ b/u02/include/sweep_line_tool.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#pragma once + +#include "tool_base.h" + +class sweep_line_tool : public tool_base { +public: + sweep_line_tool(canvas_buffer &canvas); + + // Draw example triangle + void draw(); + // Compatibility for main application (only handles draw methods with one or two points) + void draw(int _x, int _y); + // Draw triangle provided by three given points + void draw(int x0, int y0, int x1, int y1, int x2, int y2); + + void set_text(std::stringstream &stream); + +private: + // Draw every pixel on the specified y coordinate, + // in the interval given by the boundaries b1 and b2. + // The boundaries do not need to be sorted. + void draw_interval(int b1, int b2, int y); +}; diff --git a/u02/include/util.h b/u02/include/util.h index 113520d..739f45b 100644 --- a/u02/include/util.h +++ b/u02/include/util.h @@ -38,6 +38,11 @@ std::tuple barycentric_coordinates(int x0, int y0, int x1, int y1, int x2, int y2, int xp, int yp); +// Checks if the point given by (xp, yp) is inside the triangle +// given by the three points (x0, y0), (x1, y1), (x2, y2). +bool point_in_triangle(int x0, int y0, int x1, int y1, int x2, int y2, int xp, + int yp); + // Sorts the points of a triangle to be in ascending order (y0 < y1 < y2). void sort_triangle_points(int &x0, int &y0, int &x1, int &y1, int &x2, int &y2); diff --git a/u02/src/sweep_line_tool.cpp b/u02/src/sweep_line_tool.cpp new file mode 100644 index 0000000..1e3a488 --- /dev/null +++ b/u02/src/sweep_line_tool.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include "sweep_line_tool.h" +#include "dda_line_tool.h" +#include "util.h" +#include +#include +#include + +// Calculate the inverse of the DDA function. +int dda_inv(int x0, int y0, float m, int y) { + // This uses the regular function that is the basis of DDA + // + // y_i = y_0 + m·(x_i - x_0) + // + // but rearranges it to be the inverse function: + // + // y_i = y_0 + m·(x_i - x_0) + // ⇔ y_i - y_0 + x_0·m = x_i·m + // ⇔ x_i = (y_i - y_0)/m + x_0 + // + // This returns a valid x coordinate on the line + // starting from (x0, y0) with the slope m. + + // Handle special case of flat line + if (m == 0) + return x0; + else + return round((y - y0) / m + x0); +} + +sweep_line_tool::sweep_line_tool(canvas_buffer &canvas) : tool_base(canvas) { + shape = TS_NONE; + is_draggable = false; +} + +void sweep_line_tool::draw_interval(int b1, int b2, int y) { + for (int x = std::min(b1, b2); x <= std::max(b1, b2); x++) { + canvas.set_pixel(x, y); + } +} + +void sweep_line_tool::draw() { draw(10, 10, 90, 30, 30, 90); } + +void sweep_line_tool::draw(int _x, int _y) { draw(); } + +void sweep_line_tool::draw(int x0, int y0, int x1, int y1, int x2, int y2) { + // Terminology: + // + // (x0, y0) + // + + // | \ + // | \ m_1 + // |first\ + // | pass \ + // m_shared |---------+ (x1, y1) + // |second / + // |pass / + // | / m_2 + // | / + // + + // (x2, y2) + + // Sort triangle points (in place) so that y0 < y1 < y2 + sort_triangle_points(x0, y0, x1, y1, x2, y2); + + // Slope of the side limiting the first pass (only) + float m_1 = slope(x0, y0, x1, y1); + // Slope of the side limiting the second pass (only) + float m_2 = slope(x1, y1, x2, y2); + // Slope of the side limiting both passes + float m_shared = slope(x0, y0, x2, y2); + + // First pass + if (y0 == y1) { + // If the first two points are on the same height, only draw one line. + // This is only needed for the first interval, + // because in the case that y1 == y2, + // the problematic line would have already been handled in the first pass. + draw_interval(x0, x1, y0); + } else { + for (int y = y0; y <= y1; y++) { + int b1 = dda_inv(x0, y0, m_1, y); + int b2 = dda_inv(x0, y0, m_shared, y); + draw_interval(b1, b2, y); + } + } + + // Second pass + // it can start iterating at y1 + 1, + // because y1 is already included in the first pass. + for (int y = y1 + 1; y <= y2; y++) { + int b1 = dda_inv(x1, y1, m_2, y); + int b2 = dda_inv(x0, y0, m_shared, y); + draw_interval(b1, b2, y); + } +} + +void sweep_line_tool::set_text(std::stringstream &stream) { + stream << "Tool: Sweep-Line"; +} diff --git a/u02/src/tests.cpp b/u02/src/tests.cpp index d3a2197..dd77b82 100644 --- a/u02/src/tests.cpp +++ b/u02/src/tests.cpp @@ -13,6 +13,7 @@ #include "non_recursive_fill_tool.h" #include "rectangle_tool.h" #include "recursive_fill_tool.h" +#include "sweep_line_tool.h" #include "util.h" using Catch::Matchers::WithinRel; @@ -479,3 +480,45 @@ TEST_CASE("Slope") { // Special case: Infinite slope, must be normalized REQUIRE(slope(10, 10, 10, 40) == 0.0); } + +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)) { + // Barycentric coordinates say, point is not in triangle, + // 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)); +} diff --git a/u02/src/util.cpp b/u02/src/util.cpp index 5ed7266..78369d0 100644 --- a/u02/src/util.cpp +++ b/u02/src/util.cpp @@ -160,6 +160,15 @@ std::tuple barycentric_coordinates(int x0, int y0, int x1, return {b1, b2, b3}; } +bool point_in_triangle(int x0, int y0, int x1, int y1, int x2, int y2, int xp, + int yp) { + float b1, b2, b3; + std::tie(b1, b2, b3) = + barycentric_coordinates(x0, y0, x1, y1, x2, y2, xp, yp); + return b1 >= 0.0 && b1 <= 1.0 && b2 >= 0.0 && b2 <= 1.0 && b3 >= 0.0 && + b3 <= 1.0; +} + void sort_triangle_points(int &x0, int &y0, int &x1, int &y1, int &x2, int &y2) { // Bubble sort is not really ideal in general.