git repos / sago_multi_scrambler_puzzle2

src/PuzzlePieceEditorState.cpp

browsing at commit = 2d9bff86eda4975ad92ec1072b994971fd13b7ba

raw · blame · history

/*
===========================================================================
 * 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 "sago/SagoMisc.hpp"
#include <SDL2/SDL2_gfxPrimitives.h>
#include <iostream>
#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 layoutPath = "piece_layouts/" + pictureId + ".layout";
	std::ostringstream oss;
	oss << pieces_logical.size() << "\n";
	for (const SDL_Rect& piece : pieces_logical) {
		oss << piece.x << " " << piece.y << " " << piece.w << " " << piece.h << "\n";
	}
	sago::WriteFileContent(layoutPath.c_str(), oss.str());
	saved = true;
	std::cout << "Saved piece layout to " << layoutPath << 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();
	}
}