#include "state.hpp" #include #include #include #include #include #include #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(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( 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* 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 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; }