commit fd30486e
Add editor
Changed files
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 <SDL2/SDL2_gfxPrimitives.h>
+#include <iostream>
+#include <fstream>
+#include <sstream>
+
+PuzzlePieceEditorState::PuzzlePieceEditorState() {
+}
+
+PuzzlePieceEditorState::PuzzlePieceEditorState(const std::string& imageFilePath, const std::string& pictureId,
+ SDL_Texture* texture, int sourceWidth, int sourceHeight,
+ const std::vector<SDL_Rect>& 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 <string>
+#include <vector>
+
+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<SDL_Rect>& 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<SDL_Rect>& 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<SDL_Rect> pieces_logical;
+ std::vector<SDL_Rect> 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 <iostream>
#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<std::string, std::string> 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<SDL_Rect> 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<SDL_Rect>& 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<SDL_Rect>& 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 <string>
#include <map>
#include <vector>
+#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<SDL_Rect>& 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<SDL_Rect>& 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