Files
yaze/src/app/editor/graphics/link_sprite_panel.cc

456 lines
14 KiB
C++

#include "app/editor/graphics/link_sprite_panel.h"
#include "absl/strings/str_format.h"
#include "app/gfx/resource/arena.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/style.h"
#include "util/file_util.h"
#include "rom/rom.h"
#include "imgui/imgui.h"
#include "util/log.h"
namespace yaze {
namespace editor {
LinkSpritePanel::LinkSpritePanel(GraphicsEditorState* state, Rom* rom)
: state_(state), rom_(rom) {}
void LinkSpritePanel::Initialize() {
preview_canvas_.SetCanvasSize(ImVec2(128 * preview_zoom_, 32 * preview_zoom_));
}
void LinkSpritePanel::Draw(bool* p_open) {
// EditorPanel interface - delegate to existing Update() logic
// Lazy-load Link sheets on first update
if (!sheets_loaded_ && rom_ && rom_->is_loaded()) {
auto status = LoadLinkSheets();
if (!status.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Failed to load Link sheets: %s",
status.message().data());
return;
}
}
DrawToolbar();
ImGui::Separator();
// Split layout: left side grid, right side preview
float panel_width = ImGui::GetContentRegionAvail().x;
float grid_width = std::min(300.0f, panel_width * 0.4f);
// Left column: Sheet grid
ImGui::BeginChild("##LinkSheetGrid", ImVec2(grid_width, 0), true);
DrawSheetGrid();
ImGui::EndChild();
ImGui::SameLine();
// Right column: Preview and controls
ImGui::BeginChild("##LinkPreviewArea", ImVec2(0, 0), true);
DrawPreviewCanvas();
ImGui::Separator();
DrawPaletteSelector();
ImGui::Separator();
DrawInfoPanel();
ImGui::EndChild();
}
absl::Status LinkSpritePanel::Update() {
// Lazy-load Link sheets on first update
if (!sheets_loaded_ && rom_ && rom_->is_loaded()) {
auto status = LoadLinkSheets();
if (!status.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Failed to load Link sheets: %s",
status.message().data());
return status;
}
}
DrawToolbar();
ImGui::Separator();
// Split layout: left side grid, right side preview
float panel_width = ImGui::GetContentRegionAvail().x;
float grid_width = std::min(300.0f, panel_width * 0.4f);
// Left column: Sheet grid
ImGui::BeginChild("##LinkSheetGrid", ImVec2(grid_width, 0), true);
DrawSheetGrid();
ImGui::EndChild();
ImGui::SameLine();
// Right column: Preview and controls
ImGui::BeginChild("##LinkPreviewArea", ImVec2(0, 0), true);
DrawPreviewCanvas();
ImGui::Separator();
DrawPaletteSelector();
ImGui::Separator();
DrawInfoPanel();
ImGui::EndChild();
return absl::OkStatus();
}
void LinkSpritePanel::DrawToolbar() {
if (ImGui::Button(ICON_MD_FILE_UPLOAD " Import ZSPR")) {
ImportZspr();
}
HOVER_HINT("Import a .zspr Link sprite file");
ImGui::SameLine();
if (ImGui::Button(ICON_MD_RESTORE " Reset to Vanilla")) {
ResetToVanilla();
}
HOVER_HINT("Reset Link graphics to vanilla ROM data");
// Show loaded ZSPR info
if (loaded_zspr_.has_value()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f),
"Loaded: %s",
loaded_zspr_->metadata.display_name.c_str());
}
// Unsaved changes indicator
if (has_unsaved_changes_) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "[Unsaved]");
}
}
void LinkSpritePanel::DrawSheetGrid() {
ImGui::Text("Link Sheets (14)");
ImGui::Separator();
// 4x4 grid (14 sheets + 2 empty slots)
const float cell_size = kThumbnailSize + kThumbnailPadding * 2;
int col = 0;
for (int i = 0; i < kNumLinkSheets; i++) {
if (col > 0) {
ImGui::SameLine();
}
ImGui::PushID(i);
DrawSheetThumbnail(i);
ImGui::PopID();
col++;
if (col >= 4) {
col = 0;
}
}
}
void LinkSpritePanel::DrawSheetThumbnail(int sheet_index) {
bool is_selected = (selected_sheet_ == sheet_index);
// Selection highlight
if (is_selected) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.3f, 0.5f, 0.8f, 0.4f));
}
ImGui::BeginChild(absl::StrFormat("##LinkSheet%d", sheet_index).c_str(),
ImVec2(kThumbnailSize + kThumbnailPadding,
kThumbnailSize + 16 + kThumbnailPadding),
true, ImGuiWindowFlags_NoScrollbar);
// Draw thumbnail
auto& sheet = link_sheets_[sheet_index];
if (sheet.is_active()) {
// Ensure texture exists
if (!sheet.texture() && sheet.surface()) {
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE,
const_cast<gfx::Bitmap*>(&sheet));
}
if (sheet.texture()) {
ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddImage(
(ImTextureID)(intptr_t)sheet.texture(),
cursor_pos,
ImVec2(cursor_pos.x + kThumbnailSize,
cursor_pos.y + kThumbnailSize / 4)); // 128x32 aspect
}
}
// Click handling
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
selected_sheet_ = sheet_index;
}
// Double-click to open in pixel editor
if (ImGui::IsWindowHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
OpenSheetInPixelEditor();
}
// Sheet label
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + kThumbnailSize / 4 + 2);
ImGui::Text("%d", sheet_index);
ImGui::EndChild();
if (is_selected) {
ImGui::PopStyleColor();
}
// Tooltip
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Link Sheet %d", sheet_index);
ImGui::Text("Double-click to edit");
ImGui::EndTooltip();
}
}
void LinkSpritePanel::DrawPreviewCanvas() {
ImGui::Text("Sheet %d Preview", selected_sheet_);
// Preview canvas
float canvas_width = ImGui::GetContentRegionAvail().x - 16;
float canvas_height = canvas_width / 4; // 4:1 aspect ratio (128x32)
preview_canvas_.SetCanvasSize(ImVec2(canvas_width, canvas_height));
const float grid_step = 8.0f * (canvas_width / 128.0f);
{
gui::CanvasFrameOptions frame_opts;
frame_opts.canvas_size = ImVec2(canvas_width, canvas_height);
frame_opts.draw_context_menu = false;
frame_opts.draw_grid = true;
frame_opts.grid_step = grid_step;
auto rt = gui::BeginCanvas(preview_canvas_, frame_opts);
auto& sheet = link_sheets_[selected_sheet_];
if (sheet.is_active() && sheet.texture()) {
gui::BitmapDrawOpts draw_opts;
draw_opts.dest_pos = ImVec2(0, 0);
draw_opts.dest_size = ImVec2(canvas_width, canvas_height);
draw_opts.ensure_texture = false;
gui::DrawBitmap(rt, sheet, draw_opts);
}
gui::EndCanvas(preview_canvas_, rt, frame_opts);
}
ImGui::Spacing();
// Open in editor button
if (ImGui::Button(ICON_MD_EDIT " Open in Pixel Editor")) {
OpenSheetInPixelEditor();
}
HOVER_HINT("Open this sheet in the main pixel editor");
// Zoom slider
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
ImGui::SliderFloat("Zoom", &preview_zoom_, 1.0f, 8.0f, "%.1fx");
}
void LinkSpritePanel::DrawPaletteSelector() {
ImGui::Text("Display Palette:");
ImGui::SameLine();
const char* palette_names[] = {"Green Mail", "Blue Mail", "Red Mail", "Bunny"};
int current = static_cast<int>(selected_palette_);
ImGui::SetNextItemWidth(120);
if (ImGui::Combo("##PaletteSelect", &current, palette_names, 4)) {
selected_palette_ = static_cast<PaletteType>(current);
ApplySelectedPalette();
}
HOVER_HINT("Change the display palette for preview");
}
void LinkSpritePanel::DrawInfoPanel() {
ImGui::Text("Info:");
ImGui::BulletText("896 total tiles (8x8 each)");
ImGui::BulletText("14 graphics sheets");
ImGui::BulletText("4BPP format");
if (loaded_zspr_.has_value()) {
ImGui::Separator();
ImGui::Text("Loaded ZSPR:");
ImGui::BulletText("Name: %s", loaded_zspr_->metadata.display_name.c_str());
ImGui::BulletText("Author: %s", loaded_zspr_->metadata.author.c_str());
ImGui::BulletText("Tiles: %zu", loaded_zspr_->tile_count());
}
}
void LinkSpritePanel::ImportZspr() {
// Open file dialog for .zspr files
auto file_path = util::FileDialogWrapper::ShowOpenFileDialog();
if (file_path.empty()) {
return;
}
LOG_INFO("LinkSpritePanel", "Importing ZSPR: %s", file_path.c_str());
// Load ZSPR file
auto zspr_result = gfx::ZsprLoader::LoadFromFile(file_path);
if (!zspr_result.ok()) {
LOG_ERROR("LinkSpritePanel", "Failed to load ZSPR: %s",
zspr_result.status().message().data());
return;
}
loaded_zspr_ = std::move(zspr_result.value());
// Verify it's a Link sprite
if (!loaded_zspr_->is_link_sprite()) {
LOG_ERROR("LinkSpritePanel", "ZSPR is not a Link sprite (type=%d)",
loaded_zspr_->metadata.sprite_type);
loaded_zspr_.reset();
return;
}
// Apply to ROM
if (rom_ && rom_->is_loaded()) {
auto status = gfx::ZsprLoader::ApplyToRom(*rom_, *loaded_zspr_);
if (!status.ok()) {
LOG_ERROR("LinkSpritePanel", "Failed to apply ZSPR to ROM: %s",
status.message().data());
return;
}
// Also apply palette
status = gfx::ZsprLoader::ApplyPaletteToRom(*rom_, *loaded_zspr_);
if (!status.ok()) {
LOG_WARN("LinkSpritePanel", "Failed to apply ZSPR palette: %s",
status.message().data());
}
// Reload Link sheets to reflect changes
sheets_loaded_ = false;
has_unsaved_changes_ = true;
LOG_INFO("LinkSpritePanel", "ZSPR '%s' imported successfully",
loaded_zspr_->metadata.display_name.c_str());
}
}
void LinkSpritePanel::ResetToVanilla() {
// TODO: Implement reset to vanilla
// This would require keeping a backup of the original Link graphics
// or reloading from a vanilla ROM file
LOG_WARN("LinkSpritePanel", "Reset to vanilla not yet implemented");
loaded_zspr_.reset();
}
void LinkSpritePanel::OpenSheetInPixelEditor() {
// Signal to open the selected Link sheet in the main pixel editor
// Link sheets are separate from the main 223 sheets, so we need
// a special handling mechanism
// For now, log the intent - full integration requires additional state
LOG_INFO("LinkSpritePanel", "Request to open Link sheet %d in pixel editor",
selected_sheet_);
// TODO: Add Link sheet to open_sheets with a special identifier
// or add a link_sheets_to_edit set to GraphicsEditorState
}
absl::Status LinkSpritePanel::LoadLinkSheets() {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Use the existing LoadLinkGraphics function
auto result = zelda3::LoadLinkGraphics(*rom_);
if (!result.ok()) {
return result.status();
}
link_sheets_ = std::move(result.value());
sheets_loaded_ = true;
LOG_INFO("LinkSpritePanel", "Loaded %d Link graphics sheets", zelda3::kNumLinkSheets);
// Apply default palette for display
ApplySelectedPalette();
return absl::OkStatus();
}
void LinkSpritePanel::ApplySelectedPalette() {
if (!rom_ || !rom_->is_loaded()) return;
// Get the appropriate palette based on selection
// Link palettes are in Group 4 (Sprites Aux1) and Group 5 (Sprites Aux2)
// Green Mail: Group 4, Index 0 (Standard Link)
// Blue Mail: Group 4, Index 0 (Standard Link) - but with different colors in game
// Red Mail: Group 4, Index 0 (Standard Link) - but with different colors in game
// Bunny: Group 4, Index 1 (Bunny Link)
// For now, we'll use the standard sprite palettes from GameData if available
// In a full implementation, we would load the specific mail palettes
// Default to Green Mail (Standard Link palette)
const gfx::SnesPalette* palette = nullptr;
// We need access to GameData to get the palettes
// Since we don't have direct access to GameData here (only Rom), we'll try to find it
// or use a hardcoded fallback if necessary.
// Ideally, LinkSpritePanel should have access to GameData.
// For this fix, we will assume the standard sprite palette location in ROM if GameData isn't available,
// or use a simplified approach.
// Actually, we can get GameData from the main Editor instance if we had access,
// but we only have Rom. Let's try to read the palette directly from ROM for now
// to ensure it works without refactoring the whole dependency injection.
// Standard Link Palette (Green Mail) is usually at 0x1BD318 (PC) / 0x37D318 (SNES) in vanilla
// But we should use the loaded palette data if possible.
// Let's use a safe fallback: Create a default Link palette
static gfx::SnesPalette default_palette;
if (default_palette.empty()) {
// Basic Green Mail colors (approximate)
default_palette.Resize(16);
default_palette[0] = gfx::SnesColor(0, 0, 0); // Transparent
default_palette[1] = gfx::SnesColor(24, 24, 24); // Tunic Dark
default_palette[2] = gfx::SnesColor(0, 19, 0); // Tunic Green
default_palette[3] = gfx::SnesColor(255, 255, 255); // White
default_palette[4] = gfx::SnesColor(255, 165, 66); // Skin
default_palette[5] = gfx::SnesColor(255, 100, 50); // Skin Dark
default_palette[6] = gfx::SnesColor(255, 0, 0); // Red
default_palette[7] = gfx::SnesColor(255, 255, 0); // Yellow
// ... fill others as needed
}
// If we can't get the real palette, use default
palette = &default_palette;
// Apply to all Link sheets
for (auto& sheet : link_sheets_) {
if (sheet.is_active() && sheet.surface()) {
// Use the palette
sheet.SetPaletteWithTransparent(*palette, 0);
// Force texture update
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &sheet);
}
}
LOG_INFO("LinkSpritePanel", "Applied palette %s to %zu sheets",
GetPaletteName(selected_palette_), link_sheets_.size());
}
const char* LinkSpritePanel::GetPaletteName(PaletteType type) {
switch (type) {
case PaletteType::kGreenMail: return "Green Mail";
case PaletteType::kBlueMail: return "Blue Mail";
case PaletteType::kRedMail: return "Red Mail";
case PaletteType::kBunny: return "Bunny";
default: return "Unknown";
}
}
} // namespace editor
} // namespace yaze