diff --git a/u02/include/star_tool.h b/u02/include/star_tool.h index dbc07a4..2f60bbb 100644 --- a/u02/include/star_tool.h +++ b/u02/include/star_tool.h @@ -2,6 +2,27 @@ #pragma once #include "tool_base.h" +#include +#include + +// Return the points of a regular n-gon +// with the centre at (x, y) +// that is modified in the sense that it takes rf, +// which is used to set the radius for each individual point. +// It is rotated by base_angle, +// where 0 starts drawing at the top. +std::vector> +regular_polygon_mod(int n, std::function rf, int x = 0, int y = 0, + float base_angle = 0); + +// Return the points of a star +// with n spikes, +// the inner radius r1, +// the outer radius r2 +// and the centre (x, y), +// rotated by base_angle. +std::vector> star(int n, float r1, float r2, int x = 0, + int y = 0, float base_angle = 0); class star_tool : public tool_base { public: @@ -10,4 +31,12 @@ public: void draw(int x0, int y0, int x1, int y1); void set_text(std::stringstream &stream); + + void set_spikes(int spikes); + + void set_radius_factor(int radius_factor); + +private: + int spikes = 5; + float radius_factor = 1.0 / 3.0; }; diff --git a/u02/src/star_tool.cpp b/u02/src/star_tool.cpp index 41d5ece..e306784 100644 --- a/u02/src/star_tool.cpp +++ b/u02/src/star_tool.cpp @@ -1,12 +1,74 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "star_tool.h" +#include "bresenham_line_tool.h" #include +#include #include +/* + * The implementation is modeled after one in Haskell + * I created for EMI in the winter semester 2022/2023 + * to output SVGs of regular polygons varied in different ways. + * It can be found here: + * https://git.sbruder.de/simon/emi5/src/branch/master/genstar/Main.hs + */ + +std::vector> +regular_polygon_mod(int n, std::function rf, int x, int y, + float base_angle) { + if (n < 3) { + std::cerr << "Polygons must have at least 3 vertices (regular_polygon_mod " + "was called with n=" + << n << ")." << std::endl; + return {}; + } + std::vector> points = {}; + for (int step = 0; step < n; step++) { + const float angle = base_angle + (step * 2.0 * atan(1) * 4.0) / n; + const float r = rf(step); + // Changed from Haskell implementation: + // It behaves like the unit circle, + // in that it starts at (1, 0) instead of (0, 1). + points.push_back({round(x + r * cosf(angle)), round(y + r * sinf(angle))}); + } + return points; +} + +std::vector> star(int n, float r1, float r2, int x, + int y, float base_angle) { + return regular_polygon_mod( + n * 2, [r1, r2](int i) { return i % 2 == 0 ? r2 : r1; }, x, y, + base_angle); +} + star_tool::star_tool(canvas_buffer &canvas) : tool_base(canvas) { shape = TS_CIRCLE; // TODO: Use star for preview } -void star_tool::draw(int x0, int y0, int x1, int y1) {} +void star_tool::draw(int x0, int y0, int x1, int y1) { + const int r = + round(std::sqrt(std::pow((x1 - x0), 2) + std::pow((y1 - y0), 2))); -void star_tool::set_text(std::stringstream &stream) { stream << "Tool: Star"; } + const float angle = atan2(y1 - y0, x1 - x0); + + bresenham_line_tool *blt = new bresenham_line_tool(canvas); + + const std::vector> points = + star(spikes, radius_factor * r, r, x0, y0, angle); + + for (int i = 1; i <= points.size(); i++) { + blt->draw(round(points[i - 1].first), round(points[i - 1].second), + round(points[i % points.size()].first), + round(points[i % points.size()].second)); + } +} + +void star_tool::set_text(std::stringstream &stream) { + stream << "Tool: Star (" << spikes << " spikes)"; +} + +void star_tool::set_spikes(int spikes) { this->spikes = spikes; } + +void star_tool::set_radius_factor(int radius_factor) { + this->radius_factor = radius_factor; +} diff --git a/u02/src/tests.cpp b/u02/src/tests.cpp index dd77b82..97862b7 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 "star_tool.h" #include "sweep_line_tool.h" #include "util.h" @@ -522,3 +523,45 @@ TEST_CASE("Sweep line (prop: Barycentric coordinates)") { REQUIRE(deviating < abs(y1 - y0) + abs(y2 - y1) + abs(y0 - y2) + abs(x1 - x0) + abs(x2 - x1) + abs(x0 - x2)); } + +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)) { + SKIP("All coordinates have extreme value, skipping (avoid rounding error)"); + } + + 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); +} + +// The Haskell implementation of regular_polygon_mod has many tests, +// but they are not implemented here for brevity.