Compare commits

...

13 Commits

Author SHA1 Message Date
Simon Bruder e64cbfdcf2 u02: Make star preview more variable 2023-05-16 16:49:59 +02:00
Simon Bruder 18da3567cc u02/star_tool: Allow setting spikes in ctor 2023-05-16 16:49:23 +02:00
Simon Bruder f482d0a951 flake: Use correct input types for dependencies 2023-05-16 16:49:23 +02:00
Simon Bruder 1aae98c18b u02: Add readme 2023-05-16 16:49:23 +02:00
Simon Bruder 2290a6314d u02/tests: Fix inverted logic in comment 2023-05-16 16:49:22 +02:00
Simon Bruder c5289609e5 flake: Build variants without tests
Tests set the C++ version to 17, however only up to 11 is allowed in the
other files.
2023-05-16 16:49:22 +02:00
Simon Bruder 085b8fcdb9 u02/tests: Fix edge case for invalid points 2023-05-16 16:49:22 +02:00
Simon Bruder 8f784ccf3e u02/tests: Fix skipping behaviour
When the test runner only tests one case, it exits with a non-zero exit
code should that test invoke a skip (even if there are 1000+ successful
iterations).

This fixes this by falling back to a cheap message to stderr.
2023-05-16 16:49:22 +02:00
Simon Bruder ad02a06db4 u02/tests: Fix floating point comparison
I didn’t really understand what WithinRel and WithinAbs do. Now I know
that for this use case WithinAbs is the better choice.

This also increases the allowed deviance for the average point of all
points of a star, because this would now fail for larger values.
2023-05-16 16:49:22 +02:00
Simon Bruder d4794e1544 u02/tests: Add average point test for star 2023-05-16 16:49:22 +02:00
Simon Bruder 583f2ecd3e u02: Implement preview for star
It is fixed to a star with 5 spikes, but better than a circle.
2023-05-16 16:49:22 +02:00
Simon Bruder 85e38a358d u02/tests: Use floats for r/d in circle prop test
It does not improve the test’s accuracy, but it seems weird to first
convert the radius to an int and then save the distance as a double.
2023-05-16 16:49:22 +02:00
Simon Bruder 2226fb32af u02: Implement star tool 2023-05-16 16:49:22 +02:00
5 changed files with 294 additions and 16 deletions

View File

@ -7,6 +7,10 @@
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
mkNocheck = drv: drv.overrideAttrs (o: {
doCheck = false;
checkInputs = [ ];
});
in
{
packages = rec {
@ -16,7 +20,8 @@
src = ./u01;
nativeBuildInputs = [ catch2_3 cmake ];
nativeBuildInputs = [ cmake ];
checkInputs = [ catch2_3 ];
doCheck = true;
})
@ -28,11 +33,16 @@
src = ./u02;
nativeBuildInputs = [ catch2_3 cmake freeglut libGL libGLU ];
nativeBuildInputs = [ cmake ];
buildInputs = [ freeglut libGL libGLU ];
checkInputs = [ catch2_3 ];
doCheck = true;
})
{ };
u01-nocheck = mkNocheck u01;
u02-nocheck = mkNocheck u02;
};
devShells.default = pkgs.mkShell {

View File

@ -2,12 +2,45 @@
#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:
star_tool(canvas_buffer &canvas);
// Initialize a new star tool.
// It can draw stars with arbitrary number of spikes (however, at least 2).
// The preview only supports spikes from 3 to 6 and falls back to 5
// if a spike outside of that range is given.
star_tool(canvas_buffer &canvas, int spikes = 5);
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;
float radius_factor = 1.0 / 3.0;
};

75
u02/readme.txt Normal file
View File

@ -0,0 +1,75 @@
<!-- vim: set ft=markdown: -->
<!-- LTeX: language=de-DE -->
<!-- SPDX-License-Identifier: LGPL-3.0-or-later -->
# Praxisaufgabe 2 Einführung in die Computergrafik
## Team
* Simon Bruder, Matrikelnummer: 5075324
## Bearbeitete Zusatzaufgaben
* Rechteck-Werkzeug (`rectangle_tool.cpp`)
* Kreisrasterisierer (`bresenham_circle_tool.cpp`)
* Rasterisierer für Sternform (`star_tool.cpp`)
* Sweepline-Algorithmus (`sweep_line_tool.cpp`)
## Hinweise
### Projektaufbau
Die vorgegebene Ordnerstruktur wurde beibehalten.
Es wurde jedoch die `CMakeLists.txt`-Datei in das Wurzelverzeichnis verschoben,
um einen üblichen Aufbau des Projektes zu erhalten,
und es wurden die nicht benötigten Verzeichnisse (`build`, `data`, `dependencies`, `src_solution`) entfernt,
um einen Stand zu erhalten, der tauglich für Versionskontrolle ist.
Das Projekt kann (abweichend von der Ausgangskonfiguration)
mit folgenden (für CMake-Projekte übliche) Befehlen gebaut werden:
```bash
# in `u02`
mkdir -p build
cd build
cmake ..
make -j$(nproc)
```
Für Tests der Implementation wurde [Catch2](https://github.com/catchorg/Catch2) eingebunden,
was jedoch optional ist und bei Nichtvorhandensein lediglich eine Nachricht beim Aufruf von CMake ausgibt,
welche aber keinen Fehler darstellt.
Zu einer Auslagerung von grundlegenden Funktionalitäten,
die nicht einem bestimmten Werkzeug zuzuordnen sind,
wurde die Hilfsdatei `util.cpp` (und der zugehörige Header `util.h`) angelegt,
welche in CMake eingebunden ist.
### Sternrasterisierer
Der Sternrasterisierer ermöglicht theoretisch
die Rasterisierung von Sternen mit beliebigen Zackenanzahlen
(wobei ein hartes Limit von mindestens 2 Zacken besteht,
jedoch erst ab 3 eine Art Stern vorliegt).
In dem Beispielprogramm kann jedoch aus Komplexitätsgründen nur aus bestimmten Zackenanzahlen gewählt werden,
was jedoch per Kontextmenü möglich ist und damit der Aufgabenstellung entspricht.
Darüber hinaus wurde rudimentär eine Vorschau der Sternform hinzugefügt.
Da die vorgegebene Architektur keine Spezialisierungen der Formen eines Tools erlaubt,
und C++ (anders als bspw. Rust) keine Tuple/Struct enums zulässt,
wurden hier nur die über die Benutzerschnittstelle verfügbaren Zackenanzahlen fest implementiert.
### Sweepline-Algorithmus
Der Sweepline-Algorithmus, der im Programm aufrufbar ist,
hat für ein Beispiel-Dreieck 3 Eckpunkte fest im Code definiert.
Er ist jedoch allgemein ausgelegt und kann mit drei beliebigen Punkten aufgerufen werden,
die ein valides Dreieck bilden.
Dafür wurde `tool_base` so modifiziert,
dass es Werkzeugen möglich ist,
eine `draw`-Methode mit keinem (für die fest definierten Eckpunkte)
oder drei Punkten (für beliebige Dreiecke)
anzubieten.
Damit das Werkzeug kompatibel mit der Architektur des Hauptprogramms ist,
bietet es jedoch zusätzlich die `draw`-Methode mit zwei Punkten an,
welche jedoch die Punkte ignoriert und die `draw`-Methode ohne Punkte aufruft.

View File

@ -1,12 +1,92 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "star_tool.h"
#include "bresenham_line_tool.h"
#include <cmath>
#include <iostream>
#include <util.h>
star_tool::star_tool(canvas_buffer &canvas) : tool_base(canvas) {
shape = TS_CIRCLE; // TODO: Use star for preview
/*
* 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;
}
void star_tool::draw(int x0, int y0, int x1, int y1) {}
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);
}
void star_tool::set_text(std::stringstream &stream) { stream << "Tool: Star"; }
star_tool::star_tool(canvas_buffer &canvas, int spikes) : tool_base(canvas) {
set_spikes(spikes);
}
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)));
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;
switch (spikes) {
case 3:
shape = TS_STAR_3;
break;
case 4:
shape = TS_STAR_4;
break;
// 5 is default, handled at the end
case 6:
shape = TS_STAR_6;
break;
case 5:
default:
shape = TS_STAR_5;
break;
}
}
void star_tool::set_radius_factor(int radius_factor) {
this->radius_factor = radius_factor;
}

View File

@ -5,6 +5,7 @@
#include <catch2/generators/catch_generators_adapters.hpp>
#include <catch2/generators/catch_generators_random.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <iostream>
#include "bresenham_circle_tool.h"
#include "bresenham_line_tool.h"
@ -13,10 +14,11 @@
#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"
using Catch::Matchers::WithinRel;
using Catch::Matchers::WithinAbs;
TEST_CASE("Transform Mirror") {
// elementary operations
@ -348,10 +350,15 @@ TEST_CASE("Bresenham circle (prop: √(x²+y²)-r<ε)") {
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)");
// catch2s 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;
}
const int r =
const float r =
round(std::sqrt(std::pow((x1 - x0), 2) + std::pow((y1 - y0), 2)));
canvas_buffer *canvas = new canvas_buffer(size, size);
@ -362,7 +369,7 @@ TEST_CASE("Bresenham circle (prop: √(x²+y²)-r<ε)") {
bool pass = true;
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
double distance =
float distance =
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.
@ -431,16 +438,22 @@ TEST_CASE("Barycentric coordinates (prop: Σ = 1)") {
int x2 = GENERATE(take(2, random(-100, 100)));
int y2 = GENERATE(take(2, random(-100, 100)));
// 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;
}
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);
// If all points are on a straight line, the property does not hold
if (!(x0 == x1 && x1 == x2) && !(y0 == y1 && y1 == y2)) {
REQUIRE_THAT(b1 + b2 + b3, WithinRel(1.0, 0.01));
}
REQUIRE_THAT(b1 + b2 + b3, WithinAbs(1.0, 0.01));
}
TEST_CASE("Sort triangle points") {
@ -501,7 +514,7 @@ TEST_CASE("Sweep line (prop: Barycentric coordinates)") {
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,
// Barycentric coordinates say, point is in triangle,
// but point is not set.
// This must not happen → fail test.
pass = false;
@ -522,3 +535,70 @@ 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)) {
// see above
std::cerr
<< "All coordinates have extreme value, skipping (avoid rounding error)"
<< std::endl;
return;
}
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);
}
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;
}
REQUIRE_THAT(x / points.size(), WithinAbs(x0, 0.5));
REQUIRE_THAT(y / points.size(), WithinAbs(y0, 0.5));
}
// The Haskell implementation of regular_polygon_mod has many more tests,
// but they are not implemented here for brevity.