diff --git a/README.md b/README.md index 3c7744e..c755f6b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,31 @@ Currently an image must be given on the command line like so: ./sago_multi_scrambler_puzzle2 data/collections/fairy_tales/The\ Three\ Bears\ \(1918\).jpg ``` +## Features + +### Rectangular Mode + +In the Settings menu, you can enable "Rectangular Mode" which creates puzzles with rectangular pieces instead of the default random splitting algorithm. + +### Custom Piece Layout Editor + +When in Rectangular Mode, you can customize the placement and size of puzzle pieces: + +1. Open an image in Rectangular Mode +2. Select **File → Edit Piece Layout** to launch the piece editor +3. In the editor, you can: + - **Click** on a piece to select it + - **Drag** a selected piece to move it + - **Drag corners** to resize from that corner + - **Drag edges** to resize from that edge + - Press **N** to add a new piece + - Press **Delete** to remove the selected piece + - Press **Ctrl+S** to save the custom layout +4. Press **ESC** or close via menu to exit the editor +5. Your custom piece layout will be saved and used automatically the next time you load that image + +Custom layouts are saved per-image based on the image's unique hash, so each image can have its own custom puzzle layout. + ## Desktop Integration ### Adding "Open with Sago Multi Scrambler Puzzle II" to Context Menu diff --git a/src/PuzzlePieceEditorState.cpp b/src/PuzzlePieceEditorState.cpp new file mode 100644 index 0000000..9e7fc80 --- /dev/null +++ b/src/PuzzlePieceEditorState.cpp @@ -0,0 +1,458 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/sago_multi_scrambler_puzzle2 +=========================================================================== +*/ + +#include "PuzzlePieceEditorState.hpp" +#include "globals.hpp" +#include "SagoImGui.hpp" +#include "config.hpp" +#include "os.hpp" +#include +#include +#include +#include + +PuzzlePieceEditorState::PuzzlePieceEditorState() { +} + +PuzzlePieceEditorState::PuzzlePieceEditorState(const std::string& imageFilePath, const std::string& pictureId, + SDL_Texture* texture, int sourceWidth, int sourceHeight, + const std::vector& initialPieces) + : imageFilePath(imageFilePath), pictureId(pictureId), pictureTex(texture), + source_image_width(sourceWidth), source_image_height(sourceHeight), + pieces_logical(initialPieces) { + + // Calculate logical size + resized_image_logical_height = resized_image_logical_height_max; + resized_image_logical_width = double(resized_image_logical_height) * (double(source_image_width) / double(source_image_height)); + if (resized_image_logical_width > resized_image_logical_width_max) { + resized_image_logical_width = resized_image_logical_width_max; + resized_image_logical_height = double(resized_image_logical_width) * (double(source_image_height) / double(source_image_width)); + } + + ResizeImagePhysical(); +} + +PuzzlePieceEditorState::~PuzzlePieceEditorState() { +} + +bool PuzzlePieceEditorState::IsActive() { + return isActive; +} + +void PuzzlePieceEditorState::ResizeImagePhysical() { + int resized_image_physical_height_max = globalData.ysize - 150; + int resized_image_physical_width_max = globalData.xsize - 70; + resized_image_physical_height = resized_image_physical_height_max; + resized_image_physical_width = double(resized_image_physical_height) * (double(source_image_width) / double(source_image_height)); + if (resized_image_physical_width > resized_image_physical_width_max) { + resized_image_physical_width = resized_image_physical_width_max; + resized_image_physical_height = double(resized_image_physical_width) * (double(source_image_height) / double(source_image_width)); + } + CreatePhysicalPieces(); +} + +void PuzzlePieceEditorState::CreatePhysicalPieces() { + pieces_physical.clear(); + for (size_t i = 0; i < pieces_logical.size(); ++i) { + SDL_Rect piece = pieces_logical.at(i); + piece.x = (piece.x) * resized_image_physical_width / resized_image_logical_width; + piece.y = (piece.y) * resized_image_physical_height / resized_image_logical_height; + piece.w = piece.w * resized_image_physical_width / resized_image_logical_width; + piece.h = piece.h * resized_image_physical_height / resized_image_logical_height; + pieces_physical.push_back(piece); + } +} + +PuzzlePieceEditorState::DragMode PuzzlePieceEditorState::GetDragModeForPosition(int x, int y, int pieceIndex) { + if (pieceIndex < 0 || pieceIndex >= pieces_physical.size()) { + return DragMode::NONE; + } + + SDL_Rect rect; + rect.x = globalData.xsize / 2 - resized_image_physical_width / 2; + rect.y = globalData.ysize / 2 - resized_image_physical_height / 2 + 40; + + const SDL_Rect& piece = pieces_physical[pieceIndex]; + int px = rect.x + piece.x; + int py = rect.y + piece.y; + + // Check corners first (higher priority) + if (abs(x - px) <= HANDLE_SIZE && abs(y - py) <= HANDLE_SIZE) { + return DragMode::RESIZE_NW; + } + if (abs(x - (px + piece.w)) <= HANDLE_SIZE && abs(y - py) <= HANDLE_SIZE) { + return DragMode::RESIZE_NE; + } + if (abs(x - px) <= HANDLE_SIZE && abs(y - (py + piece.h)) <= HANDLE_SIZE) { + return DragMode::RESIZE_SW; + } + if (abs(x - (px + piece.w)) <= HANDLE_SIZE && abs(y - (py + piece.h)) <= HANDLE_SIZE) { + return DragMode::RESIZE_SE; + } + + // Check edges + if (abs(y - py) <= HANDLE_SIZE && x >= px && x <= px + piece.w) { + return DragMode::RESIZE_N; + } + if (abs(y - (py + piece.h)) <= HANDLE_SIZE && x >= px && x <= px + piece.w) { + return DragMode::RESIZE_S; + } + if (abs(x - px) <= HANDLE_SIZE && y >= py && y <= py + piece.h) { + return DragMode::RESIZE_W; + } + if (abs(x - (px + piece.w)) <= HANDLE_SIZE && y >= py && y <= py + piece.h) { + return DragMode::RESIZE_E; + } + + // Check if inside piece for move + if (x >= px && x <= px + piece.w && y >= py && y <= py + piece.h) { + return DragMode::MOVE; + } + + return DragMode::NONE; +} + +void PuzzlePieceEditorState::UpdateDrag() { + if (dragMode == DragMode::NONE || selected_piece < 0 || selected_piece >= pieces_logical.size()) { + return; + } + int mouseX = globalData.mousex; + int mouseY = globalData.mousey; + int deltaX = mouseX - dragStartX; + int deltaY = mouseY - dragStartY; + // Convert delta to logical coordinates + deltaX = deltaX * resized_image_logical_width / resized_image_physical_width; + deltaY = deltaY * resized_image_logical_height / resized_image_physical_height; + SDL_Rect& piece = pieces_logical[selected_piece]; + switch (dragMode) { + case DragMode::MOVE: + piece.x = dragStartRect.x + deltaX; + piece.y = dragStartRect.y + deltaY; + // Clamp to image bounds + if (piece.x < 0) piece.x = 0; + if (piece.y < 0) piece.y = 0; + if (piece.x + piece.w > resized_image_logical_width) piece.x = resized_image_logical_width - piece.w; + if (piece.y + piece.h > resized_image_logical_height) piece.y = resized_image_logical_height - piece.h; + break; + case DragMode::RESIZE_N: + piece.y = dragStartRect.y + deltaY; + piece.h = dragStartRect.h - deltaY; + if (piece.h < MIN_PIECE_SIZE) { + piece.y = dragStartRect.y + dragStartRect.h - MIN_PIECE_SIZE; + piece.h = MIN_PIECE_SIZE; + } + if (piece.y < 0) { + piece.h = dragStartRect.y + dragStartRect.h; + piece.y = 0; + } + break; + case DragMode::RESIZE_S: + piece.h = dragStartRect.h + deltaY; + if (piece.h < MIN_PIECE_SIZE) piece.h = MIN_PIECE_SIZE; + if (piece.y + piece.h > resized_image_logical_height) piece.h = resized_image_logical_height - piece.y; + break; + case DragMode::RESIZE_W: + piece.x = dragStartRect.x + deltaX; + piece.w = dragStartRect.w - deltaX; + if (piece.w < MIN_PIECE_SIZE) { + piece.x = dragStartRect.x + dragStartRect.w - MIN_PIECE_SIZE; + piece.w = MIN_PIECE_SIZE; + } + if (piece.x < 0) { + piece.w = dragStartRect.x + dragStartRect.w; + piece.x = 0; + } + break; + + case DragMode::RESIZE_E: + piece.w = dragStartRect.w + deltaX; + if (piece.w < MIN_PIECE_SIZE) piece.w = MIN_PIECE_SIZE; + if (piece.x + piece.w > resized_image_logical_width) piece.w = resized_image_logical_width - piece.x; + break; + case DragMode::RESIZE_NW: + piece.x = dragStartRect.x + deltaX; + piece.y = dragStartRect.y + deltaY; + piece.w = dragStartRect.w - deltaX; + piece.h = dragStartRect.h - deltaY; + if (piece.w < MIN_PIECE_SIZE) { + piece.x = dragStartRect.x + dragStartRect.w - MIN_PIECE_SIZE; + piece.w = MIN_PIECE_SIZE; + } + if (piece.h < MIN_PIECE_SIZE) { + piece.y = dragStartRect.y + dragStartRect.h - MIN_PIECE_SIZE; + piece.h = MIN_PIECE_SIZE; + } + if (piece.x < 0) { + piece.w = dragStartRect.x + dragStartRect.w; + piece.x = 0; + } + if (piece.y < 0) { + piece.h = dragStartRect.y + dragStartRect.h; + piece.y = 0; + } + break; + + case DragMode::RESIZE_NE: + piece.y = dragStartRect.y + deltaY; + piece.w = dragStartRect.w + deltaX; + piece.h = dragStartRect.h - deltaY; + if (piece.w < MIN_PIECE_SIZE) piece.w = MIN_PIECE_SIZE; + if (piece.h < MIN_PIECE_SIZE) { + piece.y = dragStartRect.y + dragStartRect.h - MIN_PIECE_SIZE; + piece.h = MIN_PIECE_SIZE; + } + if (piece.x + piece.w > resized_image_logical_width) piece.w = resized_image_logical_width - piece.x; + if (piece.y < 0) { + piece.h = dragStartRect.y + dragStartRect.h; + piece.y = 0; + } + break; + case DragMode::RESIZE_SW: + piece.x = dragStartRect.x + deltaX; + piece.w = dragStartRect.w - deltaX; + piece.h = dragStartRect.h + deltaY; + if (piece.w < MIN_PIECE_SIZE) { + piece.x = dragStartRect.x + dragStartRect.w - MIN_PIECE_SIZE; + piece.w = MIN_PIECE_SIZE; + } + if (piece.h < MIN_PIECE_SIZE) piece.h = MIN_PIECE_SIZE; + if (piece.x < 0) { + piece.w = dragStartRect.x + dragStartRect.w; + piece.x = 0; + } + if (piece.y + piece.h > resized_image_logical_height) piece.h = resized_image_logical_height - piece.y; + break; + case DragMode::RESIZE_SE: + piece.w = dragStartRect.w + deltaX; + piece.h = dragStartRect.h + deltaY; + if (piece.w < MIN_PIECE_SIZE) piece.w = MIN_PIECE_SIZE; + if (piece.h < MIN_PIECE_SIZE) piece.h = MIN_PIECE_SIZE; + if (piece.x + piece.w > resized_image_logical_width) piece.w = resized_image_logical_width - piece.x; + if (piece.y + piece.h > resized_image_logical_height) piece.h = resized_image_logical_height - piece.y; + break; + default: + break; + } + CreatePhysicalPieces(); +} + +void PuzzlePieceEditorState::SavePieces() { + std::string savePath = getPathToSaveFiles() + "/piece_layouts"; + OsCreateFolder(savePath); + std::string filename = savePath + "/" + pictureId + ".layout"; + std::ofstream file(filename); + if (file.is_open()) { + file << pieces_logical.size() << std::endl; + for (const SDL_Rect& piece : pieces_logical) { + file << piece.x << " " << piece.y << " " << piece.w << " " << piece.h << std::endl; + } + file.close(); + saved = true; + std::cout << "Saved piece layout to " << filename << std::endl; + } else { + std::cerr << "Failed to save piece layout to " << filename << std::endl; + } +} + +void PuzzlePieceEditorState::DeleteSelectedPiece() { + if (selected_piece >= 0 && selected_piece < pieces_logical.size()) { + pieces_logical.erase(pieces_logical.begin() + selected_piece); + selected_piece = -1; + CreatePhysicalPieces(); + } +} + +void PuzzlePieceEditorState::AddNewPiece() { + // Add a new piece in the center + SDL_Rect newPiece; + newPiece.w = 150; + newPiece.h = 150; + newPiece.x = resized_image_logical_width / 2 - newPiece.w / 2; + newPiece.y = resized_image_logical_height / 2 - newPiece.h / 2; + + pieces_logical.push_back(newPiece); + CreatePhysicalPieces(); + selected_piece = pieces_logical.size() - 1; +} + +void PuzzlePieceEditorState::ProcessInput(const SDL_Event& event, bool& processed) { + if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_ESCAPE) { + isActive = false; + processed = true; + } + else if (event.key.keysym.sym == SDLK_DELETE || event.key.keysym.sym == SDLK_BACKSPACE) { + DeleteSelectedPiece(); + processed = true; + } + else if (event.key.keysym.sym == SDLK_n) { + AddNewPiece(); + processed = true; + } + else if (event.key.keysym.sym == SDLK_s && (SDL_GetModState() & KMOD_CTRL)) { + SavePieces(); + processed = true; + } + } + if (event.type == SDL_WINDOWEVENT) { + if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + ResizeImagePhysical(); + } + } + if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { + int mouseX = event.button.x; + int mouseY = event.button.y; + // Check if clicking on a piece + bool foundPiece = false; + for (int i = pieces_physical.size() - 1; i >= 0; --i) { + DragMode mode = GetDragModeForPosition(mouseX, mouseY, i); + if (mode != DragMode::NONE) { + selected_piece = i; + dragMode = mode; + dragStartX = mouseX; + dragStartY = mouseY; + dragStartRect = pieces_logical[i]; + foundPiece = true; + processed = true; + break; + } + } + if (!foundPiece) { + selected_piece = -1; + } + } + if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { + dragMode = DragMode::NONE; + } + ImGui_ImplSDL2_ProcessEvent(&event); +} + +void PuzzlePieceEditorState::Draw(SDL_Renderer* target) { + SDL_SetRenderDrawColor(target, 40, 40, 40, 255); + SDL_RenderClear(target); + // Draw title + SDL_SetRenderDrawColor(target, 60, 60, 60, 255); + SDL_Rect titleBar = {0, 0, globalData.xsize, 35}; + SDL_RenderFillRect(target, &titleBar); + // Draw the image + SDL_Rect imageRect; + imageRect.x = globalData.xsize / 2 - resized_image_physical_width / 2; + imageRect.y = globalData.ysize / 2 - resized_image_physical_height / 2 + 40; + imageRect.h = resized_image_physical_height; + imageRect.w = resized_image_physical_width; + if (pictureTex) { + SDL_RenderCopy(target, pictureTex, NULL, &imageRect); + } + // Draw piece outlines and handles + for (size_t i = 0; i < pieces_physical.size(); ++i) { + const SDL_Rect& piece = pieces_physical[i]; + SDL_Rect pieceRect; + pieceRect.x = imageRect.x + piece.x; + pieceRect.y = imageRect.y + piece.y; + pieceRect.w = piece.w; + pieceRect.h = piece.h; + // Draw piece outline + if (i == selected_piece) { + rectangleRGBA(target, pieceRect.x, pieceRect.y, pieceRect.x + pieceRect.w, pieceRect.y + pieceRect.h, 255, 255, 0, 255); + rectangleRGBA(target, pieceRect.x + 1, pieceRect.y + 1, pieceRect.x + pieceRect.w - 1, pieceRect.y + pieceRect.h - 1, 255, 255, 0, 255); + + // Draw resize handles for selected piece + SDL_SetRenderDrawColor(target, 255, 255, 0, 255); + + // Corner handles + SDL_Rect handle; + handle.w = HANDLE_SIZE * 2; + handle.h = HANDLE_SIZE * 2; + + // NW + handle.x = pieceRect.x - HANDLE_SIZE; + handle.y = pieceRect.y - HANDLE_SIZE; + SDL_RenderFillRect(target, &handle); + + // NE + handle.x = pieceRect.x + pieceRect.w - HANDLE_SIZE; + handle.y = pieceRect.y - HANDLE_SIZE; + SDL_RenderFillRect(target, &handle); + + // SW + handle.x = pieceRect.x - HANDLE_SIZE; + handle.y = pieceRect.y + pieceRect.h - HANDLE_SIZE; + SDL_RenderFillRect(target, &handle); + + // SE + handle.x = pieceRect.x + pieceRect.w - HANDLE_SIZE; + handle.y = pieceRect.y + pieceRect.h - HANDLE_SIZE; + SDL_RenderFillRect(target, &handle); + } else { + rectangleRGBA(target, pieceRect.x, pieceRect.y, pieceRect.x + pieceRect.w, pieceRect.y + pieceRect.h, 100, 200, 255, 200); + } + } + + // Draw UI with ImGui + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save (Ctrl+S)", nullptr, false, true)) { + SavePieces(); + } + if (ImGui::MenuItem("Close", "ESC")) { + isActive = false; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Add Piece", "N")) { + AddNewPiece(); + } + if (ImGui::MenuItem("Delete Selected", "Del", false, selected_piece >= 0)) { + DeleteSelectedPiece(); + } + ImGui::EndMenu(); + } + ImGui::Text(" | Pieces: %zu", pieces_logical.size()); + if (selected_piece >= 0) { + ImGui::Text(" | Selected: #%d", selected_piece + 1); + } + ImGui::EndMainMenuBar(); + } + + // Instructions window + ImGui::SetNextWindowPos(ImVec2(10, 45), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver); + ImGui::Begin("Instructions"); + ImGui::Text("Piece Editor"); + ImGui::Separator(); + ImGui::BulletText("Click to select a piece"); + ImGui::BulletText("Drag to move selected piece"); + ImGui::BulletText("Drag corners/edges to resize"); + ImGui::BulletText("Press N to add new piece"); + ImGui::BulletText("Press Delete to remove selected"); + ImGui::BulletText("Press Ctrl+S to save"); + ImGui::BulletText("Press ESC to close"); + ImGui::End(); +} + +void PuzzlePieceEditorState::Update() { + if (dragMode != DragMode::NONE && (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(1))) { + UpdateDrag(); + } +} diff --git a/src/PuzzlePieceEditorState.hpp b/src/PuzzlePieceEditorState.hpp new file mode 100644 index 0000000..b3d4fa3 --- /dev/null +++ b/src/PuzzlePieceEditorState.hpp @@ -0,0 +1,99 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/sago_multi_scrambler_puzzle2 +=========================================================================== +*/ + +#ifndef PUZZLEPIECEEDITORSTATE_HPP +#define PUZZLEPIECEEDITORSTATE_HPP + +#include "sago/GameStateInterface.hpp" +#include "SDL.h" +#include +#include + +class PuzzlePieceEditorState : public sago::GameStateInterface { +public: + PuzzlePieceEditorState(); + PuzzlePieceEditorState(const std::string& imageFilePath, const std::string& pictureId, SDL_Texture* texture, int sourceWidth, int sourceHeight, const std::vector& initialPieces); + PuzzlePieceEditorState(const PuzzlePieceEditorState& orig) = delete; + virtual ~PuzzlePieceEditorState(); + + bool IsActive() override; + void ProcessInput(const SDL_Event& event, bool& processed) override; + void Draw(SDL_Renderer* target) override; + void Update() override; + + const std::vector& GetEditedPieces() const { return pieces_logical; } + bool WasSaved() const { return saved; } + +private: + enum class DragMode { + NONE, + MOVE, + RESIZE_N, + RESIZE_S, + RESIZE_E, + RESIZE_W, + RESIZE_NE, + RESIZE_NW, + RESIZE_SE, + RESIZE_SW + }; + + void ResizeImagePhysical(); + void CreatePhysicalPieces(); + void SavePieces(); + void DeleteSelectedPiece(); + void AddNewPiece(); + DragMode GetDragModeForPosition(int x, int y, int pieceIndex); + void UpdateDrag(); + + bool isActive = true; + bool saved = false; + SDL_Texture* pictureTex = nullptr; + std::string imageFilePath; + std::string pictureId; + + int source_image_width = 1; + int source_image_height = 1; + + int resized_image_logical_width = 1100; + int resized_image_logical_height = 700; + const int resized_image_logical_width_max = 1100; + const int resized_image_logical_height_max = 700; + + int resized_image_physical_width = 1100; + int resized_image_physical_height = 700; + + std::vector pieces_logical; + std::vector pieces_physical; + + int selected_piece = -1; + DragMode dragMode = DragMode::NONE; + int dragStartX = 0; + int dragStartY = 0; + SDL_Rect dragStartRect; + + const int HANDLE_SIZE = 8; + const int MIN_PIECE_SIZE = 50; +}; + +#endif // PUZZLEPIECEEDITORSTATE_HPP diff --git a/src/PuzzleSingleImageState.cpp b/src/PuzzleSingleImageState.cpp index affc734..c4ac61f 100644 --- a/src/PuzzleSingleImageState.cpp +++ b/src/PuzzleSingleImageState.cpp @@ -22,6 +22,7 @@ https://github.com/sago007/saland */ #include "PuzzleSingleImageState.hpp" +#include "PuzzlePieceEditorState.hpp" #include "SDL_image.h" #include #include "globals.hpp" @@ -31,6 +32,7 @@ https://github.com/sago007/saland #include "SagoImGui.hpp" #include "rhash.hpp" #include "config.hpp" +#include "sago_common.hpp" PuzzleSingleImageState::PuzzleSingleImageState() { std::map config = LoadConfigMap(); @@ -163,6 +165,9 @@ void PuzzleSingleImageState::Draw(SDL_Renderer* target) { if (ImGui::MenuItem("Shuffle")) { Shuffle(); } + if (rectangularMode && ImGui::MenuItem("Edit Piece Layout")) { + shouldLaunchEditor = true; + } if (imageFilePath.length()) { bool isFav = IsFavorite(imageFilePath); const char* favoriteText = isFav ? "Remove from Favorites" : "Add to Favorites"; @@ -202,6 +207,13 @@ void PuzzleSingleImageState::Draw(SDL_Renderer* target) { void PuzzleSingleImageState::Update() { + // Launch editor if requested (must be done outside of Draw/ImGui frame) + if (shouldLaunchEditor) { + shouldLaunchEditor = false; + LaunchPieceEditor(); + return; + } + static Uint32 lastTime = SDL_GetTicks(); Uint32 currentTime = SDL_GetTicks(); float deltaTime = (currentTime - lastTime) / 1000.0f; @@ -319,8 +331,14 @@ void PuzzleSingleImageState::LoadPictureFromFile(const std::string& filename, SD pieces_logical.clear(); if (rectangularMode) { - // Create a 4x4 grid of rectangular pieces - CreateRectangularPieces(4, 4); + // Try to load custom piece layout first + if (!LoadCustomPieceLayout(picture_id, pieces_logical)) { + // Create a 4x4 grid of rectangular pieces if no custom layout exists + CreateRectangularPieces(4, 4); + } + else { + std::cerr << "Loaded custom piece layout with " << pieces_logical.size() << " pieces\n"; + } } else { // Use the original splitting algorithm @@ -504,4 +522,27 @@ void PuzzleSingleImageState::CheckSolved() { puzzleSolved = true; confetti.Burst(globalData.xsize, globalData.ysize); } +} + +void PuzzleSingleImageState::LaunchPieceEditor() { + if (!rectangularMode || !pictureTex) { + return; + } + + // Create and run the editor state + PuzzlePieceEditorState editor(imageFilePath, picture_id, pictureTex, + source_image_width, source_image_height, + pieces_logical); + RunGameState(editor); + + // If the editor saved changes, reload the pieces + if (editor.WasSaved()) { + std::vector newPieces = editor.GetEditedPieces(); + if (!newPieces.empty()) { + pieces_logical = newPieces; + CreatePhysicalPieces(); + Shuffle(); + std::cout << "Applied edited piece layout\n"; + } + } } \ No newline at end of file diff --git a/src/PuzzleSingleImageState.hpp b/src/PuzzleSingleImageState.hpp index cbffd2a..5a78212 100644 --- a/src/PuzzleSingleImageState.hpp +++ b/src/PuzzleSingleImageState.hpp @@ -52,10 +52,12 @@ public: void ResizeImage(); void ResizeImagePhysical(); void CreatePhysicalPieces(); + void LaunchPieceEditor(); bool flipMode = false; bool rectangularMode = false; private: + bool shouldLaunchEditor = false; void ClearPicture(); bool isActive = true; diff --git a/src/config.cpp b/src/config.cpp index 2067fb9..eccb050 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -178,3 +178,47 @@ bool IsFavorite(const std::string& imagePath) { return false; } + +bool LoadCustomPieceLayout(const std::string& pictureId, std::vector& pieces) { + std::string layoutPath = getPathToSaveFiles() + "/piece_layouts/" + pictureId + ".layout"; + std::ifstream file(layoutPath); + if (!file.is_open()) { + return false; + } + pieces.clear(); + int numPieces; + file >> numPieces; + for (int i = 0; i < numPieces; ++i) { + SDL_Rect piece; + file >> piece.x >> piece.y >> piece.w >> piece.h; + pieces.push_back(piece); + } + file.close(); + return !pieces.empty(); +} + +bool SaveCustomPieceLayout(const std::string& pictureId, const std::vector& pieces) { + std::string savePath = getPathToSaveFiles() + "/piece_layouts"; + OsCreateFolder(savePath); + + std::string layoutPath = savePath + "/" + pictureId + ".layout"; + std::ofstream file(layoutPath); + + if (!file.is_open()) { + return false; + } + + file << pieces.size() << std::endl; + for (const SDL_Rect& piece : pieces) { + file << piece.x << " " << piece.y << " " << piece.w << " " << piece.h << std::endl; + } + + file.close(); + return true; +} + +bool HasCustomPieceLayout(const std::string& pictureId) { + std::string layoutPath = getPathToSaveFiles() + "/piece_layouts/" + pictureId + ".layout"; + std::ifstream file(layoutPath); + return file.is_open(); +} diff --git a/src/config.hpp b/src/config.hpp index 0100d37..706f281 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -28,6 +28,7 @@ SOFTWARE. #include #include #include +#include "SDL.h" /** * @brief Loads configuration from file into a map @@ -123,4 +124,27 @@ void RemoveFavorite(const std::string& imagePath); */ bool IsFavorite(const std::string& imagePath); +/** + * @brief Loads custom piece layout for an image + * @param pictureId Unique identifier for the image (hash) + * @param pieces Vector to fill with piece rectangles + * @return true if custom layout was loaded, false otherwise + */ +bool LoadCustomPieceLayout(const std::string& pictureId, std::vector& pieces); + +/** + * @brief Saves custom piece layout for an image + * @param pictureId Unique identifier for the image (hash) + * @param pieces Vector of piece rectangles to save + * @return true if layout was saved successfully, false otherwise + */ +bool SaveCustomPieceLayout(const std::string& pictureId, const std::vector& pieces); + +/** + * @brief Checks if a custom piece layout exists for an image + * @param pictureId Unique identifier for the image (hash) + * @return true if custom layout exists, false otherwise + */ +bool HasCustomPieceLayout(const std::string& pictureId); + #endif // CONFIG_HPP