Triangles/src/state.cpp

747 lines
23 KiB
C++

#include "state.hpp"
#include <cstring>
#include <cassert>
#include <string>
#include <cmath>
#include <cstdio>
#include <raylib.h>
#include "helpers.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
Tri::State::State(int argc, char **argv) :
flags(),
width(DEFAULT_WIDTH),
height(DEFAULT_HEIGHT),
dt(1.0f/60.0f),
notificationAlpha(1.0f),
notificationAmt(0.0F),
notificationText(),
tris(),
currentTri(),
currentTri_state(CurrentState::NONE),
pointCircle({7.0F, 7.0F}, 7.0F, WHITE),
colorPickerColor{255, 255, 255, 255},
bgColorPickerColor{0, 0, 0, 255},
bgColor(BLACK),
saveFilenameBuffer(),
failedMessage(),
drawCache(),
pi(std::acos(-1.0f)),
selectedTri(),
selectedTriColor{255, 255, 255, 255},
prevTriColor{255, 255, 255, 255},
selectedTriBlinkTimer(),
inputWidth(800),
inputHeight(600),
history(),
history_idx(0),
clickTimeout(0.0F)
{
InitWindow(width, height, "Triangles");
SetTargetFPS(60);
flags.set(F_IS_RUNNING); // is running
set_notification_text("Press \"H\" for help");
saveFilenameBuffer.fill(0);
drawCache = LoadRenderTexture(width, height);
flags.set(F_DRAW_CACHE_INITIALIZED);
flags.set(F_DRAW_CACHE_DIRTY);
GuiSetStyle(DEFAULT, BACKGROUND_COLOR, TRI_GUI_BG_COLOR);
GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, TRI_GUI_BASE_COLOR);
GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, TRI_GUI_TEXT_COLOR);
GuiSetStyle(DEFAULT, TEXT_SIZE, TRI_GUI_TEXT_SIZE);
}
#pragma GCC diagnostic pop
Tri::State::Action::Action() :
type(Tri::State::Action::AT_NONE),
idx(0),
color(BLACK),
data{{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}}
{}
Tri::State::Action::Action(const Type& type, IndexT idx, const Color& color, float *data) :
type(type),
idx(idx),
color(color),
data()
{
init(data);
}
Tri::State::Action::Action(Type&& type, IndexT idx, Color&& color, float *data) :
type(type),
idx(idx),
color(color),
data()
{
init(data);
}
Tri::State::Action &Tri::State::Action::setNewColor(Color color) {
this->data.newColor = color;
return *this;
}
void Tri::State::Action::init(float *data) {
switch(type) {
case AT_TRI:
case AT_TRI_DEL:
for(unsigned int i = 0; i < 6; ++i) {
this->data.tri[i] = data[i];
}
break;
case AT_POINT:
this->data.point[0] = data[0];
this->data.point[1] = data[1];
break;
case AT_COLOR:
break;
default:
type = AT_NONE;
idx = 0;
color = BLACK;
this->data = {{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}};
break;
}
}
Tri::State::~State() {
UnloadRenderTexture(drawCache);
CloseWindow();
}
void Tri::State::handle_events() {
if(WindowShouldClose()) {
flags.reset(F_IS_RUNNING);
}
int keyPressed = GetKeyPressed();
while(keyPressed > 0) {
if(!flags.test(F_DISPLAY_SAVE)) {
switch(keyPressed) {
case KEY_H:
flags.flip(F_DISPLAY_HELP);
break;
case KEY_U:
if (flags.test(F_TRI_EDIT_MODE)) {
set_notification_text("Cannot undo during editing tri!");
break;
}
flags.set(F_DRAW_CACHE_DIRTY);
if (history_idx > 0) {
switch(history[history_idx-1].type) {
case Action::AT_TRI:
tris.erase(
tris.cbegin() + history[history_idx-1].idx);
restore_points_on_tri_del(history_idx - 1);
break;
case Action::AT_TRI_DEL:
{
tris.emplace(
tris.cbegin() + history[history_idx].idx,
Triangle(
{{
{history[history_idx-1].data.tri[0],
history[history_idx-1].data.tri[1]},
{history[history_idx-1].data.tri[2],
history[history_idx-1].data.tri[3]},
{history[history_idx-1].data.tri[4],
history[history_idx-1].data.tri[5]},
}},
history[history_idx-1].color
)
);
currentTri_state = CurrentState::NONE;
break;
}
case Action::AT_POINT:
assert(history[history_idx-1].idx + 1 == currentTri_state
&& "Point in history must match point index");
assert(currentTri_state > 0
&& "There must be a point to undo a point");
currentTri_state = static_cast<CurrentState>(currentTri_state - 1);
break;
case Action::AT_COLOR:
tris.at(history[history_idx-1].idx).fillColor = history[history_idx-1].color;
break;
default:
assert(!"Unreachable code");
break;
}
--history_idx;
}
break;
case KEY_R:
flags.set(F_DRAW_CACHE_DIRTY);
if(history_idx < history.size()) {
switch(history[history_idx].type) {
case Action::AT_TRI:
{
tris.emplace(
tris.cbegin() + history[history_idx].idx,
Triangle(
{{
{history[history_idx].data.tri[0],
history[history_idx].data.tri[1]},
{history[history_idx].data.tri[2],
history[history_idx].data.tri[3]},
{history[history_idx].data.tri[4],
history[history_idx].data.tri[5]},
}},
history[history_idx].color
)
);
currentTri_state = CurrentState::NONE;
break;
}
break;
case Action::AT_TRI_DEL:
tris.erase(
tris.cbegin() + history[history_idx].idx);
restore_points_on_tri_del(history_idx);
break;
case Action::AT_POINT:
assert(history[history_idx].idx == currentTri_state
&& "Point in history must match point index");
assert(currentTri_state < CurrentState::SECOND
&& "Current point state must be 0 or 1");
currentTri[currentTri_state].x = history[history_idx].data.point[0];
currentTri[currentTri_state].y = history[history_idx].data.point[1];
currentTri_state = static_cast<CurrentState>(
currentTri_state + 1);
pointCircle.fillColor = history[history_idx].color;
break;
case Action::AT_COLOR:
tris.at(history[history_idx].idx).fillColor = history[history_idx].data.newColor;
break;
default:
assert(!"Unreachable code");
break;
}
++history_idx;
}
break;
case KEY_C:
if(flags.test(F_DISPLAY_COLOR_P)) {
close_color_picker();
} else {
flags.set(F_DISPLAY_COLOR_P);
}
break;
case KEY_B:
if(flags.test(F_DISPLAY_BG_COLOR_P)) {
close_bg_color_picker();
} else {
flags.set(F_DISPLAY_BG_COLOR_P);
}
break;
case KEY_S:
flags.flip(F_DISPLAY_SAVE);
break;
case KEY_P:
flags.flip(F_COPY_COLOR_MODE);
if(flags.test(F_COPY_COLOR_MODE)) {
set_notification_text(
"Copy color mode\n"
"Click to change\n"
"current draw color\n"
"to what was\n"
"clicked on");
} else {
clear_notification_alpha();
}
break;
case KEY_I:
flags.flip(F_DISPLAY_CHANGE_SIZE);
if(!flags.test(F_DISPLAY_CHANGE_SIZE)) {
close_input_width_height_window();
} else {
failedMessage = "Press TAB to switch between width/height";
}
break;
case KEY_E:
if(flags.test(F_TRI_EDIT_MODE)) {
close_selected_tri_mode();
} else {
flags.flip(F_SELECT_TRI_MODE);
if(flags.test(F_SELECT_TRI_MODE)) {
set_notification_text("Click on a tri\nto edit it");
}
}
break;
case KEY_TAB:
flags.flip(F_TAB_TOGGLE);
break;
}
}
keyPressed = GetKeyPressed();
}
if(IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && clickTimeout == 0.0F) {
if(can_draw()) {
switch(currentTri_state) {
case CurrentState::NONE: {
currentTri[0] = {GetMouseX(), GetMouseY()};
currentTri_state = CurrentState::FIRST;
if(history_idx < history.size()) {
history.resize(history_idx);
}
float points[2] = {currentTri[0].x, currentTri[0].y};
history.push_back(Action(Action::AT_POINT,
0,
pointCircle.fillColor,
points));
++history_idx;
break; }
case CurrentState::FIRST: {
currentTri[1] = {GetMouseX(), GetMouseY()};
currentTri_state = CurrentState::SECOND;
if(history_idx < history.size()) {
history.resize(history_idx);
}
float points[2] = {currentTri[1].x, currentTri[1].y};
history.push_back(Action(Action::AT_POINT,
1,
pointCircle.fillColor,
points));
++history_idx;
break; }
case CurrentState::SECOND: {
currentTri[2] = {GetMouseX(), GetMouseY()};
make_counter_clockwise(currentTri);
tris.emplace_back(currentTri, pointCircle.fillColor);
currentTri_state = CurrentState::NONE;
flags.set(F_DRAW_CACHE_DIRTY);
if(history_idx < history.size()) {
history.resize(history_idx);
}
float points[6] = {
currentTri[0].x, currentTri[0].y,
currentTri[1].x, currentTri[1].y,
currentTri[2].x, currentTri[2].y,
};
history.push_back(Action(Action::AT_TRI,
tris.size()-1,
pointCircle.fillColor,
points));
++history_idx;
break; }
}
} else if(flags.test(F_COPY_COLOR_MODE)) {
check_draw_cache();
Image drawImage = LoadImageFromTexture(drawCache.texture);
Color *colors = LoadImageColors(drawImage);
int mx = GetMouseX();
int my = GetMouseY();
if(mx < 0) { mx = 0; }
else if(mx >= drawImage.width) { mx = drawImage.width - 1; }
if(my < 0) { my = 0; }
else if(my >= drawImage.height) { my = drawImage.height - 1; }
my = drawImage.height - my;
colorPickerColor = colors[mx + my * drawImage.width];
pointCircle.fillColor = colors[mx + my * drawImage.width];
flags.reset(F_COPY_COLOR_MODE);
set_notification_text("Color set");
UnloadImageColors(colors);
UnloadImage(drawImage);
} else if(flags.test(F_SELECT_TRI_MODE)) {
int mx = GetMouseX();
int my = GetMouseY();
if(mx < 0) { mx = 0; }
else if(mx >= (int)width) { mx = width - 1; }
if(my < 0) { my = 0; }
else if(my >= (int)height) { my = height - 1; }
for (unsigned int i = 0; i < tris.size(); ++i) {
if(is_within_shape(tris[i], {mx, my})) {
tris[i].outlineColor = invert_color(tris[i].fillColor);
flags.reset(F_SELECT_TRI_MODE);
flags.set(F_TRI_EDIT_MODE);
flags.set(F_TRI_EDIT_DRAW_TRI);
selectedTriBlinkTimer = 1.0f;
selectedTriColor = tris[i].fillColor;
prevTriColor = tris[i].fillColor;
selectedTri = i;
clickTimeout = CLICK_TIMEOUT_TIME;
break;
}
}
if(!flags.test(F_TRI_EDIT_MODE)) {
set_notification_text("Did not select\nanything");
}
}
}
}
void Tri::State::update() {
dt = GetFrameTime();
if(notificationAlpha > 0.0f) {
notificationAmt += dt * NOTIFICATION_FADE_RATE;
if (notificationAmt > 1.0F) {
clear_notification_alpha();
} else {
notificationAlpha = Tri::sq_lerp(1.0F, 0.0F, notificationAmt);
}
}
if(flags.test(F_COLOR_P_COLOR_DIRTY)) {
flags.reset(F_COLOR_P_COLOR_DIRTY);
pointCircle.fillColor = colorPickerColor;
}
if(flags.test(F_BG_COLOR_P_COLOR_DIRTY)) {
flags.reset(F_BG_COLOR_P_COLOR_DIRTY);
bgColor = bgColorPickerColor;
bgColor.a = 255;
}
if(flags.test(F_TRI_EDIT_MODE)) {
selectedTriBlinkTimer -= dt * TRIANGLES_EDIT_TRI_BLINK_RATE;
if(selectedTriBlinkTimer <= 0.0f) {
selectedTriBlinkTimer = 1.0f;
flags.flip(F_TRI_EDIT_DRAW_TRI);
}
}
if (clickTimeout > 0.0F) {
clickTimeout -= dt;
if (clickTimeout < 0.0F) {
clickTimeout = 0.0F;
}
}
}
void Tri::State::draw() {
// Should be able to directly draw a texture held by the RenderTexture2D
if(flags.test(F_DRAW_CACHE_INITIALIZED)) {
// draw cache initialized
check_draw_cache();
BeginDrawing();
// hack to flip in the y direction, since RenderTexture2D's texture
// is flipped
DrawTextureRec(
drawCache.texture,
{
0.0f,
0.0f,
(float)drawCache.texture.width,
(float)-drawCache.texture.height
},
{0.0f, 0.0f},
WHITE);
} else {
BeginDrawing();
draw_to_target(nullptr);
}
if(flags.test(F_TRI_EDIT_MODE) && flags.test(F_TRI_EDIT_DRAW_TRI)) {
// tris.at(selectedTri).setOutlineThickness(4.0f);
tris.at(selectedTri).draw();
// tris.at(selectedTri).setOutlineThickness(0.0f);
}
if(can_draw()) {
for(unsigned int i = 0; i < currentTri_state; ++i) {
DrawCircle(currentTri[i].x, currentTri[i].y, pointCircle.getRadius(), pointCircle.fillColor);
}
}
Tri::draw_notification(this);
Tri::draw_color_picker(this);
Tri::draw_bg_color_picker(this);
Tri::draw_edit_tri(this);
Tri::draw_change_size(this);
Tri::draw_save(this);
Tri::draw_help(this);
EndDrawing();
}
void Tri::State::draw_to_target(RenderTexture2D *target) {
if(target) {
BeginTextureMode(*target);
ClearBackground(bgColor);
// draw tris
for(unsigned int i = 0; i < tris.size(); ++i) {
tris[i].draw();
}
EndTextureMode();
} else {
// Expects BeginDrawing() already having been called prior to this fn
ClearBackground(bgColor);
// draw tris
for(unsigned int i = 0; i < tris.size(); ++i) {
tris[i].draw();
}
}
}
unsigned int Tri::State::get_width() const {
return width;
}
unsigned int Tri::State::get_height() const {
return height;
}
const Tri::State::BitsetType Tri::State::get_flags() const {
return flags;
}
float Tri::State::get_notification_alpha() const {
return notificationAlpha;
}
void Tri::State::reset_notification_alpha() {
notificationAlpha = 1.0F;
notificationAmt = 0.0F;
}
void Tri::State::clear_notification_alpha() {
notificationAlpha = 0.0F;
notificationAmt = 1.0F;
}
const char* Tri::State::get_notification_text() const {
return notificationText.data();
}
void Tri::State::set_notification_text(const char *text) {
notificationText.fill(0);
std::strncpy(notificationText.data(),
text,
notificationText.max_size() - 1);
reset_notification_alpha();
}
void Tri::State::append_notification_text(const char *text) {
auto length = std::strlen(notificationText.data());
if(length + 1 >= notificationText.max_size()) {
return;
}
std::strncpy(
notificationText.data() + length,
text,
notificationText.max_size() - length - 1);
reset_notification_alpha();
}
Color& Tri::State::get_color() {
flags.set(F_COLOR_P_COLOR_DIRTY);
return colorPickerColor;
}
Color& Tri::State::get_bg_color() {
flags.set(F_BG_COLOR_P_COLOR_DIRTY);
return bgColorPickerColor;
}
std::array<char, 256>* Tri::State::get_save_filename_buffer() {
return &saveFilenameBuffer;
}
bool Tri::State::do_save() {
RenderTexture2D saveTexture = LoadRenderTexture(width, height);
draw_to_target(&saveTexture);
Image saveImage = LoadImageFromTexture(saveTexture.texture);
ImageFlipVertical(&saveImage);
UnloadRenderTexture(saveTexture);
if(ExportImage(saveImage, saveFilenameBuffer.data())) {
#ifndef NDEBUG
printf("Saved to \"%s\"\n", saveFilenameBuffer.data());
#endif
failedMessage.clear();
UnloadImage(saveImage);
return true;
} else {
#ifndef NDEBUG
printf("ERROR: Failed to save \"%s\"\n", saveFilenameBuffer.data());
#endif
failedMessage = std::string("Failed to save (does the name end in \".png\"?)");
UnloadImage(saveImage);
return false;
}
}
const std::string& Tri::State::failed_message() const {
return failedMessage;
}
void Tri::State::close_save() {
flags.reset(F_DISPLAY_SAVE);
}
bool Tri::State::can_draw() const {
return !flags.test(F_DISPLAY_HELP)
&& !flags.test(F_DISPLAY_COLOR_P)
&& !flags.test(F_DISPLAY_BG_COLOR_P)
&& !flags.test(F_DISPLAY_SAVE)
&& !flags.test(F_COPY_COLOR_MODE)
&& !flags.test(F_DISPLAY_CHANGE_SIZE)
&& !flags.test(F_SELECT_TRI_MODE)
&& !flags.test(F_TRI_EDIT_MODE);
}
void Tri::State::reset_modes() {
flags.reset(F_DISPLAY_HELP);
flags.reset(F_DISPLAY_COLOR_P);
flags.reset(F_DISPLAY_BG_COLOR_P);
flags.reset(F_DISPLAY_SAVE);
flags.reset(F_COPY_COLOR_MODE);
flags.reset(F_DISPLAY_CHANGE_SIZE);
flags.reset(F_SELECT_TRI_MODE);
flags.reset(F_TRI_EDIT_MODE);
}
void Tri::State::close_help() {
flags.reset(F_DISPLAY_HELP);
}
void Tri::State::close_color_picker() {
flags.reset(F_DISPLAY_COLOR_P);
flags.set(F_DRAW_CACHE_DIRTY);
}
void Tri::State::close_bg_color_picker() {
flags.reset(F_DISPLAY_BG_COLOR_P);
flags.set(F_DRAW_CACHE_DIRTY);
}
bool Tri::State::change_width_height() {
if(inputWidth < 0 || inputHeight < 0) {
failedMessage = "Width or Height cannot be less than 0";
return false;
}
if(inputWidth < 800) {
inputWidth = 800;
}
if(inputHeight < 600) {
inputHeight = 600;
}
notificationText.fill(0);
std::array<char, 5> tempBuf = {0, 0, 0, 0, 0};
append_notification_text("Width set to ");
snprintf(tempBuf.data(), 5, "%u", inputWidth);
append_notification_text(tempBuf.data());
append_notification_text(", Height set to ");
snprintf(tempBuf.data(), 5, "%u", inputHeight);
append_notification_text(tempBuf.data());
this->width = inputWidth;
this->height = inputHeight;
UnloadRenderTexture(drawCache);
SetWindowSize(this->width, this->height);
drawCache = LoadRenderTexture(this->width, this->height);
flags.set(F_DRAW_CACHE_DIRTY);
currentTri_state = CurrentState::NONE;
return true;
}
void Tri::State::close_input_width_height_window() {
failedMessage.clear();
inputWidth = width;
inputHeight = height;
flags.reset(F_DISPLAY_CHANGE_SIZE);
}
float Tri::State::get_pi() const {
return pi;
}
Color& Tri::State::get_selected_tri_color() {
tris.at(selectedTri).fillColor = selectedTriColor;
return selectedTriColor;
}
void Tri::State::close_selected_tri_mode() {
// Set tri's new color in history.
if (prevTriColor != selectedTriColor) {
if (history_idx < history.size()) {
history.resize(history_idx);
}
history.emplace_back(Action::AT_COLOR, selectedTri, prevTriColor, nullptr);
history.back().setNewColor(selectedTriColor);
++history_idx;
}
tris.at(selectedTri).fillColor = selectedTriColor;
flags.set(F_DRAW_CACHE_DIRTY);
reset_modes();
}
bool Tri::State::check_draw_cache() {
if(flags.test(F_DRAW_CACHE_INITIALIZED) && flags.test(F_DRAW_CACHE_DIRTY)) {
// draw cache initialized and dirty
flags.reset(F_DRAW_CACHE_DIRTY);
draw_to_target(&drawCache);
return true;
} else {
return false;
}
}
int* Tri::State::get_input_width() {
return &inputWidth;
}
int* Tri::State::get_input_height() {
return &inputHeight;
}
void Tri::State::restore_points_on_tri_del(Action::IndexT end) {
assert(end < history.size()
&& "Index on history must be in range");
currentTri[2].x = history[end].data.tri[4];
currentTri[2].y = history[end].data.tri[5];
pointCircle.fillColor = history[end].color;
unsigned int currentTriIdx = 1;
while(end-- > 0) {
assert(history[end].type == Action::AT_POINT
&& "Latest history must be AT_POINT type");
assert(history[end].idx == currentTriIdx
&& "Last point must be second point");
currentTri[currentTriIdx].x = history[end].data.point[0];
currentTri[currentTriIdx].y = history[end].data.point[1];
if(currentTriIdx > 0) {
--currentTriIdx;
} else {
currentTri_state = CurrentState::SECOND;
return;
}
}
assert(!"Unreachable code");
return;
}
float Tri::State::get_click_timeout() const {
return clickTimeout;
}