u02: Implement star tool

This commit is contained in:
Simon Bruder 2023-05-10 20:45:19 +02:00
parent 7e06373aa1
commit 2226fb32af
3 changed files with 136 additions and 2 deletions

View file

@ -2,6 +2,27 @@
#pragma once
#include "tool_base.h"
#include <functional>
#include <vector>
// 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<std::pair<float, float>>
regular_polygon_mod(int n, std::function<float(int)> 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<std::pair<float, float>> 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;
};

View file

@ -1,12 +1,74 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "star_tool.h"
#include "bresenham_line_tool.h"
#include <cmath>
#include <iostream>
#include <util.h>
/*
* 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<std::pair<float, float>>
regular_polygon_mod(int n, std::function<float(int)> 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<std::pair<float, float>> 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<std::pair<float, float>> 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<std::pair<float, float>> 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;
}

View file

@ -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.