356 lines
10 KiB
C++
356 lines
10 KiB
C++
//
|
|
// Created by natty on 15.7.23.
|
|
//
|
|
|
|
#include "renderwindow.h"
|
|
#include "svgpathextractor.h"
|
|
|
|
#include <iostream>
|
|
#include <QPushButton>
|
|
#include <random>
|
|
#include <QComboBox>
|
|
#include <QLabel>
|
|
#include <QTimer>
|
|
#include <QMenu>
|
|
#include <QMenuBar>
|
|
#include <QFileDialog>
|
|
#include <fstream>
|
|
#include <ranges>
|
|
|
|
std::unique_ptr<Strategy> strategyFromName(const std::string& name) {
|
|
if (name == "Anna") {
|
|
return std::make_unique<AnnaStrategy>();
|
|
} else {
|
|
throw std::runtime_error("Unknown strategy");
|
|
}
|
|
}
|
|
|
|
RenderWindow::RenderWindow() : window(nullptr), renderer(nullptr) {
|
|
this->mainWindowWidget = new QWidget();
|
|
this->layout = new QVBoxLayout(this->mainWindowWidget);
|
|
|
|
auto* menu = new QMenuBar();
|
|
auto* fileMenu = menu->addMenu("File");
|
|
auto* openAction = fileMenu->addAction("Open pattern");
|
|
openAction->setShortcut(QKeySequence::Open);
|
|
QObject::connect(openAction, &QAction::triggered, this, [this](){
|
|
auto filename = QFileDialog::getOpenFileName(this, "Open pattern", "", "SVG path files (*.*)");
|
|
if (!filename.isNull()) {
|
|
this->loadPattern(filename);
|
|
}
|
|
});
|
|
auto* quitAction = fileMenu->addAction("Quit");
|
|
quitAction->setShortcut(QKeySequence::Quit);
|
|
quitAction->setMenuRole(QAction::QuitRole);
|
|
QObject::connect(quitAction, &QAction::triggered, this, &RenderWindow::close);
|
|
this->setMenuBar(menu);
|
|
|
|
this->renderArea = new QWidget();
|
|
this->layout->addWidget(this->renderArea);
|
|
|
|
this->controlsLayout = new QHBoxLayout();
|
|
this->layout->addLayout(this->controlsLayout);
|
|
|
|
auto* strategyLabel = new QLabel("Strategy:");
|
|
this->controlsLayout->addWidget(strategyLabel);
|
|
|
|
this->strategySelector = new QComboBox();
|
|
this->strategySelector->addItem("Anna");
|
|
QObject::connect(this->strategySelector, &QComboBox::currentIndexChanged, this, &RenderWindow::resetState);
|
|
this->controlsLayout->addWidget(this->strategySelector);
|
|
|
|
this->controlsLayout->addStretch();
|
|
|
|
this->distanceLabel = new QLabel("Distance: 0");
|
|
this->controlsLayout->addWidget(this->distanceLabel);
|
|
|
|
this->controlsLayout->addStretch();
|
|
|
|
auto *resetButton = new QPushButton("Reset");
|
|
QObject::connect(resetButton, &QPushButton::clicked, this, &RenderWindow::resetState);
|
|
this->controlsLayout->addWidget(resetButton);
|
|
|
|
auto *hardResetButton = new QPushButton("Reset (new vertices)");
|
|
QObject::connect(hardResetButton, &QPushButton::clicked, this, &RenderWindow::hardResetState);
|
|
this->controlsLayout->addWidget(hardResetButton);
|
|
|
|
auto* solveButton = new QPushButton("Solve");
|
|
QObject::connect(solveButton, &QPushButton::clicked, this, &RenderWindow::solve);
|
|
this->controlsLayout->addWidget(solveButton);
|
|
|
|
this->playButton = new QPushButton("Play");
|
|
this->playButton->setCheckable(true);
|
|
QObject::connect(this->playButton, &QPushButton::toggled, this, &RenderWindow::togglePlay);
|
|
this->controlsLayout->addWidget(this->playButton);
|
|
|
|
auto* stepButton = new QPushButton("Step");
|
|
QObject::connect(stepButton, &QPushButton::clicked, this, &RenderWindow::step);
|
|
this->controlsLayout->addWidget(stepButton);
|
|
|
|
this->setCentralWidget(this->mainWindowWidget);
|
|
|
|
this->animTimer = new QTimer(this);
|
|
QObject::connect(this->animTimer, &QTimer::timeout, this, &RenderWindow::step);
|
|
|
|
this->renderArea->setUpdatesEnabled(false);
|
|
this->renderArea->setFixedSize(800, 800);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_WINDOW_SHARE_PIXEL_FORMAT, "1");
|
|
|
|
this->window = SDL_CreateWindowFrom(reinterpret_cast<const void*>(this->renderArea->winId()));
|
|
|
|
if (this->window == nullptr) {
|
|
std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl;
|
|
throw std::runtime_error("Error creating SDL window");
|
|
}
|
|
|
|
this->renderer = SDL_CreateRenderer(this->window, -1, SDL_RENDERER_ACCELERATED);
|
|
|
|
if (this->renderer == nullptr) {
|
|
std::cerr << "Error creating SDL renderer: " << SDL_GetError() << std::endl;
|
|
throw std::runtime_error("Error creating SDL renderer");
|
|
}
|
|
|
|
this->hardResetState();
|
|
}
|
|
|
|
void RenderWindow::hardResetState()
|
|
{
|
|
this->state.vertices = {};
|
|
|
|
std::random_device rd;
|
|
std::mt19937 gen(rd());
|
|
std::uniform_real_distribution<double> dist(100.0, 700.0);
|
|
|
|
for (size_t i = 0; i < 100; i++) {
|
|
this->state.vertices.push_back({
|
|
.x = dist(gen),
|
|
.y = dist(gen)
|
|
});
|
|
}
|
|
|
|
this->resetState();
|
|
}
|
|
|
|
void RenderWindow::resetState() {
|
|
this->animTimer->stop();
|
|
this->playButton->setChecked(false);
|
|
|
|
this->strategy = strategyFromName(this->strategySelector->currentText().toStdString());
|
|
|
|
this->state.edges = {};
|
|
|
|
this->render();
|
|
}
|
|
|
|
void RenderWindow::paintEvent(QPaintEvent* event)
|
|
{
|
|
QMainWindow::paintEvent(event);
|
|
this->render();
|
|
}
|
|
|
|
void RenderWindow::render() {
|
|
SDL_SetRenderDrawColor(this->renderer, 0, 0, 0, 255);
|
|
SDL_RenderClear(this->renderer);
|
|
|
|
SDL_SetRenderDrawColor(this->renderer, 255, 255, 255, 255);
|
|
|
|
for (auto& vertex : this->state.vertices) {
|
|
SDL_Rect rect = {
|
|
static_cast<int>(vertex.x - 3),
|
|
static_cast<int>(vertex.y - 3),
|
|
6,
|
|
6
|
|
};
|
|
|
|
SDL_RenderFillRect(this->renderer, &rect);
|
|
}
|
|
|
|
double dist = 0.0;
|
|
|
|
for (auto& edge : this->state.edges) {
|
|
SDL_SetRenderDrawColor(this->renderer, 255, 255, 0, 255);
|
|
|
|
dist += std::hypot(
|
|
this->state.vertices[edge.from].x - this->state.vertices[edge.to].x,
|
|
this->state.vertices[edge.from].y - this->state.vertices[edge.to].y
|
|
);
|
|
|
|
SDL_RenderDrawLine(
|
|
this->renderer,
|
|
static_cast<int>(this->state.vertices[edge.from].x),
|
|
static_cast<int>(this->state.vertices[edge.from].y),
|
|
static_cast<int>(this->state.vertices[edge.to].x),
|
|
static_cast<int>(this->state.vertices[edge.to].y)
|
|
);
|
|
}
|
|
|
|
this->distanceLabel->setText(QString("Distance: %1").arg(dist));
|
|
|
|
SDL_RenderPresent(this->renderer);
|
|
}
|
|
|
|
void RenderWindow::step() {
|
|
if (this->strategy->step(this->state)) {
|
|
this->animTimer->stop();
|
|
this->playButton->setChecked(false);
|
|
}
|
|
this->render();
|
|
}
|
|
|
|
void RenderWindow::solve() {
|
|
if (this->state.edges.size() == this->state.vertices.size()) {
|
|
this->resetState();
|
|
}
|
|
|
|
while (!this->strategy->step(this->state)) {}
|
|
|
|
this->render();
|
|
}
|
|
|
|
RenderWindow::~RenderWindow() {
|
|
if (this->renderer != nullptr) {
|
|
SDL_DestroyRenderer(this->renderer);
|
|
this->renderer = nullptr;
|
|
}
|
|
|
|
if (this->window != nullptr) {
|
|
SDL_DestroyWindow(this->window);
|
|
this->window = nullptr;
|
|
}
|
|
}
|
|
|
|
void RenderWindow::togglePlay(bool play)
|
|
{
|
|
if (play) {
|
|
if (this->state.edges.size() == this->state.vertices.size()) {
|
|
this->resetState();
|
|
}
|
|
|
|
this->animTimer->start(5);
|
|
} else {
|
|
this->animTimer->stop();
|
|
}
|
|
}
|
|
|
|
void RenderWindow::loadPattern(const QString& qString)
|
|
{
|
|
try {
|
|
std::ifstream file(qString.toStdString());
|
|
file.seekg(0, std::ios::end);
|
|
size_t size = file.tellg();
|
|
std::string buffer(size, '\0');
|
|
file.seekg(0);
|
|
file.read(&buffer[0], static_cast<std::streamsize>(size));
|
|
|
|
auto vertices = extractVertices(buffer);
|
|
|
|
const auto min_x = std::ranges::min(vertices | std::ranges::views::transform([](const Vertex& v) { return v.x; }));
|
|
const auto min_y = std::ranges::min(vertices | std::ranges::views::transform([](const Vertex& v) { return v.y; }));
|
|
const auto max_x = std::ranges::max(vertices | std::ranges::views::transform([](const Vertex& v) { return v.x; }));
|
|
const auto max_y = std::ranges::max(vertices | std::ranges::views::transform([](const Vertex& v) { return v.y; }));
|
|
|
|
const double scale = std::min(600.0 / (max_x - min_x), 600.0 / (max_y - min_y));
|
|
for (auto& vertex : vertices) {
|
|
vertex.x = (vertex.x - min_x) * scale + 100.0;
|
|
vertex.y = (vertex.y - min_y) * scale + 100.0;
|
|
}
|
|
|
|
this->state.vertices = vertices;
|
|
this->resetState();
|
|
} catch (std::exception& e) {
|
|
std::cerr << "Error loading pattern: " << e.what() << std::endl;
|
|
}
|
|
}
|
|
|
|
bool AnnaStrategy::step(State& state) {
|
|
if (!this->init) {
|
|
this->init = true;
|
|
this->unvisited = {};
|
|
for (size_t i = 0; i < state.vertices.size(); i++) {
|
|
this->unvisited.push_back(i);
|
|
}
|
|
std::shuffle(this->unvisited.begin(), this->unvisited.end(), std::mt19937(std::random_device()()));
|
|
}
|
|
|
|
if (state.vertices.size() < 2) {
|
|
return true;
|
|
}
|
|
|
|
if (state.vertices.size() == 2) {
|
|
state.edges = {{
|
|
.from = 0,
|
|
.to = 1
|
|
}};
|
|
return true;
|
|
}
|
|
|
|
if (this->unvisited.empty())
|
|
return true;
|
|
|
|
if (state.vertices.size() - this->unvisited.size() == 0) {
|
|
size_t a, b, c;
|
|
a = this->unvisited.back();
|
|
this->unvisited.pop_back();
|
|
b = this->unvisited.back();
|
|
this->unvisited.pop_back();
|
|
c = this->unvisited.back();
|
|
this->unvisited.pop_back();
|
|
|
|
state.edges = {{
|
|
.from = a,
|
|
.to = b
|
|
}, {
|
|
.from = b,
|
|
.to = c
|
|
}, {
|
|
.from = c,
|
|
.to = a
|
|
}};
|
|
|
|
return false;
|
|
}
|
|
|
|
size_t i = this->unvisited.back();
|
|
this->unvisited.pop_back();
|
|
|
|
const auto& vertex = state.vertices[i];
|
|
|
|
auto& closest_edge = *std::min_element(state.edges.begin(), state.edges.end(), [&state, &vertex](const Edge& a, const Edge& b) {
|
|
return a.distanceOf(state, vertex) < b.distanceOf(state, vertex);
|
|
});
|
|
|
|
size_t old_to = closest_edge.to;
|
|
closest_edge.to = i;
|
|
state.edges.push_back({
|
|
.from = i,
|
|
.to = old_to
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
double Edge::distanceOf(const State& state, const Vertex& vertex) const {
|
|
// Adapted from https://stackoverflow.com/a/1501725
|
|
auto x1 = state.vertices[this->from].x;
|
|
auto y1 = state.vertices[this->from].y;
|
|
auto x2 = state.vertices[this->to].x;
|
|
auto y2 = state.vertices[this->to].y;
|
|
|
|
auto dx = x2 - x1;
|
|
auto dy = y2 - y1;
|
|
|
|
auto l2 = dx * dx + dy * dy;
|
|
|
|
constexpr auto epsilon = 0.0001;
|
|
|
|
if (l2 < epsilon)
|
|
return std::hypot(vertex.x - x1, vertex.y - y1);
|
|
|
|
const double t = std::max(0.0, std::min(1.0, ((vertex.x - x1) * dx + (vertex.y - y1) * dy) / l2));
|
|
|
|
const double projection_x = x1 + t * dx;
|
|
const double projection_y = y1 + t * dy;
|
|
|
|
return std::hypot(vertex.x - projection_x, vertex.y - projection_y);
|
|
} |