backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -4,21 +4,25 @@
#include "absl/strings/str_cat.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gfx/types/snes_tile.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/core/color.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/input.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#include "rom/rom.h"
namespace yaze {
namespace editor {
using ImGui::BeginChild;
using ImGui::BeginCombo;
using ImGui::BeginGroup;
using ImGui::BeginTabBar;
using ImGui::BeginTabItem;
using ImGui::BeginTable;
using ImGui::EndChild;
using ImGui::EndCombo;
using ImGui::EndGroup;
using ImGui::EndTabBar;
using ImGui::EndTabItem;
@@ -29,8 +33,10 @@ using ImGui::IsItemClicked;
using ImGui::PopID;
using ImGui::PushID;
using ImGui::SameLine;
using ImGui::Selectable;
using ImGui::Separator;
using ImGui::SetNextItemWidth;
using ImGui::SliderFloat;
using ImGui::TableHeadersRow;
using ImGui::TableNextColumn;
using ImGui::TableNextRow;
@@ -40,11 +46,50 @@ using ImGui::Text;
using gfx::kPaletteGroupNames;
using gfx::PaletteCategory;
namespace {
// Constants for sheet display
constexpr int kSheetDisplayWidth = 256; // 2x scale from 128px sheets
constexpr int kSheetDisplayHeight = 64; // 2x scale from 32px sheets
constexpr float kDefaultScale = 2.0f;
constexpr int kTileSize = 16; // 8px tiles at 2x scale
// Draw a single sheet with proper scaling and unique ID
void DrawScaledSheet(gui::Canvas& canvas, gfx::Bitmap& sheet, int unique_id,
float scale = kDefaultScale) {
PushID(unique_id);
// Calculate scaled dimensions
int display_width =
static_cast<int>(gfx::kTilesheetWidth * scale);
int display_height =
static_cast<int>(gfx::kTilesheetHeight * scale);
// Draw canvas background
canvas.DrawBackground(ImVec2(display_width + 1, display_height + 1));
canvas.DrawContextMenu();
// Draw bitmap with proper scale
canvas.DrawBitmap(sheet, 2, scale);
// Draw grid at scaled tile size
canvas.DrawGrid(static_cast<int>(8 * scale));
canvas.DrawOverlay();
PopID();
}
} // namespace
absl::Status GfxGroupEditor::Update() {
if (BeginTabBar("GfxGroupEditor")) {
if (BeginTabItem("Main")) {
// Palette controls at top for all tabs
DrawPaletteControls();
Separator();
if (BeginTabBar("##GfxGroupEditorTabs")) {
if (BeginTabItem("Blocksets")) {
gui::InputHexByte("Selected Blockset", &selected_blockset_,
(uint8_t)0x24);
static_cast<uint8_t>(0x24));
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "blockset", "0x" + std::to_string(selected_blockset_),
"Blockset " + std::to_string(selected_blockset_));
@@ -52,8 +97,9 @@ absl::Status GfxGroupEditor::Update() {
EndTabItem();
}
if (BeginTabItem("Rooms")) {
gui::InputHexByte("Selected Blockset", &selected_roomset_, (uint8_t)81);
if (BeginTabItem("Roomsets")) {
gui::InputHexByte("Selected Roomset", &selected_roomset_,
static_cast<uint8_t>(81));
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "roomset", "0x" + std::to_string(selected_roomset_),
"Roomset " + std::to_string(selected_roomset_));
@@ -61,22 +107,16 @@ absl::Status GfxGroupEditor::Update() {
EndTabItem();
}
if (BeginTabItem("Sprites")) {
if (BeginTabItem("Spritesets")) {
gui::InputHexByte("Selected Spriteset", &selected_spriteset_,
(uint8_t)143);
static_cast<uint8_t>(143));
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "spriteset", "0x" + std::to_string(selected_spriteset_),
"Spriteset " + std::to_string(selected_spriteset_));
Text("Values");
DrawSpritesetViewer();
EndTabItem();
}
if (BeginTabItem("Palettes")) {
DrawPaletteViewer();
EndTabItem();
}
EndTabBar();
}
@@ -84,6 +124,13 @@ absl::Status GfxGroupEditor::Update() {
}
void GfxGroupEditor::DrawBlocksetViewer(bool sheet_only) {
if (!game_data()) {
Text("No game data loaded");
return;
}
PushID("BlocksetViewer");
if (BeginTable("##BlocksetTable", sheet_only ? 1 : 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
ImVec2(0, 0))) {
@@ -92,90 +139,125 @@ void GfxGroupEditor::DrawBlocksetViewer(bool sheet_only) {
GetContentRegionAvail().x);
}
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256);
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
kSheetDisplayWidth + 16);
TableHeadersRow();
TableNextRow();
if (!sheet_only) {
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 8; i++) {
SetNextItemWidth(100.f);
gui::InputHexByte(("0x" + std::to_string(i)).c_str(),
&rom()->main_blockset_ids[selected_blockset_][i]);
}
EndGroup();
}
}
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 8; i++) {
int sheet_id = rom()->main_blockset_ids[selected_blockset_][i];
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
gui::BitmapCanvasPipeline(blockset_canvas_, sheet, 256, 0x10 * 0x04,
0x20, true, false, 22);
for (int idx = 0; idx < 8; idx++) {
SetNextItemWidth(100.f);
gui::InputHexByte(
("Slot " + std::to_string(idx)).c_str(),
&game_data()->main_blockset_ids[selected_blockset_][idx]);
}
EndGroup();
}
TableNextColumn();
BeginGroup();
for (int idx = 0; idx < 8; idx++) {
int sheet_id = game_data()->main_blockset_ids[selected_blockset_][idx];
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
// Apply current palette if selected
if (use_custom_palette_ && current_palette_) {
sheet.SetPalette(*current_palette_);
gfx::Arena::Get().NotifySheetModified(sheet_id);
}
// Unique ID combining blockset, slot, and sheet
int unique_id = (selected_blockset_ << 16) | (idx << 8) | sheet_id;
DrawScaledSheet(blockset_canvases_[idx], sheet, unique_id, view_scale_);
}
EndGroup();
EndTable();
}
PopID();
}
void GfxGroupEditor::DrawRoomsetViewer() {
Text("Values - Overwrites 4 of main blockset");
if (BeginTable("##Roomstable", 3,
if (!game_data()) {
Text("No game data loaded");
return;
}
PushID("RoomsetViewer");
Text("Roomsets overwrite slots 4-7 of the main blockset");
if (BeginTable("##RoomsTable", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
ImVec2(0, 0))) {
TableSetupColumn("List", ImGuiTableColumnFlags_WidthFixed, 100);
TableSetupColumn("List", ImGuiTableColumnFlags_WidthFixed, 120);
TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch,
GetContentRegionAvail().x);
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256);
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
kSheetDisplayWidth + 16);
TableHeadersRow();
TableNextRow();
// Roomset list column
TableNextColumn();
{
BeginChild("##RoomsetList");
for (int i = 0; i < 0x51; i++) {
BeginGroup();
std::string roomset_label = absl::StrFormat("0x%02X", i);
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "roomset", roomset_label, "Roomset " + roomset_label);
if (IsItemClicked()) {
selected_roomset_ = i;
if (BeginChild("##RoomsetListChild", ImVec2(0, 300))) {
for (int idx = 0; idx < 0x51; idx++) {
PushID(idx);
std::string roomset_label = absl::StrFormat("0x%02X", idx);
bool is_selected = (selected_roomset_ == idx);
if (Selectable(roomset_label.c_str(), is_selected)) {
selected_roomset_ = idx;
}
EndGroup();
PopID();
}
EndChild();
}
EndChild();
// Inputs column
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 4; i++) {
SetNextItemWidth(100.f);
gui::InputHexByte(("0x" + std::to_string(i)).c_str(),
&rom()->room_blockset_ids[selected_roomset_][i]);
}
EndGroup();
BeginGroup();
Text("Sheet IDs (overwrites slots 4-7):");
for (int idx = 0; idx < 4; idx++) {
SetNextItemWidth(100.f);
gui::InputHexByte(
("Slot " + std::to_string(idx + 4)).c_str(),
&game_data()->room_blockset_ids[selected_roomset_][idx]);
}
EndGroup();
// Sheets column
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 4; i++) {
int sheet_id = rom()->room_blockset_ids[selected_roomset_][i];
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
gui::BitmapCanvasPipeline(roomset_canvas_, sheet, 256, 0x10 * 0x04,
0x20, true, false, 23);
BeginGroup();
for (int idx = 0; idx < 4; idx++) {
int sheet_id = game_data()->room_blockset_ids[selected_roomset_][idx];
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
// Apply current palette if selected
if (use_custom_palette_ && current_palette_) {
sheet.SetPalette(*current_palette_);
gfx::Arena::Get().NotifySheetModified(sheet_id);
}
EndGroup();
// Unique ID combining roomset, slot, and sheet
int unique_id = (0x1000) | (selected_roomset_ << 8) | (idx << 4) | sheet_id;
DrawScaledSheet(roomset_canvases_[idx], sheet, unique_id, view_scale_);
}
EndGroup();
EndTable();
}
PopID();
}
void GfxGroupEditor::DrawSpritesetViewer(bool sheet_only) {
if (!game_data()) {
Text("No game data loaded");
return;
}
PushID("SpritesetViewer");
if (BeginTable("##SpritesTable", sheet_only ? 1 : 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
ImVec2(0, 0))) {
@@ -183,35 +265,47 @@ void GfxGroupEditor::DrawSpritesetViewer(bool sheet_only) {
TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch,
GetContentRegionAvail().x);
}
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256);
TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
kSheetDisplayWidth + 16);
TableHeadersRow();
TableNextRow();
if (!sheet_only) {
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 4; i++) {
SetNextItemWidth(100.f);
gui::InputHexByte(("0x" + std::to_string(i)).c_str(),
&rom()->spriteset_ids[selected_spriteset_][i]);
}
EndGroup();
}
}
TableNextColumn();
{
BeginGroup();
for (int i = 0; i < 4; i++) {
int sheet_id = rom()->spriteset_ids[selected_spriteset_][i];
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(115 + sheet_id);
gui::BitmapCanvasPipeline(spriteset_canvas_, sheet, 256, 0x10 * 0x04,
0x20, true, false, 24);
Text("Sprite sheet IDs (base 115+):");
for (int idx = 0; idx < 4; idx++) {
SetNextItemWidth(100.f);
gui::InputHexByte(
("Slot " + std::to_string(idx)).c_str(),
&game_data()->spriteset_ids[selected_spriteset_][idx]);
}
EndGroup();
}
TableNextColumn();
BeginGroup();
for (int idx = 0; idx < 4; idx++) {
int sheet_offset = game_data()->spriteset_ids[selected_spriteset_][idx];
int sheet_id = 115 + sheet_offset;
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
// Apply current palette if selected
if (use_custom_palette_ && current_palette_) {
sheet.SetPalette(*current_palette_);
gfx::Arena::Get().NotifySheetModified(sheet_id);
}
// Unique ID combining spriteset, slot, and sheet
int unique_id =
(0x2000) | (selected_spriteset_ << 8) | (idx << 4) | sheet_offset;
DrawScaledSheet(spriteset_canvases_[idx], sheet, unique_id, view_scale_);
}
EndGroup();
EndTable();
}
PopID();
}
namespace {
@@ -219,84 +313,137 @@ void DrawPaletteFromPaletteGroup(gfx::SnesPalette& palette) {
if (palette.empty()) {
return;
}
for (size_t n = 0; n < palette.size(); n++) {
PushID(n);
if ((n % 8) != 0)
for (size_t color_idx = 0; color_idx < palette.size(); color_idx++) {
PushID(static_cast<int>(color_idx));
if ((color_idx % 8) != 0) {
SameLine(0.0f, GetStyle().ItemSpacing.y);
}
// Small icon of the color in the palette
if (gui::SnesColorButton(absl::StrCat("Palette", n), palette[n],
ImGuiColorEditFlags_NoAlpha |
ImGuiColorEditFlags_NoPicker |
ImGuiColorEditFlags_NoTooltip)) {}
gui::SnesColorButton(absl::StrCat("Palette", color_idx), palette[color_idx],
ImGuiColorEditFlags_NoAlpha |
ImGuiColorEditFlags_NoPicker |
ImGuiColorEditFlags_NoTooltip);
PopID();
}
}
} // namespace
void GfxGroupEditor::DrawPaletteViewer() {
if (!rom()->is_loaded()) {
void GfxGroupEditor::DrawPaletteControls() {
if (!game_data()) {
return;
}
gui::InputHexByte("Selected Paletteset", &selected_paletteset_);
if (selected_paletteset_ >= 71) {
selected_paletteset_ = 71;
// View scale control
Text(ICON_MD_ZOOM_IN " View");
SameLine();
SetNextItemWidth(100.f);
SliderFloat("##ViewScale", &view_scale_, 1.0f, 4.0f, "%.1fx");
SameLine();
// Palette category selector
Text(ICON_MD_PALETTE " Palette");
SameLine();
SetNextItemWidth(150.f);
// Use the category names array for display
static constexpr int kNumPaletteCategories = 14;
if (BeginCombo("##PaletteCategory",
gfx::kPaletteCategoryNames[selected_palette_category_].data())) {
for (int cat = 0; cat < kNumPaletteCategories; cat++) {
auto category = static_cast<PaletteCategory>(cat);
bool is_selected = (selected_palette_category_ == category);
if (Selectable(gfx::kPaletteCategoryNames[category].data(), is_selected)) {
selected_palette_category_ = category;
selected_palette_index_ = 0;
UpdateCurrentPalette();
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
EndCombo();
}
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "paletteset", "0x" + std::to_string(selected_paletteset_),
"Paletteset " + std::to_string(selected_paletteset_));
uint8_t& dungeon_main_palette_val =
rom()->paletteset_ids[selected_paletteset_][0];
uint8_t& dungeon_spr_pal_1_val =
rom()->paletteset_ids[selected_paletteset_][1];
uint8_t& dungeon_spr_pal_2_val =
rom()->paletteset_ids[selected_paletteset_][2];
uint8_t& dungeon_spr_pal_3_val =
rom()->paletteset_ids[selected_paletteset_][3];
gui::InputHexByte("Dungeon Main", &dungeon_main_palette_val);
rom()->resource_label()->SelectableLabelWithNameEdit(
false, kPaletteGroupNames[PaletteCategory::kDungeons].data(),
std::to_string(dungeon_main_palette_val), "Unnamed dungeon palette");
auto& palette = *rom()->mutable_palette_group()->dungeon_main.mutable_palette(
rom()->paletteset_ids[selected_paletteset_][0]);
DrawPaletteFromPaletteGroup(palette);
Separator();
gui::InputHexByte("Dungeon Spr Pal 1", &dungeon_spr_pal_1_val);
auto& spr_aux_pal1 =
*rom()->mutable_palette_group()->sprites_aux1.mutable_palette(
rom()->paletteset_ids[selected_paletteset_][1]);
DrawPaletteFromPaletteGroup(spr_aux_pal1);
SameLine();
rom()->resource_label()->SelectableLabelWithNameEdit(
false, kPaletteGroupNames[PaletteCategory::kSpritesAux1].data(),
std::to_string(dungeon_spr_pal_1_val), "Dungeon Spr Pal 1");
Separator();
SetNextItemWidth(80.f);
if (gui::InputHexByte("##PaletteIndex", &selected_palette_index_)) {
UpdateCurrentPalette();
}
gui::InputHexByte("Dungeon Spr Pal 2", &dungeon_spr_pal_2_val);
auto& spr_aux_pal2 =
*rom()->mutable_palette_group()->sprites_aux2.mutable_palette(
rom()->paletteset_ids[selected_paletteset_][2]);
DrawPaletteFromPaletteGroup(spr_aux_pal2);
SameLine();
rom()->resource_label()->SelectableLabelWithNameEdit(
false, kPaletteGroupNames[PaletteCategory::kSpritesAux2].data(),
std::to_string(dungeon_spr_pal_2_val), "Dungeon Spr Pal 2");
Separator();
ImGui::Checkbox("Apply", &use_custom_palette_);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Apply selected palette to sheet previews");
}
gui::InputHexByte("Dungeon Spr Pal 3", &dungeon_spr_pal_3_val);
auto& spr_aux_pal3 =
*rom()->mutable_palette_group()->sprites_aux3.mutable_palette(
rom()->paletteset_ids[selected_paletteset_][3]);
DrawPaletteFromPaletteGroup(spr_aux_pal3);
SameLine();
rom()->resource_label()->SelectableLabelWithNameEdit(
false, kPaletteGroupNames[PaletteCategory::kSpritesAux3].data(),
std::to_string(dungeon_spr_pal_3_val), "Dungeon Spr Pal 3");
// Show current palette preview
if (current_palette_ && !current_palette_->empty()) {
SameLine();
DrawPaletteFromPaletteGroup(*current_palette_);
}
}
void GfxGroupEditor::UpdateCurrentPalette() {
if (!game_data()) {
current_palette_ = nullptr;
return;
}
auto& groups = game_data()->palette_groups;
switch (selected_palette_category_) {
case PaletteCategory::kSword:
current_palette_ = groups.swords.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kShield:
current_palette_ = groups.shields.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kClothes:
current_palette_ = groups.armors.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kWorldColors:
current_palette_ =
groups.overworld_main.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kAreaColors:
current_palette_ =
groups.overworld_aux.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kGlobalSprites:
current_palette_ =
groups.global_sprites.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kSpritesAux1:
current_palette_ =
groups.sprites_aux1.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kSpritesAux2:
current_palette_ =
groups.sprites_aux2.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kSpritesAux3:
current_palette_ =
groups.sprites_aux3.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kDungeons:
current_palette_ =
groups.dungeon_main.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kWorldMap:
case PaletteCategory::kDungeonMap:
current_palette_ =
groups.overworld_mini_map.mutable_palette(selected_palette_index_);
break;
case PaletteCategory::kTriforce:
case PaletteCategory::kCrystal:
current_palette_ =
groups.object_3d.mutable_palette(selected_palette_index_);
break;
default:
current_palette_ = nullptr;
break;
}
}
} // namespace editor

View File

@@ -1,10 +1,13 @@
#ifndef YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H
#define YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H
#include <array>
#include "absl/status/status.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/canvas/canvas.h"
#include "app/rom.h"
#include "rom/rom.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace editor {
@@ -12,6 +15,13 @@ namespace editor {
/**
* @class GfxGroupEditor
* @brief Manage graphics group configurations in a Rom.
*
* Provides a UI for viewing and editing:
* - Blocksets (8 sheets per blockset)
* - Roomsets (4 sheets that override blockset slots 4-7)
* - Spritesets (4 sheets for enemy/NPC graphics)
*
* Features palette preview controls for viewing sheets with different palettes.
*/
class GfxGroupEditor {
public:
@@ -20,28 +30,43 @@ class GfxGroupEditor {
void DrawBlocksetViewer(bool sheet_only = false);
void DrawRoomsetViewer();
void DrawSpritesetViewer(bool sheet_only = false);
void DrawPaletteViewer();
void DrawPaletteControls();
void SetSelectedBlockset(uint8_t blockset) { selected_blockset_ = blockset; }
void SetSelectedRoomset(uint8_t roomset) { selected_roomset_ = roomset; }
void SetSelectedSpriteset(uint8_t spriteset) {
selected_spriteset_ = spriteset;
}
void set_rom(Rom* rom) { rom_ = rom; }
void SetRom(Rom* rom) { rom_ = rom; }
Rom* rom() const { return rom_; }
void SetGameData(zelda3::GameData* data) { game_data_ = data; }
zelda3::GameData* game_data() const { return game_data_; }
private:
void UpdateCurrentPalette();
// Selection state
uint8_t selected_blockset_ = 0;
uint8_t selected_roomset_ = 0;
uint8_t selected_spriteset_ = 0;
uint8_t selected_paletteset_ = 0;
gui::Canvas blockset_canvas_;
gui::Canvas roomset_canvas_;
gui::Canvas spriteset_canvas_;
// View controls
float view_scale_ = 2.0f;
// Palette controls
gfx::PaletteCategory selected_palette_category_ =
gfx::PaletteCategory::kDungeons;
uint8_t selected_palette_index_ = 0;
bool use_custom_palette_ = false;
gfx::SnesPalette* current_palette_ = nullptr;
// Individual canvases for each sheet slot to avoid ID conflicts
std::array<gui::Canvas, 8> blockset_canvases_;
std::array<gui::Canvas, 4> roomset_canvases_;
std::array<gui::Canvas, 4> spriteset_canvases_;
gfx::SnesPalette palette_;
Rom* rom_ = nullptr;
zelda3::GameData* game_data_ = nullptr;
};
} // namespace editor

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,31 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_EDITOR_H
#define YAZE_APP_EDITOR_GRAPHICS_EDITOR_H
#include <stack>
#include <array>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "app/editor/editor.h"
#include "app/editor/palette/palette_editor.h"
#include "app/editor/graphics/gfx_group_editor.h"
#include "app/editor/graphics/graphics_editor_state.h"
#include "app/editor/graphics/link_sprite_panel.h"
#include "app/editor/graphics/palette_controls_panel.h"
#include "app/editor/graphics/paletteset_editor_panel.h"
#include "app/editor/graphics/pixel_editor_panel.h"
#include "app/editor/graphics/polyhedral_editor_panel.h"
#include "app/editor/graphics/sheet_browser_panel.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_tile.h"
#include "app/gui/app/editor_layout.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/widgets/asset_browser.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#include "imgui_memory_editor.h"
#include "zelda3/overworld/overworld.h"
#include "rom/rom.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace editor {
// "99973","A3D80",
// Super Donkey prototype graphics offsets (from leaked dev materials)
const std::string kSuperDonkeyTiles[] = {
"97C05", "98219", "9871E", "98C00", "99084", "995AF", "99DE0", "9A27E",
"9A741", "9AC31", "9B07E", "9B55C", "9B963", "9BB99", "9C009", "9C4B4",
@@ -63,7 +68,7 @@ class GraphicsEditor : public Editor {
void Initialize() override;
absl::Status Load() override;
absl::Status Save() override { return absl::UnimplementedError("Save"); }
absl::Status Save() override;
absl::Status Update() override;
absl::Status Cut() override { return absl::UnimplementedError("Cut"); }
absl::Status Copy() override { return absl::UnimplementedError("Copy"); }
@@ -74,75 +79,60 @@ class GraphicsEditor : public Editor {
// Set the ROM pointer
void set_rom(Rom* rom) { rom_ = rom; }
// Set the game data pointer
void SetGameData(zelda3::GameData* game_data) override {
game_data_ = game_data;
if (palette_controls_panel_) {
palette_controls_panel_->SetGameData(game_data);
}
}
// Editor shortcuts
void NextSheet();
void PrevSheet();
// Get the ROM pointer
Rom* rom() const { return rom_; }
private:
enum class GfxEditMode {
kSelect,
kPencil,
kFill,
};
// Editor-level shortcut handling
void HandleEditorShortcuts();
// Graphics Editor Tab
absl::Status UpdateGfxEdit();
absl::Status UpdateGfxSheetList();
absl::Status UpdateGfxTabView();
absl::Status UpdatePaletteColumn();
void DrawGfxEditToolset();
// --- Panel-Based Architecture ---
GraphicsEditorState state_;
std::unique_ptr<SheetBrowserPanel> sheet_browser_panel_;
std::unique_ptr<PixelEditorPanel> pixel_editor_panel_;
std::unique_ptr<PaletteControlsPanel> palette_controls_panel_;
std::unique_ptr<LinkSpritePanel> link_sprite_panel_;
std::unique_ptr<PolyhedralEditorPanel> polyhedral_panel_;
std::unique_ptr<GfxGroupEditor> gfx_group_panel_;
std::unique_ptr<PalettesetEditorPanel> paletteset_panel_;
// Link Graphics Edit Tab
absl::Status UpdateLinkGfxView();
// Prototype Graphics Viewer
absl::Status UpdateScadView();
// Import Functions
// --- Prototype Viewer (Super Donkey / Dev Format Imports) ---
void DrawPrototypeViewer();
absl::Status DrawCgxImport();
absl::Status DrawScrImport();
absl::Status DrawFileImport();
absl::Status DrawObjImport();
absl::Status DrawTilemapImport();
// Other Functions
absl::Status DrawPaletteControls();
absl::Status DrawClipboardImport();
absl::Status DrawExperimentalFeatures();
absl::Status DrawMemoryEditor();
absl::Status DecompressImportData(int size);
absl::Status DecompressSuperDonkey();
// Member Variables
// Card visibility managed by EditorCardManager
ImVec4 current_color_;
uint16_t current_sheet_ = 0;
uint8_t tile_size_ = 0x01;
std::set<uint16_t> open_sheets_;
std::set<uint16_t> child_window_sheets_;
std::stack<uint16_t> release_queue_;
uint64_t edit_palette_group_name_index_ = 0;
uint64_t edit_palette_group_index_ = 0;
uint64_t edit_palette_index_ = 0;
uint64_t edit_palette_sub_index_ = 0;
float sheet_scale_ = 2.0f;
float current_scale_ = 4.0f;
// Prototype Graphics Viewer
// Prototype viewer state
int current_palette_ = 0;
uint64_t current_offset_ = 0;
uint64_t current_size_ = 0;
uint64_t current_palette_index_ = 0;
int current_bpp_ = 0;
int scr_mod_value_ = 0;
uint64_t num_sheets_to_load_ = 1;
uint64_t bin_size_ = 0;
uint64_t clipboard_offset_ = 0;
uint64_t clipboard_size_ = 0;
bool refresh_graphics_ = false;
bool open_memory_editor_ = false;
bool gfx_loaded_ = false;
@@ -154,29 +144,20 @@ class GraphicsEditor : public Editor {
bool obj_loaded_ = false;
bool tilemap_loaded_ = false;
std::string file_path_ = "";
std::string col_file_path_ = "";
std::string col_file_name_ = "";
std::string cgx_file_path_ = "";
std::string cgx_file_name_ = "";
std::string scr_file_path_ = "";
std::string scr_file_name_ = "";
std::string obj_file_path_ = "";
std::string tilemap_file_path_ = "";
std::string tilemap_file_name_ = "";
gui::GfxSheetAssetBrowser asset_browser_;
GfxEditMode gfx_edit_mode_ = GfxEditMode::kSelect;
std::string file_path_;
std::string col_file_path_;
std::string col_file_name_;
std::string cgx_file_path_;
std::string cgx_file_name_;
std::string scr_file_path_;
std::string scr_file_name_;
std::string obj_file_path_;
std::string tilemap_file_path_;
std::string tilemap_file_name_;
Rom temp_rom_;
Rom tilemap_rom_;
zelda3::Overworld overworld_{&temp_rom_};
MemoryEditor cgx_memory_editor_;
MemoryEditor col_memory_editor_;
PaletteEditor palette_editor_;
std::vector<uint8_t> import_data_;
std::vector<uint8_t> graphics_buffer_;
std::vector<uint8_t> decoded_cgx_;
std::vector<uint8_t> cgx_data_;
std::vector<uint8_t> extra_cgx_data_;
@@ -186,27 +167,20 @@ class GraphicsEditor : public Editor {
gfx::Bitmap cgx_bitmap_;
gfx::Bitmap scr_bitmap_;
gfx::Bitmap bin_bitmap_;
gfx::Bitmap link_full_sheet_;
std::array<gfx::Bitmap, kNumGfxSheets> gfx_sheets_;
std::array<gfx::Bitmap, kNumLinkSheets> link_sheets_;
std::array<gfx::Bitmap, zelda3::kNumGfxSheets> gfx_sheets_;
gfx::PaletteGroup col_file_palette_group_;
gfx::SnesPalette z3_rom_palette_;
gfx::SnesPalette col_file_palette_;
gfx::SnesPalette link_palette_;
gui::Canvas import_canvas_;
gui::Canvas scr_canvas_;
gui::Canvas super_donkey_canvas_;
gui::Canvas graphics_bin_canvas_;
gui::Canvas current_sheet_canvas_{"CurrentSheetCanvas", ImVec2(0x80, 0x20),
gui::CanvasGridSize::k8x8};
gui::Canvas link_canvas_{
"LinkCanvas",
ImVec2(gfx::kTilesheetWidth * 4, gfx::kTilesheetHeight * 0x10 * 4),
gui::CanvasGridSize::k16x16};
// Status tracking
absl::Status status_;
// Core references
Rom* rom_;
zelda3::GameData* game_data_ = nullptr;
};
} // namespace editor

View File

@@ -0,0 +1,247 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_EDITOR_STATE_H
#define YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_EDITOR_STATE_H
#include <cstdint>
#include <functional>
#include <set>
#include <stack>
#include <vector>
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_palette.h"
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
/**
* @brief Pixel editing tool types for the graphics editor
*/
enum class PixelTool {
kSelect, // Rectangle selection
kLasso, // Freeform selection
kPencil, // Single pixel drawing
kBrush, // Multi-pixel brush
kEraser, // Set pixels to transparent (index 0)
kFill, // Flood fill
kLine, // Line drawing
kRectangle, // Rectangle outline/fill
kEyedropper, // Color picker from canvas
};
/**
* @brief Selection data for copy/paste operations
*/
struct PixelSelection {
std::vector<uint8_t> pixel_data; // Copied pixel indices
gfx::SnesPalette palette; // Associated palette
int x = 0; // Selection origin X
int y = 0; // Selection origin Y
int width = 0; // Selection width
int height = 0; // Selection height
bool is_active = false; // Whether selection exists
bool is_floating = false; // Floating vs committed
void Clear() {
pixel_data.clear();
x = y = width = height = 0;
is_active = false;
is_floating = false;
}
};
/**
* @brief Snapshot for undo/redo operations
*/
struct PixelEditorSnapshot {
uint16_t sheet_id;
std::vector<uint8_t> pixel_data;
gfx::SnesPalette palette;
bool operator==(const PixelEditorSnapshot& other) const {
return sheet_id == other.sheet_id && pixel_data == other.pixel_data;
}
};
/**
* @brief Shared state between GraphicsEditor panel components
*
* This class maintains the state that needs to be shared between the
* Sheet Browser, Pixel Editor, and Palette Controls panels. It provides
* a single source of truth for selection, current sheet, palette, and
* editing state.
*/
class GraphicsEditorState {
public:
// --- Current Selection ---
uint16_t current_sheet_id = 0;
std::set<uint16_t> open_sheets;
std::set<uint16_t> selected_sheets; // Multi-select support
// --- Editing State ---
PixelTool current_tool = PixelTool::kPencil;
uint8_t current_color_index = 1; // Palette index (0 = transparent)
ImVec4 current_color; // RGBA for display
uint8_t brush_size = 1; // 1-8 pixel brush
bool fill_contiguous = true; // Fill tool: contiguous only
// --- View State ---
float zoom_level = 4.0f; // 1x to 16x
bool show_grid = true; // 8x8 tile grid
bool show_tile_boundaries = true; // 16x16 tile boundaries
ImVec2 pan_offset = {0, 0}; // Canvas pan offset
// --- Overlay State (for enhanced UX) ---
bool show_cursor_crosshair = true; // Crosshair at cursor position
bool show_brush_preview = true; // Preview circle for brush/eraser
bool show_transparency_grid = true; // Checkerboard for transparent pixels
bool show_pixel_info_tooltip = true; // Tooltip with pixel info on hover
// --- Palette State ---
uint64_t palette_group_index = 0;
uint64_t palette_index = 0;
uint64_t sub_palette_index = 0;
bool refresh_graphics = false;
// --- Selection State ---
PixelSelection selection;
ImVec2 selection_start; // Drag start point
bool is_selecting = false; // Currently drawing selection
// --- Undo/Redo ---
std::vector<PixelEditorSnapshot> undo_stack;
std::vector<PixelEditorSnapshot> redo_stack;
static constexpr size_t kMaxUndoHistory = 50;
// --- Modified Sheets Tracking ---
std::set<uint16_t> modified_sheets;
// --- Callbacks for cross-panel communication ---
std::function<void(uint16_t)> on_sheet_selected;
std::function<void()> on_palette_changed;
std::function<void()> on_tool_changed;
std::function<void(uint16_t)> on_sheet_modified;
// --- Methods ---
/**
* @brief Mark a sheet as modified for save tracking
*/
void MarkSheetModified(uint16_t sheet_id) {
modified_sheets.insert(sheet_id);
if (on_sheet_modified) {
on_sheet_modified(sheet_id);
}
}
/**
* @brief Clear modification tracking (after save)
*/
void ClearModifiedSheets() { modified_sheets.clear(); }
/**
* @brief Check if any sheets have unsaved changes
*/
bool HasUnsavedChanges() const { return !modified_sheets.empty(); }
/**
* @brief Push current state to undo stack before modification
*/
void PushUndoState(uint16_t sheet_id, const std::vector<uint8_t>& pixel_data,
const gfx::SnesPalette& palette) {
// Clear redo stack on new action
redo_stack.clear();
// Add to undo stack
undo_stack.push_back({sheet_id, pixel_data, palette});
// Limit stack size
if (undo_stack.size() > kMaxUndoHistory) {
undo_stack.erase(undo_stack.begin());
}
}
/**
* @brief Pop and return the last undo state
*/
bool PopUndoState(PixelEditorSnapshot& out) {
if (undo_stack.empty()) return false;
out = undo_stack.back();
redo_stack.push_back(out);
undo_stack.pop_back();
return true;
}
/**
* @brief Pop and return the last redo state
*/
bool PopRedoState(PixelEditorSnapshot& out) {
if (redo_stack.empty()) return false;
out = redo_stack.back();
undo_stack.push_back(out);
redo_stack.pop_back();
return true;
}
bool CanUndo() const { return !undo_stack.empty(); }
bool CanRedo() const { return !redo_stack.empty(); }
/**
* @brief Select a sheet for editing
*/
void SelectSheet(uint16_t sheet_id) {
current_sheet_id = sheet_id;
open_sheets.insert(sheet_id);
if (on_sheet_selected) {
on_sheet_selected(sheet_id);
}
}
/**
* @brief Close a sheet tab
*/
void CloseSheet(uint16_t sheet_id) { open_sheets.erase(sheet_id); }
/**
* @brief Set the current editing tool
*/
void SetTool(PixelTool tool) {
current_tool = tool;
if (on_tool_changed) {
on_tool_changed();
}
}
/**
* @brief Set zoom level with clamping
*/
void SetZoom(float zoom) {
zoom_level = std::clamp(zoom, 1.0f, 16.0f);
}
void ZoomIn() { SetZoom(zoom_level + 1.0f); }
void ZoomOut() { SetZoom(zoom_level - 1.0f); }
/**
* @brief Get tool name for status display
*/
const char* GetToolName() const {
switch (current_tool) {
case PixelTool::kSelect: return "Select";
case PixelTool::kLasso: return "Lasso";
case PixelTool::kPencil: return "Pencil";
case PixelTool::kBrush: return "Brush";
case PixelTool::kEraser: return "Eraser";
case PixelTool::kFill: return "Fill";
case PixelTool::kLine: return "Line";
case PixelTool::kRectangle: return "Rectangle";
case PixelTool::kEyedropper: return "Eyedropper";
default: return "Unknown";
}
}
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_EDITOR_STATE_H

View File

@@ -0,0 +1,455 @@
#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

View File

@@ -0,0 +1,170 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_LINK_SPRITE_PANEL_H
#define YAZE_APP_EDITOR_GRAPHICS_LINK_SPRITE_PANEL_H
#include <array>
#include <string>
#include "absl/status/status.h"
#include "app/editor/graphics/graphics_editor_state.h"
#include "app/editor/system/editor_panel.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/util/zspr_loader.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/core/icons.h"
namespace yaze {
class Rom;
namespace editor {
/**
* @brief Dedicated panel for editing Link's 14 graphics sheets
*
* Features:
* - Sheet thumbnail grid (4x4 layout, 14 sheets)
* - ZSPR import support
* - Palette switcher (Green/Blue/Red/Bunny mail)
* - Integration with main pixel editor
* - Reset to vanilla option
*/
class LinkSpritePanel : public EditorPanel {
public:
static constexpr int kNumLinkSheets = 14;
/**
* @brief Link sprite palette types
*/
enum class PaletteType {
kGreenMail = 0,
kBlueMail = 1,
kRedMail = 2,
kBunny = 3
};
LinkSpritePanel(GraphicsEditorState* state, Rom* rom);
// ==========================================================================
// EditorPanel Identity
// ==========================================================================
std::string GetId() const override { return "graphics.link_sprite"; }
std::string GetDisplayName() const override { return "Link Sprite"; }
std::string GetIcon() const override { return ICON_MD_PERSON; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 40; }
// ==========================================================================
// EditorPanel Lifecycle
// ==========================================================================
/**
* @brief Initialize the panel and load Link sheets
*/
void Initialize();
/**
* @brief Draw the panel UI (EditorPanel interface)
*/
void Draw(bool* p_open) override;
/**
* @brief Legacy Update method for backward compatibility
* @return Status of the render operation
*/
absl::Status Update();
/**
* @brief Check if the panel has unsaved changes
*/
bool HasUnsavedChanges() const { return has_unsaved_changes_; }
private:
/**
* @brief Draw the toolbar with Import/Reset buttons
*/
void DrawToolbar();
/**
* @brief Draw the 4x4 sheet selection grid
*/
void DrawSheetGrid();
/**
* @brief Draw a single Link sheet thumbnail
*/
void DrawSheetThumbnail(int sheet_index);
/**
* @brief Draw the preview canvas for selected sheet
*/
void DrawPreviewCanvas();
/**
* @brief Draw the palette selector dropdown
*/
void DrawPaletteSelector();
/**
* @brief Draw info panel with stats
*/
void DrawInfoPanel();
/**
* @brief Handle ZSPR file import
*/
void ImportZspr();
/**
* @brief Reset Link sheets to vanilla ROM data
*/
void ResetToVanilla();
/**
* @brief Open selected sheet in the main pixel editor
*/
void OpenSheetInPixelEditor();
/**
* @brief Load Link graphics sheets from ROM
*/
absl::Status LoadLinkSheets();
/**
* @brief Apply the selected palette to Link sheets for display
*/
void ApplySelectedPalette();
/**
* @brief Get the name of a palette type
*/
static const char* GetPaletteName(PaletteType type);
GraphicsEditorState* state_;
Rom* rom_;
// Link sheets loaded from ROM
std::array<gfx::Bitmap, kNumLinkSheets> link_sheets_;
bool sheets_loaded_ = false;
// UI state
int selected_sheet_ = 0;
PaletteType selected_palette_ = PaletteType::kGreenMail;
bool has_unsaved_changes_ = false;
// Preview canvas
gui::Canvas preview_canvas_;
float preview_zoom_ = 4.0f;
// Currently loaded ZSPR (if any)
std::optional<gfx::ZsprData> loaded_zspr_;
// Thumbnail size
static constexpr float kThumbnailSize = 64.0f;
static constexpr float kThumbnailPadding = 4.0f;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_LINK_SPRITE_PANEL_H

View File

@@ -0,0 +1,301 @@
#include "app/editor/graphics/palette_controls_panel.h"
#include "absl/strings/str_format.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/style.h"
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
using gfx::kPaletteGroupAddressesKeys;
void PaletteControlsPanel::Initialize() {
// Initialize with default palette group
state_->palette_group_index = 0;
state_->palette_index = 0;
state_->sub_palette_index = 0;
}
void PaletteControlsPanel::Draw(bool* p_open) {
// EditorPanel interface - delegate to existing Update() logic
if (!rom_ || !rom_->is_loaded()) {
ImGui::TextDisabled("Load a ROM to manage palettes");
return;
}
DrawPresets();
ImGui::Separator();
DrawPaletteGroupSelector();
ImGui::Separator();
DrawPaletteDisplay();
ImGui::Separator();
DrawApplyButtons();
}
absl::Status PaletteControlsPanel::Update() {
if (!rom_ || !rom_->is_loaded()) {
ImGui::TextDisabled("Load a ROM to manage palettes");
return absl::OkStatus();
}
DrawPresets();
ImGui::Separator();
DrawPaletteGroupSelector();
ImGui::Separator();
DrawPaletteDisplay();
ImGui::Separator();
DrawApplyButtons();
return absl::OkStatus();
}
void PaletteControlsPanel::DrawPresets() {
gui::TextWithSeparators("Quick Presets");
if (ImGui::Button(ICON_MD_LANDSCAPE " Overworld")) {
state_->palette_group_index = 0; // Dungeon Main (used for overworld too)
state_->palette_index = 0;
state_->refresh_graphics = true;
}
HOVER_HINT("Standard overworld palette");
ImGui::SameLine();
if (ImGui::Button(ICON_MD_CASTLE " Dungeon")) {
state_->palette_group_index = 0; // Dungeon Main
state_->palette_index = 1;
state_->refresh_graphics = true;
}
HOVER_HINT("Standard dungeon palette");
ImGui::SameLine();
if (ImGui::Button(ICON_MD_PERSON " Sprites")) {
state_->palette_group_index = 4; // Sprites Aux1
state_->palette_index = 0;
state_->refresh_graphics = true;
}
HOVER_HINT("Sprite/enemy palette");
if (ImGui::Button(ICON_MD_ACCOUNT_BOX " Link")) {
state_->palette_group_index = 3; // Sprite Aux3 (Link's palettes)
state_->palette_index = 0;
state_->refresh_graphics = true;
}
HOVER_HINT("Link's palette");
ImGui::SameLine();
if (ImGui::Button(ICON_MD_MENU " HUD")) {
state_->palette_group_index = 6; // HUD palettes
state_->palette_index = 0;
state_->refresh_graphics = true;
}
HOVER_HINT("HUD/menu palette");
}
void PaletteControlsPanel::DrawPaletteGroupSelector() {
gui::TextWithSeparators("Palette Selection");
// Palette group combo
ImGui::SetNextItemWidth(160);
if (ImGui::Combo("Group", reinterpret_cast<int*>(&state_->palette_group_index),
kPaletteGroupAddressesKeys,
IM_ARRAYSIZE(kPaletteGroupAddressesKeys))) {
state_->refresh_graphics = true;
}
// Palette index within group
ImGui::SetNextItemWidth(100);
int palette_idx = static_cast<int>(state_->palette_index);
if (ImGui::InputInt("Palette", &palette_idx)) {
state_->palette_index = static_cast<uint64_t>(std::max(0, palette_idx));
state_->refresh_graphics = true;
}
HOVER_HINT("Palette index within the group");
// Sub-palette index (for multi-row palettes)
ImGui::SetNextItemWidth(100);
int sub_idx = static_cast<int>(state_->sub_palette_index);
if (ImGui::InputInt("Sub-Palette", &sub_idx)) {
state_->sub_palette_index = static_cast<uint64_t>(std::max(0, sub_idx));
state_->refresh_graphics = true;
}
HOVER_HINT("Sub-palette row (0-7 for SNES 128-color palettes)");
}
void PaletteControlsPanel::DrawPaletteDisplay() {
gui::TextWithSeparators("Current Palette");
// Get the current palette from GameData
if (!game_data_) return;
auto palette_group_result = game_data_->palette_groups.get_group(
kPaletteGroupAddressesKeys[state_->palette_group_index]);
if (!palette_group_result) {
ImGui::TextDisabled("Invalid palette group");
return;
}
auto palette_group = *palette_group_result;
if (state_->palette_index >= palette_group.size()) {
ImGui::TextDisabled("Invalid palette index");
return;
}
auto palette = palette_group.palette(state_->palette_index);
// Display palette colors in rows of 16
int colors_per_row = 16;
int total_colors = static_cast<int>(palette.size());
int num_rows = (total_colors + colors_per_row - 1) / colors_per_row;
for (int row = 0; row < num_rows; row++) {
for (int col = 0; col < colors_per_row; col++) {
int idx = row * colors_per_row + col;
if (idx >= total_colors) break;
if (col > 0) ImGui::SameLine();
auto& color = palette[idx];
ImVec4 im_color(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
color.rgb().z / 255.0f, 1.0f);
// Highlight current sub-palette row
bool in_sub_palette =
(row == static_cast<int>(state_->sub_palette_index));
if (in_sub_palette) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 0.0f, 1.0f));
}
std::string id = absl::StrFormat("##PalColor%d", idx);
if (ImGui::ColorButton(id.c_str(), im_color,
ImGuiColorEditFlags_NoTooltip, ImVec2(18, 18))) {
// Clicking a color in a row selects that sub-palette
state_->sub_palette_index = static_cast<uint64_t>(row);
state_->refresh_graphics = true;
}
if (in_sub_palette) {
ImGui::PopStyleColor();
ImGui::PopStyleVar();
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Index: %d (Row %d, Col %d)", idx, row, col);
ImGui::Text("SNES: $%04X", color.snes());
ImGui::Text("RGB: %d, %d, %d", static_cast<int>(color.rgb().x),
static_cast<int>(color.rgb().y),
static_cast<int>(color.rgb().z));
ImGui::EndTooltip();
}
}
}
// Row selection buttons
ImGui::Text("Sub-palette Row:");
for (int i = 0; i < std::min(8, num_rows); i++) {
if (i > 0) ImGui::SameLine();
bool selected = (state_->sub_palette_index == static_cast<uint64_t>(i));
if (selected) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
}
if (ImGui::SmallButton(absl::StrFormat("%d", i).c_str())) {
state_->sub_palette_index = static_cast<uint64_t>(i);
state_->refresh_graphics = true;
}
if (selected) {
ImGui::PopStyleColor();
}
}
}
void PaletteControlsPanel::DrawApplyButtons() {
gui::TextWithSeparators("Apply Palette");
// Apply to current sheet
ImGui::BeginDisabled(state_->open_sheets.empty());
if (ImGui::Button(ICON_MD_BRUSH " Apply to Current Sheet")) {
ApplyPaletteToSheet(state_->current_sheet_id);
}
ImGui::EndDisabled();
HOVER_HINT("Apply palette to the currently selected sheet");
ImGui::SameLine();
// Apply to all sheets
if (ImGui::Button(ICON_MD_FORMAT_PAINT " Apply to All Sheets")) {
ApplyPaletteToAllSheets();
}
HOVER_HINT("Apply palette to all active graphics sheets");
// Apply to selected sheets (multi-select)
if (!state_->selected_sheets.empty()) {
if (ImGui::Button(
absl::StrFormat(ICON_MD_CHECKLIST " Apply to %zu Selected",
state_->selected_sheets.size())
.c_str())) {
for (uint16_t sheet_id : state_->selected_sheets) {
ApplyPaletteToSheet(sheet_id);
}
}
HOVER_HINT("Apply palette to all selected sheets in browser");
}
// Refresh button
ImGui::Separator();
if (ImGui::Button(ICON_MD_REFRESH " Refresh Graphics")) {
state_->refresh_graphics = true;
if (!state_->open_sheets.empty()) {
ApplyPaletteToSheet(state_->current_sheet_id);
}
}
HOVER_HINT("Force refresh of current sheet graphics");
}
void PaletteControlsPanel::ApplyPaletteToSheet(uint16_t sheet_id) {
if (!rom_ || !rom_->is_loaded() || !game_data_) return;
auto palette_group_result = game_data_->palette_groups.get_group(
kPaletteGroupAddressesKeys[state_->palette_group_index]);
if (!palette_group_result) return;
auto palette_group = *palette_group_result;
if (state_->palette_index >= palette_group.size()) return;
auto palette = palette_group.palette(state_->palette_index);
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
if (sheet.is_active() && sheet.surface()) {
sheet.SetPaletteWithTransparent(palette, state_->sub_palette_index);
gfx::Arena::Get().NotifySheetModified(sheet_id);
}
}
void PaletteControlsPanel::ApplyPaletteToAllSheets() {
if (!rom_ || !rom_->is_loaded() || !game_data_) return;
auto palette_group_result = game_data_->palette_groups.get_group(
kPaletteGroupAddressesKeys[state_->palette_group_index]);
if (!palette_group_result) return;
auto palette_group = *palette_group_result;
if (state_->palette_index >= palette_group.size()) return;
auto palette = palette_group.palette(state_->palette_index);
for (int i = 0; i < zelda3::kNumGfxSheets; i++) {
auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->data()[i];
if (sheet.is_active() && sheet.surface()) {
sheet.SetPaletteWithTransparent(palette, state_->sub_palette_index);
gfx::Arena::Get().NotifySheetModified(i);
}
}
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,97 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_PALETTE_CONTROLS_PANEL_H
#define YAZE_APP_EDITOR_GRAPHICS_PALETTE_CONTROLS_PANEL_H
#include "absl/status/status.h"
#include "app/editor/graphics/graphics_editor_state.h"
#include "app/editor/system/editor_panel.h"
#include "app/gui/core/icons.h"
#include "rom/rom.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace editor {
/**
* @brief Panel for managing palettes applied to graphics sheets
*
* Provides palette group selection, quick presets, and
* apply-to-sheet functionality.
*/
class PaletteControlsPanel : public EditorPanel {
public:
explicit PaletteControlsPanel(GraphicsEditorState* state, Rom* rom,
zelda3::GameData* game_data = nullptr)
: state_(state), rom_(rom), game_data_(game_data) {}
void SetGameData(zelda3::GameData* game_data) { game_data_ = game_data; }
// ==========================================================================
// EditorPanel Identity
// ==========================================================================
std::string GetId() const override { return "graphics.palette_controls"; }
std::string GetDisplayName() const override { return "Palette Controls"; }
std::string GetIcon() const override { return ICON_MD_PALETTE; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 30; }
// ==========================================================================
// EditorPanel Lifecycle
// ==========================================================================
/**
* @brief Initialize the panel
*/
void Initialize();
/**
* @brief Draw the palette controls UI (EditorPanel interface)
*/
void Draw(bool* p_open) override;
/**
* @brief Legacy Update method for backward compatibility
* @return Status of the render operation
*/
absl::Status Update();
private:
/**
* @brief Draw quick preset buttons
*/
void DrawPresets();
/**
* @brief Draw palette group selection
*/
void DrawPaletteGroupSelector();
/**
* @brief Draw the current palette display
*/
void DrawPaletteDisplay();
/**
* @brief Draw apply buttons
*/
void DrawApplyButtons();
/**
* @brief Apply current palette to specified sheet
*/
void ApplyPaletteToSheet(uint16_t sheet_id);
/**
* @brief Apply current palette to all active sheets
*/
void ApplyPaletteToAllSheets();
GraphicsEditorState* state_;
Rom* rom_;
zelda3::GameData* game_data_ = nullptr;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_PALETTE_CONTROLS_PANEL_H

View File

@@ -0,0 +1,223 @@
#include "paletteset_editor_panel.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/core/color.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/input.h"
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
using ImGui::BeginChild;
using ImGui::BeginGroup;
using ImGui::BeginTable;
using ImGui::EndChild;
using ImGui::EndGroup;
using ImGui::EndTable;
using ImGui::GetContentRegionAvail;
using ImGui::GetStyle;
using ImGui::PopID;
using ImGui::PushID;
using ImGui::SameLine;
using ImGui::Selectable;
using ImGui::Separator;
using ImGui::SetNextItemWidth;
using ImGui::TableHeadersRow;
using ImGui::TableNextColumn;
using ImGui::TableNextRow;
using ImGui::TableSetupColumn;
using ImGui::Text;
using gfx::kPaletteGroupNames;
using gfx::PaletteCategory;
absl::Status PalettesetEditorPanel::Update() {
if (!rom() || !rom()->is_loaded() || !game_data()) {
Text("No ROM loaded. Please open a Zelda 3 ROM.");
return absl::OkStatus();
}
// Header with controls
Text(ICON_MD_PALETTE " Paletteset Editor");
SameLine();
ImGui::Checkbox("Show All Colors", &show_all_colors_);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show full 16-color palettes instead of 8");
}
Separator();
// Two-column layout: list on left, editor on right
if (BeginTable("##PalettesetLayout", 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable)) {
TableSetupColumn("Palettesets", ImGuiTableColumnFlags_WidthFixed, 200);
TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch);
TableHeadersRow();
TableNextRow();
TableNextColumn();
DrawPalettesetList();
TableNextColumn();
DrawPalettesetEditor();
EndTable();
}
return absl::OkStatus();
}
void PalettesetEditorPanel::DrawPalettesetList() {
if (BeginChild("##PalettesetListChild", ImVec2(0, 400))) {
for (uint8_t idx = 0; idx < 72; idx++) {
PushID(idx);
std::string label = absl::StrFormat("0x%02X", idx);
bool is_selected = (selected_paletteset_ == idx);
// Show custom name if available
std::string display_name = label;
// if (rom()->resource_label()->HasLabel("paletteset", label)) {
// display_name =
// rom()->resource_label()->GetLabel("paletteset", label) + " (" +
// label + ")";
// }
if (Selectable(display_name.c_str(), is_selected)) {
selected_paletteset_ = idx;
}
PopID();
}
}
EndChild();
}
void PalettesetEditorPanel::DrawPalettesetEditor() {
if (selected_paletteset_ >= 72) {
selected_paletteset_ = 71;
}
// Paletteset name editing
std::string paletteset_label =
absl::StrFormat("Paletteset 0x%02X", selected_paletteset_);
Text("%s", paletteset_label.c_str());
rom()->resource_label()->SelectableLabelWithNameEdit(
false, "paletteset", "0x" + std::to_string(selected_paletteset_),
paletteset_label);
Separator();
// Get the paletteset data
auto& paletteset_ids = game_data()->paletteset_ids[selected_paletteset_];
// Dungeon Main Palette
BeginGroup();
Text(ICON_MD_LANDSCAPE " Dungeon Main Palette");
SetNextItemWidth(80.f);
gui::InputHexByte("##DungeonMainIdx", &paletteset_ids[0]);
SameLine();
Text("Index: %d", paletteset_ids[0]);
auto* dungeon_palette =
game_data()->palette_groups.dungeon_main.mutable_palette(
paletteset_ids[0]);
if (dungeon_palette) {
DrawPalettePreview(*dungeon_palette, "dungeon_main");
}
EndGroup();
Separator();
// Sprite Auxiliary Palettes
const char* sprite_labels[] = {"Sprite Aux 1", "Sprite Aux 2",
"Sprite Aux 3"};
const char* sprite_icons[] = {ICON_MD_PERSON, ICON_MD_PETS,
ICON_MD_SMART_TOY};
gfx::PaletteGroup* sprite_groups[] = {
&game_data()->palette_groups.sprites_aux1,
&game_data()->palette_groups.sprites_aux2,
&game_data()->palette_groups.sprites_aux3};
for (int slot = 0; slot < 3; slot++) {
PushID(slot);
BeginGroup();
Text("%s %s", sprite_icons[slot], sprite_labels[slot]);
SetNextItemWidth(80.f);
gui::InputHexByte("##SpriteAuxIdx", &paletteset_ids[slot + 1]);
SameLine();
Text("Index: %d", paletteset_ids[slot + 1]);
auto* sprite_palette =
sprite_groups[slot]->mutable_palette(paletteset_ids[slot + 1]);
if (sprite_palette) {
DrawPalettePreview(*sprite_palette, sprite_labels[slot]);
}
EndGroup();
if (slot < 2) {
Separator();
}
PopID();
}
}
void PalettesetEditorPanel::DrawPalettePreview(gfx::SnesPalette& palette,
const char* label) {
PushID(label);
DrawPaletteGrid(palette, false);
PopID();
}
void PalettesetEditorPanel::DrawPaletteGrid(gfx::SnesPalette& palette,
bool editable) {
if (palette.empty()) {
Text("(Empty palette)");
return;
}
size_t colors_to_show = show_all_colors_ ? palette.size() : 8;
colors_to_show = std::min(colors_to_show, palette.size());
for (size_t color_idx = 0; color_idx < colors_to_show; color_idx++) {
PushID(static_cast<int>(color_idx));
if ((color_idx % 8) != 0) {
SameLine(0.0f, GetStyle().ItemSpacing.y);
}
ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoAlpha |
ImGuiColorEditFlags_NoTooltip;
if (!editable) {
flags |= ImGuiColorEditFlags_NoPicker;
}
if (gui::SnesColorButton(absl::StrCat("Color", color_idx),
palette[color_idx], flags)) {
// Color was clicked - could open color picker if editable
}
if (ImGui::IsItemHovered()) {
auto& color = palette[color_idx];
ImGui::SetTooltip("Color %zu\nRGB: %d, %d, %d\nSNES: $%04X",
color_idx, color.rom_color().red, color.rom_color().green, color.rom_color().blue,
color.snes());
}
PopID();
}
if (!show_all_colors_ && palette.size() > 8) {
SameLine();
Text("(+%zu more)", palette.size() - 8);
}
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,51 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_PALETTESET_EDITOR_PANEL_H_
#define YAZE_APP_EDITOR_GRAPHICS_PALETTESET_EDITOR_PANEL_H_
#include <cstdint>
#include "absl/status/status.h"
#include "app/gfx/types/snes_palette.h"
#include "rom/rom.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace editor {
/**
* @class PalettesetEditorPanel
* @brief Dedicated panel for editing dungeon palette sets.
*
* A paletteset defines which palettes are used together in a dungeon room:
* - Dungeon Main: The primary background/tileset palette
* - Sprite Aux 1-3: Three auxiliary sprite palettes for enemies/NPCs
*
* This panel allows viewing and editing these associations, providing
* a better UX than the combined GfxGroupEditor tab.
*/
class PalettesetEditorPanel {
public:
absl::Status Update();
void SetRom(Rom* rom) { rom_ = rom; }
Rom* rom() const { return rom_; }
void SetGameData(zelda3::GameData* data) { game_data_ = data; }
zelda3::GameData* game_data() const { return game_data_; }
private:
void DrawPalettesetList();
void DrawPalettesetEditor();
void DrawPalettePreview(gfx::SnesPalette& palette, const char* label);
void DrawPaletteGrid(gfx::SnesPalette& palette, bool editable = false);
uint8_t selected_paletteset_ = 0;
bool show_all_colors_ = false;
Rom* rom_ = nullptr;
zelda3::GameData* game_data_ = nullptr;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_PALETTESET_EDITOR_PANEL_H_

View File

@@ -0,0 +1,239 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_PANELS_GRAPHICS_EDITOR_PANELS_H_
#define YAZE_APP_EDITOR_GRAPHICS_PANELS_GRAPHICS_EDITOR_PANELS_H_
#include <functional>
#include <string>
#include "app/editor/system/editor_panel.h"
#include "app/gui/core/icons.h"
namespace yaze {
namespace editor {
// =============================================================================
// EditorPanel wrappers for GraphicsEditor panels
// =============================================================================
/**
* @brief Sheet browser panel for navigating graphics sheets
*/
class GraphicsSheetBrowserPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsSheetBrowserPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.sheet_browser_v2"; }
std::string GetDisplayName() const override { return "Sheet Browser"; }
std::string GetIcon() const override { return ICON_MD_VIEW_LIST; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 10; }
bool IsVisibleByDefault() const override { return true; }
float GetPreferredWidth() const override { return 350.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Main pixel editing panel for graphics sheets
*/
class GraphicsPixelEditorPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsPixelEditorPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.pixel_editor"; }
std::string GetDisplayName() const override { return "Pixel Editor"; }
std::string GetIcon() const override { return ICON_MD_DRAW; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 20; }
bool IsVisibleByDefault() const override { return true; }
float GetPreferredWidth() const override { return 800.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Palette controls panel for managing graphics palettes
*/
class GraphicsPaletteControlsPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsPaletteControlsPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.palette_controls"; }
std::string GetDisplayName() const override { return "Palette Controls"; }
std::string GetIcon() const override { return ICON_MD_PALETTE; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 30; }
float GetPreferredWidth() const override { return 300.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Link sprite editor panel for editing Link's graphics
*/
class GraphicsLinkSpritePanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsLinkSpritePanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.link_sprite_editor"; }
std::string GetDisplayName() const override { return "Link Sprite Editor"; }
std::string GetIcon() const override { return ICON_MD_PERSON; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 35; }
float GetPreferredWidth() const override { return 600.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief 3D polyhedral object editor panel
*/
class GraphicsPolyhedralPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsPolyhedralPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.polyhedral_editor"; }
std::string GetDisplayName() const override { return "3D Objects"; }
std::string GetIcon() const override { return ICON_MD_VIEW_IN_AR; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 38; }
float GetPreferredWidth() const override { return 600.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Graphics group editor panel for managing GFX groups
*/
class GraphicsGfxGroupPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsGfxGroupPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.gfx_group_editor"; }
std::string GetDisplayName() const override { return "Graphics Groups"; }
std::string GetIcon() const override { return ICON_MD_VIEW_MODULE; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 39; }
float GetPreferredWidth() const override { return 500.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Prototype graphics viewer for Super Donkey and dev format imports
*/
class GraphicsPrototypeViewerPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsPrototypeViewerPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.prototype_viewer"; }
std::string GetDisplayName() const override { return "Prototype Viewer"; }
std::string GetIcon() const override { return ICON_MD_CONSTRUCTION; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 50; }
float GetPreferredWidth() const override { return 800.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief Paletteset editor panel for managing dungeon palette associations
*/
class GraphicsPalettesetPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit GraphicsPalettesetPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "graphics.paletteset_editor"; }
std::string GetDisplayName() const override { return "Palettesets"; }
std::string GetIcon() const override { return ICON_MD_COLOR_LENS; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 45; }
float GetPreferredWidth() const override { return 500.0f; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_PANELS_GRAPHICS_EDITOR_PANELS_H_

View File

@@ -0,0 +1,150 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_PANELS_SCREEN_EDITOR_PANELS_H_
#define YAZE_APP_EDITOR_GRAPHICS_PANELS_SCREEN_EDITOR_PANELS_H_
#include <functional>
#include <string>
#include "app/editor/system/editor_panel.h"
#include "app/gui/core/icons.h"
namespace yaze {
namespace editor {
// =============================================================================
// EditorPanel wrappers for ScreenEditor panels
// =============================================================================
/**
* @brief EditorPanel for Dungeon Maps Editor
*/
class DungeonMapsPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit DungeonMapsPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "screen.dungeon_maps"; }
std::string GetDisplayName() const override { return "Dungeon Maps"; }
std::string GetIcon() const override { return ICON_MD_MAP; }
std::string GetEditorCategory() const override { return "Screen"; }
int GetPriority() const override { return 10; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief EditorPanel for Inventory Menu Editor
*/
class InventoryMenuPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit InventoryMenuPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "screen.inventory_menu"; }
std::string GetDisplayName() const override { return "Inventory Menu"; }
std::string GetIcon() const override { return ICON_MD_INVENTORY; }
std::string GetEditorCategory() const override { return "Screen"; }
int GetPriority() const override { return 20; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief EditorPanel for Overworld Map Screen Editor
*/
class OverworldMapScreenPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit OverworldMapScreenPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "screen.overworld_map"; }
std::string GetDisplayName() const override { return "Overworld Map"; }
std::string GetIcon() const override { return ICON_MD_PUBLIC; }
std::string GetEditorCategory() const override { return "Screen"; }
int GetPriority() const override { return 30; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief EditorPanel for Title Screen Editor
*/
class TitleScreenPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit TitleScreenPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "screen.title_screen"; }
std::string GetDisplayName() const override { return "Title Screen"; }
std::string GetIcon() const override { return ICON_MD_TITLE; }
std::string GetEditorCategory() const override { return "Screen"; }
int GetPriority() const override { return 40; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
/**
* @brief EditorPanel for Naming Screen Editor
*/
class NamingScreenPanel : public EditorPanel {
public:
using DrawCallback = std::function<void()>;
explicit NamingScreenPanel(DrawCallback draw_callback)
: draw_callback_(std::move(draw_callback)) {}
std::string GetId() const override { return "screen.naming_screen"; }
std::string GetDisplayName() const override { return "Naming Screen"; }
std::string GetIcon() const override { return ICON_MD_EDIT; }
std::string GetEditorCategory() const override { return "Screen"; }
int GetPriority() const override { return 50; }
void Draw(bool* p_open) override {
if (draw_callback_) {
draw_callback_();
}
}
private:
DrawCallback draw_callback_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_PANELS_SCREEN_EDITOR_PANELS_H_

View File

@@ -0,0 +1,988 @@
#include "app/editor/graphics/pixel_editor_panel.h"
#include <algorithm>
#include <queue>
#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 "imgui/imgui.h"
namespace yaze {
namespace editor {
void PixelEditorPanel::Initialize() {
// Canvas is initialized via member initializer list
}
void PixelEditorPanel::Draw(bool* p_open) {
// EditorPanel interface - delegate to existing Update() logic
// Top toolbar
DrawToolbar();
ImGui::SameLine();
DrawViewControls();
ImGui::Separator();
// Main content area with canvas and side panels
ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -24), false);
// Color picker on the left
ImGui::BeginChild("##ColorPickerSide", ImVec2(120, 0), true);
DrawColorPicker();
ImGui::Separator();
DrawMiniMap();
ImGui::EndChild();
ImGui::SameLine();
// Main canvas
ImGui::BeginChild("##CanvasArea", ImVec2(0, 0), true,
ImGuiWindowFlags_HorizontalScrollbar);
DrawCanvas();
ImGui::EndChild();
ImGui::EndChild();
// Status bar
DrawStatusBar();
}
absl::Status PixelEditorPanel::Update() {
// Top toolbar
DrawToolbar();
ImGui::SameLine();
DrawViewControls();
ImGui::Separator();
// Main content area with canvas and side panels
ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -24), false);
// Color picker on the left
ImGui::BeginChild("##ColorPickerSide", ImVec2(200, 0), true);
DrawColorPicker();
ImGui::Separator();
DrawMiniMap();
ImGui::EndChild();
ImGui::SameLine();
// Main canvas
ImGui::BeginChild("##CanvasArea", ImVec2(0, 0), true,
ImGuiWindowFlags_HorizontalScrollbar);
DrawCanvas();
ImGui::EndChild();
ImGui::EndChild();
// Status bar
DrawStatusBar();
return absl::OkStatus();
}
void PixelEditorPanel::DrawToolbar() {
// Tool selection buttons
auto tool_button = [this](PixelTool tool, const char* icon,
const char* tooltip) {
bool is_selected = state_->current_tool == tool;
if (is_selected) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
}
if (ImGui::Button(icon)) {
state_->SetTool(tool);
}
if (is_selected) {
ImGui::PopStyleColor();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", tooltip);
}
ImGui::SameLine();
};
tool_button(PixelTool::kSelect, ICON_MD_SELECT_ALL, "Select (V)");
tool_button(PixelTool::kPencil, ICON_MD_DRAW, "Pencil (B)");
tool_button(PixelTool::kBrush, ICON_MD_BRUSH, "Brush (B)");
tool_button(PixelTool::kEraser, ICON_MD_AUTO_FIX_HIGH, "Eraser (E)");
tool_button(PixelTool::kFill, ICON_MD_FORMAT_COLOR_FILL, "Fill (G)");
tool_button(PixelTool::kLine, ICON_MD_HORIZONTAL_RULE, "Line");
tool_button(PixelTool::kRectangle, ICON_MD_CROP_SQUARE, "Rectangle");
tool_button(PixelTool::kEyedropper, ICON_MD_COLORIZE, "Eyedropper (I)");
ImGui::SameLine();
ImGui::Text("|");
ImGui::SameLine();
// Brush size for pencil/brush/eraser
if (state_->current_tool == PixelTool::kPencil ||
state_->current_tool == PixelTool::kBrush ||
state_->current_tool == PixelTool::kEraser) {
ImGui::SetNextItemWidth(80);
int brush = state_->brush_size;
if (ImGui::SliderInt("##BrushSize", &brush, 1, 8, "%d px")) {
state_->brush_size = static_cast<uint8_t>(brush);
}
HOVER_HINT("Brush size");
ImGui::SameLine();
}
// Undo/Redo buttons
ImGui::Text("|");
ImGui::SameLine();
ImGui::BeginDisabled(!state_->CanUndo());
if (ImGui::Button(ICON_MD_UNDO)) {
PixelEditorSnapshot snapshot;
if (state_->PopUndoState(snapshot)) {
// Apply undo state
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(snapshot.sheet_id);
sheet.set_data(snapshot.pixel_data);
gfx::Arena::Get().NotifySheetModified(snapshot.sheet_id);
}
}
ImGui::EndDisabled();
HOVER_HINT("Undo (Ctrl+Z)");
ImGui::SameLine();
ImGui::BeginDisabled(!state_->CanRedo());
if (ImGui::Button(ICON_MD_REDO)) {
PixelEditorSnapshot snapshot;
if (state_->PopRedoState(snapshot)) {
// Apply redo state
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(snapshot.sheet_id);
sheet.set_data(snapshot.pixel_data);
gfx::Arena::Get().NotifySheetModified(snapshot.sheet_id);
}
}
ImGui::EndDisabled();
HOVER_HINT("Redo (Ctrl+Y)");
}
void PixelEditorPanel::DrawViewControls() {
// Zoom controls
if (ImGui::Button(ICON_MD_ZOOM_OUT)) {
state_->ZoomOut();
}
HOVER_HINT("Zoom out (-)");
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
float zoom = state_->zoom_level;
if (ImGui::SliderFloat("##Zoom", &zoom, 1.0f, 16.0f, "%.0fx")) {
state_->SetZoom(zoom);
}
ImGui::SameLine();
if (ImGui::Button(ICON_MD_ZOOM_IN)) {
state_->ZoomIn();
}
HOVER_HINT("Zoom in (+)");
ImGui::SameLine();
ImGui::Text("|");
ImGui::SameLine();
// View overlay toggles
ImGui::Checkbox(ICON_MD_GRID_ON, &state_->show_grid);
HOVER_HINT("Toggle grid (Ctrl+G)");
ImGui::SameLine();
ImGui::Checkbox(ICON_MD_ADD, &state_->show_cursor_crosshair);
HOVER_HINT("Toggle cursor crosshair");
ImGui::SameLine();
ImGui::Checkbox(ICON_MD_BRUSH, &state_->show_brush_preview);
HOVER_HINT("Toggle brush preview");
ImGui::SameLine();
ImGui::Checkbox(ICON_MD_TEXTURE, &state_->show_transparency_grid);
HOVER_HINT("Toggle transparency grid");
}
void PixelEditorPanel::DrawCanvas() {
if (state_->open_sheets.empty()) {
ImGui::TextDisabled("No sheet selected. Select a sheet from the browser.");
return;
}
// Tab bar for open sheets
if (ImGui::BeginTabBar("##SheetTabs",
ImGuiTabBarFlags_AutoSelectNewTabs |
ImGuiTabBarFlags_Reorderable |
ImGuiTabBarFlags_TabListPopupButton)) {
std::vector<uint16_t> sheets_to_close;
for (uint16_t sheet_id : state_->open_sheets) {
bool open = true;
std::string tab_label = absl::StrFormat("%02X", sheet_id);
if (state_->modified_sheets.count(sheet_id) > 0) {
tab_label += "*";
}
if (ImGui::BeginTabItem(tab_label.c_str(), &open)) {
state_->current_sheet_id = sheet_id;
// Get the current sheet bitmap
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
if (!sheet.is_active()) {
ImGui::TextDisabled("Sheet %02X is not active", sheet_id);
ImGui::EndTabItem();
continue;
}
// Calculate canvas size based on zoom
float canvas_width = sheet.width() * state_->zoom_level;
float canvas_height = sheet.height() * state_->zoom_level;
// Draw canvas background
canvas_.DrawBackground(ImVec2(canvas_width, canvas_height));
// Draw transparency checkerboard background if enabled
if (state_->show_transparency_grid) {
DrawTransparencyGrid(canvas_width, canvas_height);
}
// Draw the sheet texture
if (sheet.texture()) {
canvas_.draw_list()->AddImage(
(ImTextureID)(intptr_t)sheet.texture(),
canvas_.zero_point(),
ImVec2(canvas_.zero_point().x + canvas_width,
canvas_.zero_point().y + canvas_height));
}
// Draw grid if enabled
if (state_->show_grid) {
canvas_.DrawGrid(8.0f * state_->zoom_level);
}
// Draw selection rectangle if active
if (state_->selection.is_active) {
ImVec2 sel_min = PixelToScreen(state_->selection.x, state_->selection.y);
ImVec2 sel_max =
PixelToScreen(state_->selection.x + state_->selection.width,
state_->selection.y + state_->selection.height);
canvas_.draw_list()->AddRect(sel_min, sel_max,
IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
// Marching ants effect (simplified)
canvas_.draw_list()->AddRect(sel_min, sel_max,
IM_COL32(0, 0, 0, 128), 0.0f, 0, 1.0f);
}
// Draw tool preview (line/rectangle)
if (show_tool_preview_ && is_drawing_) {
ImVec2 start = PixelToScreen(static_cast<int>(tool_start_pixel_.x),
static_cast<int>(tool_start_pixel_.y));
ImVec2 end = PixelToScreen(static_cast<int>(preview_end_.x),
static_cast<int>(preview_end_.y));
if (state_->current_tool == PixelTool::kLine) {
canvas_.draw_list()->AddLine(start, end, IM_COL32(255, 255, 0, 200),
2.0f);
} else if (state_->current_tool == PixelTool::kRectangle) {
canvas_.draw_list()->AddRect(start, end, IM_COL32(255, 255, 0, 200),
0.0f, 0, 2.0f);
}
}
// Draw cursor crosshair overlay if enabled and cursor in canvas
if (state_->show_cursor_crosshair && cursor_in_canvas_) {
DrawCursorCrosshair();
}
// Draw brush preview if using brush/eraser tool
if (state_->show_brush_preview && cursor_in_canvas_ &&
(state_->current_tool == PixelTool::kBrush ||
state_->current_tool == PixelTool::kEraser)) {
DrawBrushPreview();
}
canvas_.DrawOverlay();
// Handle mouse input
HandleCanvasInput();
// Show pixel info tooltip if enabled
if (state_->show_pixel_info_tooltip && cursor_in_canvas_) {
DrawPixelInfoTooltip(sheet);
}
ImGui::EndTabItem();
}
if (!open) {
sheets_to_close.push_back(sheet_id);
}
}
// Close tabs that were requested
for (uint16_t sheet_id : sheets_to_close) {
state_->CloseSheet(sheet_id);
}
ImGui::EndTabBar();
}
}
void PixelEditorPanel::DrawTransparencyGrid(float canvas_width,
float canvas_height) {
const float cell_size = 8.0f; // Checkerboard cell size
const ImU32 color1 = IM_COL32(180, 180, 180, 255);
const ImU32 color2 = IM_COL32(220, 220, 220, 255);
ImVec2 origin = canvas_.zero_point();
int cols = static_cast<int>(canvas_width / cell_size) + 1;
int rows = static_cast<int>(canvas_height / cell_size) + 1;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
bool is_light = (row + col) % 2 == 0;
ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
std::min(p_min.y + cell_size, origin.y + canvas_height));
canvas_.draw_list()->AddRectFilled(p_min, p_max,
is_light ? color1 : color2);
}
}
}
void PixelEditorPanel::DrawCursorCrosshair() {
ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
float pixel_size = state_->zoom_level;
// Vertical line through cursor pixel
ImVec2 v_start(cursor_screen.x + pixel_size / 2,
canvas_.zero_point().y);
ImVec2 v_end(cursor_screen.x + pixel_size / 2,
canvas_.zero_point().y + canvas_.canvas_size().y);
canvas_.draw_list()->AddLine(v_start, v_end, IM_COL32(255, 100, 100, 100),
1.0f);
// Horizontal line through cursor pixel
ImVec2 h_start(canvas_.zero_point().x,
cursor_screen.y + pixel_size / 2);
ImVec2 h_end(canvas_.zero_point().x + canvas_.canvas_size().x,
cursor_screen.y + pixel_size / 2);
canvas_.draw_list()->AddLine(h_start, h_end, IM_COL32(255, 100, 100, 100),
1.0f);
// Highlight current pixel with a bright outline
ImVec2 pixel_min = cursor_screen;
ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
canvas_.draw_list()->AddRect(pixel_min, pixel_max,
IM_COL32(255, 255, 255, 200), 0.0f, 0, 2.0f);
}
void PixelEditorPanel::DrawBrushPreview() {
ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
float pixel_size = state_->zoom_level;
int brush = state_->brush_size;
int half = brush / 2;
// Draw preview of brush area
ImVec2 brush_min(cursor_screen.x - half * pixel_size,
cursor_screen.y - half * pixel_size);
ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
cursor_screen.y + (brush - half) * pixel_size);
// Fill with semi-transparent color preview
ImU32 preview_color = (state_->current_tool == PixelTool::kEraser)
? IM_COL32(255, 0, 0, 50)
: IM_COL32(0, 255, 0, 50);
canvas_.draw_list()->AddRectFilled(brush_min, brush_max, preview_color);
// Outline
ImU32 outline_color = (state_->current_tool == PixelTool::kEraser)
? IM_COL32(255, 100, 100, 200)
: IM_COL32(100, 255, 100, 200);
canvas_.draw_list()->AddRect(brush_min, brush_max, outline_color, 0.0f, 0,
1.0f);
}
void PixelEditorPanel::DrawPixelInfoTooltip(const gfx::Bitmap& sheet) {
if (cursor_x_ < 0 || cursor_x_ >= sheet.width() || cursor_y_ < 0 ||
cursor_y_ >= sheet.height()) {
return;
}
uint8_t color_index = sheet.GetPixel(cursor_x_, cursor_y_);
auto palette = sheet.palette();
ImGui::BeginTooltip();
ImGui::Text("Pos: %d, %d", cursor_x_, cursor_y_);
ImGui::Text("Tile: %d, %d", cursor_x_ / 8, cursor_y_ / 8);
ImGui::Text("Index: %d", color_index);
if (color_index < palette.size()) {
ImGui::Text("SNES: $%04X", palette[color_index].snes());
ImVec4 color(palette[color_index].rgb().x / 255.0f,
palette[color_index].rgb().y / 255.0f,
palette[color_index].rgb().z / 255.0f, 1.0f);
ImGui::ColorButton("##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
ImVec2(24, 24));
if (color_index == 0) {
ImGui::SameLine();
ImGui::TextDisabled("(Transparent)");
}
}
ImGui::EndTooltip();
}
void PixelEditorPanel::DrawColorPicker() {
ImGui::Text("Colors");
if (state_->open_sheets.empty()) {
ImGui::TextDisabled("No sheet");
return;
}
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
auto palette = sheet.palette();
// Draw palette colors in 4x4 grid (16 colors)
for (int i = 0; i < static_cast<int>(palette.size()) && i < 16; i++) {
if (i > 0 && i % 4 == 0) {
// New row
} else if (i > 0) {
ImGui::SameLine();
}
ImVec4 color(palette[i].rgb().x / 255.0f, palette[i].rgb().y / 255.0f,
palette[i].rgb().z / 255.0f, 1.0f);
bool is_selected = state_->current_color_index == i;
if (is_selected) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 0.0f, 1.0f));
}
std::string id = absl::StrFormat("##Color%d", i);
if (ImGui::ColorButton(id.c_str(), color,
ImGuiColorEditFlags_NoTooltip |
ImGuiColorEditFlags_NoBorder,
ImVec2(24, 24))) {
state_->current_color_index = static_cast<uint8_t>(i);
state_->current_color = color;
}
if (is_selected) {
ImGui::PopStyleColor();
ImGui::PopStyleVar();
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Index: %d", i);
ImGui::Text("SNES: $%04X", palette[i].snes());
ImGui::Text("RGB: %d, %d, %d", static_cast<int>(palette[i].rgb().x),
static_cast<int>(palette[i].rgb().y),
static_cast<int>(palette[i].rgb().z));
if (i == 0) {
ImGui::Text("(Transparent)");
}
ImGui::EndTooltip();
}
}
ImGui::Separator();
// Current color preview
ImGui::Text("Current:");
ImGui::ColorButton("##CurrentColor", state_->current_color,
ImGuiColorEditFlags_NoTooltip, ImVec2(40, 40));
ImGui::SameLine();
ImGui::Text("Index: %d", state_->current_color_index);
}
void PixelEditorPanel::DrawMiniMap() {
ImGui::Text("Navigator");
if (state_->open_sheets.empty()) {
ImGui::TextDisabled("No sheet");
return;
}
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
if (!sheet.texture()) return;
// Draw mini version of the sheet
float mini_scale = 0.5f;
float mini_width = sheet.width() * mini_scale;
float mini_height = sheet.height() * mini_scale;
ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddImage((ImTextureID)(intptr_t)sheet.texture(),
pos,
ImVec2(pos.x + mini_width, pos.y + mini_height));
// Draw viewport rectangle
// TODO: Calculate actual viewport bounds based on scroll position
ImGui::Dummy(ImVec2(mini_width, mini_height));
}
void PixelEditorPanel::DrawStatusBar() {
ImGui::Separator();
// Tool name
ImGui::Text("%s", state_->GetToolName());
ImGui::SameLine();
// Cursor position
if (cursor_in_canvas_) {
ImGui::Text("Pos: %d, %d", cursor_x_, cursor_y_);
ImGui::SameLine();
// Tile coordinates
int tile_x = cursor_x_ / 8;
int tile_y = cursor_y_ / 8;
ImGui::Text("Tile: %d, %d", tile_x, tile_y);
ImGui::SameLine();
}
// Sheet info
ImGui::Text("Sheet: %02X", state_->current_sheet_id);
ImGui::SameLine();
// Modified indicator
if (state_->modified_sheets.count(state_->current_sheet_id) > 0) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "(Modified)");
}
// Zoom level
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80);
ImGui::Text("Zoom: %.0fx", state_->zoom_level);
}
void PixelEditorPanel::HandleCanvasInput() {
if (!ImGui::IsItemHovered()) {
cursor_in_canvas_ = false;
return;
}
cursor_in_canvas_ = true;
ImVec2 mouse_pos = ImGui::GetMousePos();
ImVec2 pixel_pos = ScreenToPixel(mouse_pos);
cursor_x_ = static_cast<int>(pixel_pos.x);
cursor_y_ = static_cast<int>(pixel_pos.y);
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
// Clamp to sheet bounds
cursor_x_ = std::clamp(cursor_x_, 0, sheet.width() - 1);
cursor_y_ = std::clamp(cursor_y_, 0, sheet.height() - 1);
// Mouse button handling
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
is_drawing_ = true;
tool_start_pixel_ = ImVec2(static_cast<float>(cursor_x_),
static_cast<float>(cursor_y_));
last_mouse_pixel_ = tool_start_pixel_;
// Save undo state before starting to draw
SaveUndoState();
// Handle tools that need start position
switch (state_->current_tool) {
case PixelTool::kPencil:
ApplyPencil(cursor_x_, cursor_y_);
break;
case PixelTool::kBrush:
ApplyBrush(cursor_x_, cursor_y_);
break;
case PixelTool::kEraser:
ApplyEraser(cursor_x_, cursor_y_);
break;
case PixelTool::kFill:
ApplyFill(cursor_x_, cursor_y_);
break;
case PixelTool::kEyedropper:
ApplyEyedropper(cursor_x_, cursor_y_);
break;
case PixelTool::kSelect:
BeginSelection(cursor_x_, cursor_y_);
break;
case PixelTool::kLine:
case PixelTool::kRectangle:
show_tool_preview_ = true;
break;
default:
break;
}
}
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && is_drawing_) {
preview_end_ = ImVec2(static_cast<float>(cursor_x_),
static_cast<float>(cursor_y_));
switch (state_->current_tool) {
case PixelTool::kPencil:
ApplyPencil(cursor_x_, cursor_y_);
break;
case PixelTool::kBrush:
ApplyBrush(cursor_x_, cursor_y_);
break;
case PixelTool::kEraser:
ApplyEraser(cursor_x_, cursor_y_);
break;
case PixelTool::kSelect:
UpdateSelection(cursor_x_, cursor_y_);
break;
default:
break;
}
last_mouse_pixel_ = ImVec2(static_cast<float>(cursor_x_),
static_cast<float>(cursor_y_));
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && is_drawing_) {
is_drawing_ = false;
switch (state_->current_tool) {
case PixelTool::kLine:
DrawLine(static_cast<int>(tool_start_pixel_.x),
static_cast<int>(tool_start_pixel_.y), cursor_x_, cursor_y_);
break;
case PixelTool::kRectangle:
DrawRectangle(static_cast<int>(tool_start_pixel_.x),
static_cast<int>(tool_start_pixel_.y), cursor_x_,
cursor_y_, false);
break;
case PixelTool::kSelect:
EndSelection();
break;
default:
break;
}
show_tool_preview_ = false;
}
}
void PixelEditorPanel::ApplyPencil(int x, int y) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
sheet.WriteToPixel(x, y, state_->current_color_index);
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
}
void PixelEditorPanel::ApplyBrush(int x, int y) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
int size = state_->brush_size;
int half = size / 2;
for (int dy = -half; dy < size - half; dy++) {
for (int dx = -half; dx < size - half; dx++) {
int px = x + dx;
int py = y + dy;
if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
sheet.WriteToPixel(px, py, state_->current_color_index);
}
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::ApplyEraser(int x, int y) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
int size = state_->brush_size;
int half = size / 2;
for (int dy = -half; dy < size - half; dy++) {
for (int dx = -half; dx < size - half; dx++) {
int px = x + dx;
int py = y + dy;
if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
sheet.WriteToPixel(px, py, 0); // Index 0 = transparent
}
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::ApplyFill(int x, int y) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
if (x < 0 || x >= sheet.width() || y < 0 || y >= sheet.height()) return;
uint8_t target_color = sheet.GetPixel(x, y);
uint8_t fill_color = state_->current_color_index;
if (target_color == fill_color) return; // Nothing to fill
// BFS flood fill
std::queue<std::pair<int, int>> queue;
std::vector<bool> visited(sheet.width() * sheet.height(), false);
queue.push({x, y});
visited[y * sheet.width() + x] = true;
while (!queue.empty()) {
auto [cx, cy] = queue.front();
queue.pop();
sheet.WriteToPixel(cx, cy, fill_color);
// Check 4-connected neighbors
const int dx[] = {0, 0, -1, 1};
const int dy[] = {-1, 1, 0, 0};
for (int i = 0; i < 4; i++) {
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx >= 0 && nx < sheet.width() && ny >= 0 && ny < sheet.height()) {
int idx = ny * sheet.width() + nx;
if (!visited[idx] && sheet.GetPixel(nx, ny) == target_color) {
visited[idx] = true;
queue.push({nx, ny});
}
}
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::ApplyEyedropper(int x, int y) {
auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
state_->current_color_index = sheet.GetPixel(x, y);
// Update current color display
auto palette = sheet.palette();
if (state_->current_color_index < palette.size()) {
auto& color = palette[state_->current_color_index];
state_->current_color =
ImVec4(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
color.rgb().z / 255.0f, 1.0f);
}
}
}
void PixelEditorPanel::DrawLine(int x1, int y1, int x2, int y2) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
// Bresenham's line algorithm
int dx = std::abs(x2 - x1);
int dy = std::abs(y2 - y1);
int sx = x1 < x2 ? 1 : -1;
int sy = y1 < y2 ? 1 : -1;
int err = dx - dy;
while (true) {
if (x1 >= 0 && x1 < sheet.width() && y1 >= 0 && y1 < sheet.height()) {
sheet.WriteToPixel(x1, y1, state_->current_color_index);
}
if (x1 == x2 && y1 == y2) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x1 += sx;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::DrawRectangle(int x1, int y1, int x2, int y2,
bool filled) {
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
int min_x = std::min(x1, x2);
int max_x = std::max(x1, x2);
int min_y = std::min(y1, y2);
int max_y = std::max(y1, y2);
if (filled) {
for (int y = min_y; y <= max_y; y++) {
for (int x = min_x; x <= max_x; x++) {
if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
sheet.WriteToPixel(x, y, state_->current_color_index);
}
}
}
} else {
// Top and bottom edges
for (int x = min_x; x <= max_x; x++) {
if (x >= 0 && x < sheet.width()) {
if (min_y >= 0 && min_y < sheet.height())
sheet.WriteToPixel(x, min_y, state_->current_color_index);
if (max_y >= 0 && max_y < sheet.height())
sheet.WriteToPixel(x, max_y, state_->current_color_index);
}
}
// Left and right edges
for (int y = min_y; y <= max_y; y++) {
if (y >= 0 && y < sheet.height()) {
if (min_x >= 0 && min_x < sheet.width())
sheet.WriteToPixel(min_x, y, state_->current_color_index);
if (max_x >= 0 && max_x < sheet.width())
sheet.WriteToPixel(max_x, y, state_->current_color_index);
}
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::BeginSelection(int x, int y) {
state_->selection.x = x;
state_->selection.y = y;
state_->selection.width = 1;
state_->selection.height = 1;
state_->selection.is_active = true;
state_->is_selecting = true;
}
void PixelEditorPanel::UpdateSelection(int x, int y) {
int start_x = static_cast<int>(tool_start_pixel_.x);
int start_y = static_cast<int>(tool_start_pixel_.y);
state_->selection.x = std::min(start_x, x);
state_->selection.y = std::min(start_y, y);
state_->selection.width = std::abs(x - start_x) + 1;
state_->selection.height = std::abs(y - start_y) + 1;
}
void PixelEditorPanel::EndSelection() {
state_->is_selecting = false;
// Copy pixel data for the selection
if (state_->selection.width > 0 && state_->selection.height > 0) {
auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
state_->selection.pixel_data.resize(state_->selection.width *
state_->selection.height);
for (int y = 0; y < state_->selection.height; y++) {
for (int x = 0; x < state_->selection.width; x++) {
int src_x = state_->selection.x + x;
int src_y = state_->selection.y + y;
if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
src_y < sheet.height()) {
state_->selection.pixel_data[y * state_->selection.width + x] =
sheet.GetPixel(src_x, src_y);
}
}
}
state_->selection.palette = sheet.palette();
}
}
void PixelEditorPanel::CopySelection() {
// Selection data is already in state_->selection
}
void PixelEditorPanel::PasteSelection(int x, int y) {
if (state_->selection.pixel_data.empty()) return;
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(state_->current_sheet_id);
SaveUndoState();
for (int dy = 0; dy < state_->selection.height; dy++) {
for (int dx = 0; dx < state_->selection.width; dx++) {
int dest_x = x + dx;
int dest_y = y + dy;
if (dest_x >= 0 && dest_x < sheet.width() && dest_y >= 0 &&
dest_y < sheet.height()) {
uint8_t pixel =
state_->selection.pixel_data[dy * state_->selection.width + dx];
sheet.WriteToPixel(dest_x, dest_y, pixel);
}
}
}
state_->MarkSheetModified(state_->current_sheet_id);
gfx::Arena::Get().NotifySheetModified(state_->current_sheet_id);
}
void PixelEditorPanel::FlipSelectionHorizontal() {
if (state_->selection.pixel_data.empty()) return;
std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
for (int y = 0; y < state_->selection.height; y++) {
for (int x = 0; x < state_->selection.width; x++) {
int src_idx = y * state_->selection.width + x;
int dst_idx = y * state_->selection.width + (state_->selection.width - 1 - x);
flipped[dst_idx] = state_->selection.pixel_data[src_idx];
}
}
state_->selection.pixel_data = std::move(flipped);
}
void PixelEditorPanel::FlipSelectionVertical() {
if (state_->selection.pixel_data.empty()) return;
std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
for (int y = 0; y < state_->selection.height; y++) {
for (int x = 0; x < state_->selection.width; x++) {
int src_idx = y * state_->selection.width + x;
int dst_idx =
(state_->selection.height - 1 - y) * state_->selection.width + x;
flipped[dst_idx] = state_->selection.pixel_data[src_idx];
}
}
state_->selection.pixel_data = std::move(flipped);
}
void PixelEditorPanel::SaveUndoState() {
auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
state_->PushUndoState(state_->current_sheet_id, sheet.vector(),
sheet.palette());
}
ImVec2 PixelEditorPanel::ScreenToPixel(ImVec2 screen_pos) {
float px = (screen_pos.x - canvas_.zero_point().x) / state_->zoom_level;
float py = (screen_pos.y - canvas_.zero_point().y) / state_->zoom_level;
return ImVec2(px, py);
}
ImVec2 PixelEditorPanel::PixelToScreen(int x, int y) {
return ImVec2(canvas_.zero_point().x + x * state_->zoom_level,
canvas_.zero_point().y + y * state_->zoom_level);
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,224 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_PIXEL_EDITOR_PANEL_H
#define YAZE_APP_EDITOR_GRAPHICS_PIXEL_EDITOR_PANEL_H
#include "absl/status/status.h"
#include "app/editor/graphics/graphics_editor_state.h"
#include "app/editor/system/editor_panel.h"
#include "app/gfx/core/bitmap.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/core/icons.h"
#include "rom/rom.h"
namespace yaze {
namespace editor {
/**
* @brief Main pixel editing panel for graphics sheets
*
* Provides a full-featured pixel editor with tools for drawing,
* selecting, and manipulating graphics data.
*/
class PixelEditorPanel : public EditorPanel {
public:
explicit PixelEditorPanel(GraphicsEditorState* state, Rom* rom)
: state_(state), rom_(rom) {}
// ==========================================================================
// EditorPanel Identity
// ==========================================================================
std::string GetId() const override { return "graphics.pixel_editor"; }
std::string GetDisplayName() const override { return "Pixel Editor"; }
std::string GetIcon() const override { return ICON_MD_BRUSH; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 20; }
// ==========================================================================
// EditorPanel Lifecycle
// ==========================================================================
/**
* @brief Initialize the panel
*/
void Initialize();
/**
* @brief Draw the pixel editor UI (EditorPanel interface)
*/
void Draw(bool* p_open) override;
/**
* @brief Legacy Update method for backward compatibility
* @return Status of the render operation
*/
absl::Status Update();
private:
/**
* @brief Draw the toolbar with tool selection
*/
void DrawToolbar();
/**
* @brief Draw zoom and view controls
*/
void DrawViewControls();
/**
* @brief Draw the main editing canvas
*/
void DrawCanvas();
/**
* @brief Draw the color palette picker
*/
void DrawColorPicker();
/**
* @brief Draw the status bar with cursor position
*/
void DrawStatusBar();
/**
* @brief Draw the mini navigation map
*/
void DrawMiniMap();
/**
* @brief Handle canvas mouse input for current tool
*/
void HandleCanvasInput();
/**
* @brief Apply pencil tool at position
*/
void ApplyPencil(int x, int y);
/**
* @brief Apply brush tool at position
*/
void ApplyBrush(int x, int y);
/**
* @brief Apply eraser tool at position
*/
void ApplyEraser(int x, int y);
/**
* @brief Apply flood fill starting at position
*/
void ApplyFill(int x, int y);
/**
* @brief Apply eyedropper tool at position
*/
void ApplyEyedropper(int x, int y);
/**
* @brief Draw line from start to end
*/
void DrawLine(int x1, int y1, int x2, int y2);
/**
* @brief Draw rectangle from start to end
*/
void DrawRectangle(int x1, int y1, int x2, int y2, bool filled);
/**
* @brief Start a new selection
*/
void BeginSelection(int x, int y);
/**
* @brief Update selection during drag
*/
void UpdateSelection(int x, int y);
/**
* @brief Finalize the selection
*/
void EndSelection();
/**
* @brief Copy selection to clipboard
*/
void CopySelection();
/**
* @brief Paste clipboard at position
*/
void PasteSelection(int x, int y);
/**
* @brief Flip selection horizontally
*/
void FlipSelectionHorizontal();
/**
* @brief Flip selection vertically
*/
void FlipSelectionVertical();
/**
* @brief Save current state for undo
*/
void SaveUndoState();
/**
* @brief Convert screen coordinates to pixel coordinates
*/
ImVec2 ScreenToPixel(ImVec2 screen_pos);
/**
* @brief Convert pixel coordinates to screen coordinates
*/
ImVec2 PixelToScreen(int x, int y);
// ==========================================================================
// Overlay Drawing
// ==========================================================================
/**
* @brief Draw checkerboard pattern for transparent pixels
*/
void DrawTransparencyGrid(float canvas_width, float canvas_height);
/**
* @brief Draw crosshair at cursor position
*/
void DrawCursorCrosshair();
/**
* @brief Draw brush size preview circle
*/
void DrawBrushPreview();
/**
* @brief Draw tooltip with pixel information
*/
void DrawPixelInfoTooltip(const gfx::Bitmap& sheet);
GraphicsEditorState* state_;
Rom* rom_;
gui::Canvas canvas_{"PixelEditorCanvas", ImVec2(128, 32),
gui::CanvasGridSize::k8x8};
// Mouse tracking for tools
bool is_drawing_ = false;
ImVec2 last_mouse_pixel_ = {-1, -1};
ImVec2 tool_start_pixel_ = {-1, -1};
// Line/rectangle preview
bool show_tool_preview_ = false;
ImVec2 preview_end_ = {0, 0};
// Current cursor position in pixel coords
int cursor_x_ = 0;
int cursor_y_ = 0;
bool cursor_in_canvas_ = false;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_PIXEL_EDITOR_PANEL_H

View File

@@ -0,0 +1,585 @@
#include "app/editor/graphics/polyhedral_editor_panel.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "app/gui/core/icons.h"
#include "app/gui/plots/implot_support.h"
#include "rom/snes.h"
#include "imgui/imgui.h"
#include "implot.h"
#include "util/macro.h"
namespace yaze {
namespace editor {
namespace {
constexpr uint32_t kPolyTableSnes = 0x09FF8C;
constexpr uint32_t kPolyEntrySize = 6;
constexpr uint32_t kPolyRegionSize = 0x74; // 116 bytes, $09:FF8C-$09:FFFF
constexpr uint8_t kPolyBank = 0x09;
constexpr ImVec4 kVertexColor(0.3f, 0.8f, 1.0f, 1.0f);
constexpr ImVec4 kSelectedVertexColor(1.0f, 0.75f, 0.2f, 1.0f);
template <typename T>
T Clamp(T value, T min_v, T max_v) {
return std::max(min_v, std::min(max_v, value));
}
std::string ShapeNameForIndex(int index) {
switch (index) {
case 0:
return "Crystal";
case 1:
return "Triforce";
default:
return absl::StrFormat("Shape %d", index);
}
}
uint32_t ToPc(uint16_t bank_offset) {
return SnesToPc((kPolyBank << 16) | bank_offset);
}
} // namespace
uint32_t PolyhedralEditorPanel::TablePc() const {
return SnesToPc(kPolyTableSnes);
}
absl::Status PolyhedralEditorPanel::Load() {
RETURN_IF_ERROR(LoadShapes());
dirty_ = false;
return absl::OkStatus();
}
absl::Status PolyhedralEditorPanel::LoadShapes() {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM is not loaded");
}
// Read the whole 3D object region to keep parsing bounds explicit.
ASSIGN_OR_RETURN(auto region,
rom_->ReadByteVector(TablePc(), kPolyRegionSize));
shapes_.clear();
// Two entries live in the table (crystal, triforce). Stop if we run out of
// room rather than reading garbage.
for (int i = 0; i < 2; ++i) {
size_t base = i * kPolyEntrySize;
if (base + kPolyEntrySize > region.size()) {
break;
}
PolyShape shape;
shape.name = ShapeNameForIndex(i);
shape.vertex_count = region[base];
shape.face_count = region[base + 1];
shape.vertex_ptr =
static_cast<uint16_t>(region[base + 2] | (region[base + 3] << 8));
shape.face_ptr =
static_cast<uint16_t>(region[base + 4] | (region[base + 5] << 8));
// Vertices (signed bytes, XYZ triples)
const uint32_t vertex_pc = ToPc(shape.vertex_ptr);
const size_t vertex_bytes = static_cast<size_t>(shape.vertex_count) * 3;
ASSIGN_OR_RETURN(auto vertex_blob,
rom_->ReadByteVector(vertex_pc, vertex_bytes));
shape.vertices.reserve(shape.vertex_count);
for (size_t idx = 0; idx + 2 < vertex_blob.size(); idx += 3) {
PolyVertex v;
v.x = static_cast<int8_t>(vertex_blob[idx]);
v.y = static_cast<int8_t>(vertex_blob[idx + 1]);
v.z = static_cast<int8_t>(vertex_blob[idx + 2]);
shape.vertices.push_back(v);
}
// Faces (count byte, indices[count], shade byte)
uint32_t face_pc = ToPc(shape.face_ptr);
shape.faces.reserve(shape.face_count);
for (int f = 0; f < shape.face_count; ++f) {
ASSIGN_OR_RETURN(auto count_byte, rom_->ReadByte(face_pc++));
PolyFace face;
face.vertex_indices.reserve(count_byte);
for (int j = 0; j < count_byte; ++j) {
ASSIGN_OR_RETURN(auto idx_byte, rom_->ReadByte(face_pc++));
face.vertex_indices.push_back(idx_byte);
}
ASSIGN_OR_RETURN(auto shade_byte, rom_->ReadByte(face_pc++));
face.shade = shade_byte;
shape.faces.push_back(std::move(face));
}
shapes_.push_back(std::move(shape));
}
selected_shape_ = 0;
selected_vertex_ = 0;
data_loaded_ = true;
return absl::OkStatus();
}
absl::Status PolyhedralEditorPanel::SaveShapes() {
for (auto& shape : shapes_) {
shape.vertex_count = static_cast<uint8_t>(shape.vertices.size());
shape.face_count = static_cast<uint8_t>(shape.faces.size());
RETURN_IF_ERROR(WriteShape(shape));
}
dirty_ = false;
return absl::OkStatus();
}
absl::Status PolyhedralEditorPanel::WriteShape(const PolyShape& shape) {
// Vertices
std::vector<uint8_t> vertex_blob;
vertex_blob.reserve(shape.vertices.size() * 3);
for (const auto& v : shape.vertices) {
vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.x)));
vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.y)));
vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.z)));
}
RETURN_IF_ERROR(
rom_->WriteVector(ToPc(shape.vertex_ptr), std::move(vertex_blob)));
// Faces
std::vector<uint8_t> face_blob;
for (const auto& face : shape.faces) {
face_blob.push_back(static_cast<uint8_t>(face.vertex_indices.size()));
for (auto idx : face.vertex_indices) {
face_blob.push_back(idx);
}
face_blob.push_back(face.shade);
}
return rom_->WriteVector(ToPc(shape.face_ptr), std::move(face_blob));
}
void PolyhedralEditorPanel::Draw(bool* p_open) {
// EditorPanel interface - delegate to existing Update() logic
if (!rom_ || !rom_->is_loaded()) {
ImGui::TextUnformatted("Load a ROM to edit 3D objects.");
return;
}
if (!data_loaded_) {
auto status = LoadShapes();
if (!status.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Failed to load shapes: %s", status.message().data());
return;
}
}
gui::plotting::EnsureImPlotContext();
ImGui::Text("ALTTP polyhedral data @ $09:%04X (PC $%05X), %u bytes",
static_cast<uint16_t>(kPolyTableSnes & 0xFFFF), TablePc(),
kPolyRegionSize);
ImGui::TextUnformatted(
"Shapes: 0 = Crystal, 1 = Triforce (IDs used by POLYSHAPE)");
// Shape selector
if (!shapes_.empty()) {
ImGui::SetNextItemWidth(180);
if (ImGui::BeginCombo("Shape", shapes_[selected_shape_].name.c_str())) {
for (size_t i = 0; i < shapes_.size(); ++i) {
bool selected = static_cast<int>(i) == selected_shape_;
if (ImGui::Selectable(shapes_[i].name.c_str(), selected)) {
selected_shape_ = static_cast<int>(i);
selected_vertex_ = 0;
}
}
ImGui::EndCombo();
}
}
if (ImGui::Button(ICON_MD_REFRESH " Reload from ROM")) {
auto status = LoadShapes();
if (!status.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Reload failed: %s", status.message().data());
}
}
ImGui::SameLine();
ImGui::BeginDisabled(!dirty_);
if (ImGui::Button(ICON_MD_SAVE " Save 3D objects")) {
auto status = SaveShapes();
if (!status.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Save failed: %s", status.message().data());
}
}
ImGui::EndDisabled();
if (shapes_.empty()) {
ImGui::TextUnformatted("No polyhedral shapes found.");
return;
}
ImGui::Separator();
DrawShapeEditor(shapes_[selected_shape_]);
}
absl::Status PolyhedralEditorPanel::Update() {
if (!rom_ || !rom_->is_loaded()) {
ImGui::TextUnformatted("Load a ROM to edit 3D objects.");
return absl::OkStatus();
}
if (!data_loaded_) {
RETURN_IF_ERROR(LoadShapes());
}
gui::plotting::EnsureImPlotContext();
ImGui::Text("ALTTP polyhedral data @ $09:%04X (PC $%05X), %u bytes",
static_cast<uint16_t>(kPolyTableSnes & 0xFFFF), TablePc(),
kPolyRegionSize);
ImGui::TextUnformatted(
"Shapes: 0 = Crystal, 1 = Triforce (IDs used by POLYSHAPE)");
// Shape selector
if (!shapes_.empty()) {
ImGui::SetNextItemWidth(180);
if (ImGui::BeginCombo("Shape", shapes_[selected_shape_].name.c_str())) {
for (size_t i = 0; i < shapes_.size(); ++i) {
bool selected = static_cast<int>(i) == selected_shape_;
if (ImGui::Selectable(shapes_[i].name.c_str(), selected)) {
selected_shape_ = static_cast<int>(i);
selected_vertex_ = 0;
}
}
ImGui::EndCombo();
}
}
if (ImGui::Button(ICON_MD_REFRESH " Reload from ROM")) {
RETURN_IF_ERROR(LoadShapes());
}
ImGui::SameLine();
ImGui::BeginDisabled(!dirty_);
if (ImGui::Button(ICON_MD_SAVE " Save 3D objects")) {
RETURN_IF_ERROR(SaveShapes());
}
ImGui::EndDisabled();
if (shapes_.empty()) {
ImGui::TextUnformatted("No polyhedral shapes found.");
return absl::OkStatus();
}
ImGui::Separator();
DrawShapeEditor(shapes_[selected_shape_]);
return absl::OkStatus();
}
void PolyhedralEditorPanel::DrawShapeEditor(PolyShape& shape) {
ImGui::Text("Vertices: %u Faces: %u", shape.vertex_count,
shape.face_count);
ImGui::Text("Vertex data @ $09:%04X (PC $%05X)", shape.vertex_ptr,
ToPc(shape.vertex_ptr));
ImGui::Text("Face data @ $09:%04X (PC $%05X)", shape.face_ptr,
ToPc(shape.face_ptr));
ImGui::Spacing();
if (ImGui::BeginTable("##poly_editor", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Data", ImGuiTableColumnFlags_WidthStretch, 0.45f);
ImGui::TableSetupColumn("Plots", ImGuiTableColumnFlags_WidthStretch, 0.55f);
ImGui::TableNextColumn();
DrawVertexList(shape);
ImGui::Spacing();
DrawFaceList(shape);
ImGui::TableNextColumn();
DrawPlot("XY (X vs Y)", PlotPlane::kXY, shape);
DrawPlot("XZ (X vs Z)", PlotPlane::kXZ, shape);
ImGui::Spacing();
DrawPreview(shape);
ImGui::EndTable();
}
}
void PolyhedralEditorPanel::DrawVertexList(PolyShape& shape) {
if (shape.vertices.empty()) {
ImGui::TextUnformatted("No vertices");
return;
}
for (size_t i = 0; i < shape.vertices.size(); ++i) {
ImGui::PushID(static_cast<int>(i));
const bool is_selected = static_cast<int>(i) == selected_vertex_;
std::string label = absl::StrFormat("Vertex %zu", i);
if (ImGui::Selectable(label.c_str(), is_selected)) {
selected_vertex_ = static_cast<int>(i);
}
ImGui::SameLine();
ImGui::SetNextItemWidth(210);
int coords[3] = {shape.vertices[i].x, shape.vertices[i].y,
shape.vertices[i].z};
if (ImGui::InputInt3("##coords", coords)) {
shape.vertices[i].x = Clamp(coords[0], -127, 127);
shape.vertices[i].y = Clamp(coords[1], -127, 127);
shape.vertices[i].z = Clamp(coords[2], -127, 127);
dirty_ = true;
}
ImGui::PopID();
}
}
void PolyhedralEditorPanel::DrawFaceList(PolyShape& shape) {
if (shape.faces.empty()) {
ImGui::TextUnformatted("No faces");
return;
}
ImGui::TextUnformatted("Faces (vertex indices + shade)");
for (size_t i = 0; i < shape.faces.size(); ++i) {
ImGui::PushID(static_cast<int>(i));
ImGui::Text("Face %zu", i);
ImGui::SameLine();
int shade = shape.faces[i].shade;
ImGui::SetNextItemWidth(70);
if (ImGui::InputInt("Shade##face", &shade, 0, 0)) {
shape.faces[i].shade = static_cast<uint8_t>(Clamp(shade, 0, 0xFF));
dirty_ = true;
}
ImGui::SameLine();
ImGui::TextUnformatted("Vertices:");
const int max_idx =
shape.vertices.empty()
? 0
: static_cast<int>(shape.vertices.size() - 1);
for (size_t v = 0; v < shape.faces[i].vertex_indices.size(); ++v) {
ImGui::SameLine();
int idx = shape.faces[i].vertex_indices[v];
ImGui::SetNextItemWidth(40);
if (ImGui::InputInt(absl::StrFormat("##v%zu", v).c_str(), &idx, 0, 0)) {
idx = Clamp(idx, 0, max_idx);
shape.faces[i].vertex_indices[v] = static_cast<uint8_t>(idx);
dirty_ = true;
}
}
ImGui::PopID();
}
}
void PolyhedralEditorPanel::DrawPlot(const char* label, PlotPlane plane,
PolyShape& shape) {
if (shape.vertices.empty()) {
return;
}
ImVec2 plot_size = ImVec2(-1, 220);
ImPlotFlags flags = ImPlotFlags_NoLegend | ImPlotFlags_Equal;
if (ImPlot::BeginPlot(label, plot_size, flags)) {
const char* x_label = (plane == PlotPlane::kYZ) ? "Y" : "X";
const char* y_label = (plane == PlotPlane::kXY)
? "Y"
: "Z";
ImPlot::SetupAxes(x_label, y_label, ImPlotAxisFlags_AutoFit,
ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, -80, 80, ImGuiCond_Once);
ImPlot::SetupAxisLimits(ImAxis_Y1, -80, 80, ImGuiCond_Once);
for (size_t i = 0; i < shape.vertices.size(); ++i) {
double x = shape.vertices[i].x;
double y = 0.0;
switch (plane) {
case PlotPlane::kXY:
y = shape.vertices[i].y;
break;
case PlotPlane::kXZ:
y = shape.vertices[i].z;
break;
case PlotPlane::kYZ:
x = shape.vertices[i].y;
y = shape.vertices[i].z;
break;
}
const bool is_selected = static_cast<int>(i) == selected_vertex_;
ImVec4 color = is_selected ? kSelectedVertexColor : kVertexColor;
// ImPlot::DragPoint wants an int ID, so compose one from vertex index and plane.
int point_id =
static_cast<int>(i * 10 + static_cast<size_t>(plane));
if (ImPlot::DragPoint(point_id, &x, &y, color, 6.0f)) {
// Round so we keep integer coordinates in ROM
int rounded_x = Clamp(static_cast<int>(std::lround(x)), -127, 127);
int rounded_y = Clamp(static_cast<int>(std::lround(y)), -127, 127);
switch (plane) {
case PlotPlane::kXY:
shape.vertices[i].x = rounded_x;
shape.vertices[i].y = rounded_y;
break;
case PlotPlane::kXZ:
shape.vertices[i].x = rounded_x;
shape.vertices[i].z = rounded_y;
break;
case PlotPlane::kYZ:
shape.vertices[i].y = rounded_x;
shape.vertices[i].z = rounded_y;
break;
}
dirty_ = true;
if (!is_selected) {
selected_vertex_ = static_cast<int>(i);
}
}
}
ImPlot::EndPlot();
}
}
void PolyhedralEditorPanel::DrawPreview(PolyShape& shape) {
if (shape.vertices.empty() || shape.faces.empty()) {
return;
}
static float rot_x = 0.35f;
static float rot_y = -0.4f;
static float rot_z = 0.0f;
static float zoom = 1.0f;
ImGui::TextUnformatted("Preview (orthographic)");
ImGui::SetNextItemWidth(120);
ImGui::SliderFloat("Rot X", &rot_x, -3.14f, 3.14f, "%.2f");
ImGui::SameLine();
ImGui::SetNextItemWidth(120);
ImGui::SliderFloat("Rot Y", &rot_y, -3.14f, 3.14f, "%.2f");
ImGui::SameLine();
ImGui::SetNextItemWidth(120);
ImGui::SliderFloat("Rot Z", &rot_z, -3.14f, 3.14f, "%.2f");
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
ImGui::SliderFloat("Zoom", &zoom, 0.5f, 3.0f, "%.2f");
// Precompute rotated vertices
struct RotV {
double x;
double y;
double z;
};
std::vector<RotV> rotated(shape.vertices.size());
const double cx = std::cos(rot_x);
const double sx = std::sin(rot_x);
const double cy = std::cos(rot_y);
const double sy = std::sin(rot_y);
const double cz = std::cos(rot_z);
const double sz = std::sin(rot_z);
for (size_t i = 0; i < shape.vertices.size(); ++i) {
const auto& v = shape.vertices[i];
double x = v.x;
double y = v.y;
double z = v.z;
// Rotate around X
double y1 = y * cx - z * sx;
double z1 = y * sx + z * cx;
// Rotate around Y
double x2 = x * cy + z1 * sy;
double z2 = -x * sy + z1 * cy;
// Rotate around Z
double x3 = x2 * cz - y1 * sz;
double y3 = x2 * sz + y1 * cz;
rotated[i] = {x3 * zoom, y3 * zoom, z2 * zoom};
}
struct FaceDepth {
double depth;
size_t idx;
};
std::vector<FaceDepth> order;
order.reserve(shape.faces.size());
for (size_t i = 0; i < shape.faces.size(); ++i) {
double accum = 0.0;
for (auto idx : shape.faces[i].vertex_indices) {
if (idx < rotated.size()) {
accum += rotated[idx].z;
}
}
double avg = shape.faces[i].vertex_indices.empty()
? 0.0
: accum / static_cast<double>(shape.faces[i].vertex_indices.size());
order.push_back({avg, i});
}
std::sort(order.begin(), order.end(),
[](const FaceDepth& a, const FaceDepth& b) {
return a.depth < b.depth; // back to front
});
ImVec2 preview_size(-1, 260);
ImPlotFlags flags = ImPlotFlags_NoLegend | ImPlotFlags_Equal;
if (ImPlot::BeginPlot("PreviewXY", preview_size, flags)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxisLimits(ImAxis_X1, -120, 120, ImGuiCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1, -120, 120, ImGuiCond_Always);
ImDrawList* dl = ImPlot::GetPlotDrawList();
ImVec4 base_color = ImVec4(0.8f, 0.9f, 1.0f, 0.55f);
for (const auto& fd : order) {
const auto& face = shape.faces[fd.idx];
if (face.vertex_indices.size() < 3) {
continue;
}
std::vector<ImVec2> pts;
pts.reserve(face.vertex_indices.size());
for (auto idx : face.vertex_indices) {
if (idx >= rotated.size()) {
continue;
}
ImVec2 p = ImPlot::PlotToPixels(rotated[idx].x, rotated[idx].y);
pts.push_back(p);
}
if (pts.size() < 3) {
continue;
}
ImU32 fill_col = ImGui::GetColorU32(base_color);
ImU32 line_col = ImGui::GetColorU32(ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
dl->AddConvexPolyFilled(pts.data(), static_cast<int>(pts.size()),
fill_col);
dl->AddPolyline(pts.data(), static_cast<int>(pts.size()), line_col,
ImDrawFlags_Closed, 2.0f);
}
// Draw vertices as dots
for (size_t i = 0; i < rotated.size(); ++i) {
ImVec2 p = ImPlot::PlotToPixels(rotated[i].x, rotated[i].y);
ImU32 col = ImGui::GetColorU32(kVertexColor);
dl->AddCircleFilled(p, 4.0f, col);
}
ImPlot::EndPlot();
}
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,97 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_POLYHEDRAL_EDITOR_PANEL_H_
#define YAZE_APP_EDITOR_GRAPHICS_POLYHEDRAL_EDITOR_PANEL_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "app/editor/system/editor_panel.h"
#include "app/gui/core/icons.h"
#include "rom/rom.h"
namespace yaze {
namespace editor {
struct PolyVertex {
int x = 0;
int y = 0;
int z = 0;
};
struct PolyFace {
uint8_t shade = 0;
std::vector<uint8_t> vertex_indices;
};
struct PolyShape {
std::string name;
uint8_t vertex_count = 0;
uint8_t face_count = 0;
uint16_t vertex_ptr = 0;
uint16_t face_ptr = 0;
std::vector<PolyVertex> vertices;
std::vector<PolyFace> faces;
};
class PolyhedralEditorPanel : public EditorPanel {
public:
explicit PolyhedralEditorPanel(Rom* rom = nullptr) : rom_(rom) {}
// ==========================================================================
// EditorPanel Identity
// ==========================================================================
std::string GetId() const override { return "graphics.polyhedral"; }
std::string GetDisplayName() const override { return "Polyhedral Editor"; }
std::string GetIcon() const override { return ICON_MD_VIEW_IN_AR; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 50; }
// ==========================================================================
// EditorPanel Lifecycle
// ==========================================================================
void SetRom(Rom* rom) {
rom_ = rom;
data_loaded_ = false;
}
absl::Status Load();
/**
* @brief Draw the polyhedral editor UI (EditorPanel interface)
*/
void Draw(bool* p_open) override;
/**
* @brief Legacy Update method for backward compatibility
*/
absl::Status Update();
private:
enum class PlotPlane { kXY, kXZ, kYZ };
absl::Status LoadShapes();
absl::Status SaveShapes();
absl::Status WriteShape(const PolyShape& shape);
void DrawShapeEditor(PolyShape& shape);
void DrawVertexList(PolyShape& shape);
void DrawFaceList(PolyShape& shape);
void DrawPlot(const char* label, PlotPlane plane, PolyShape& shape);
void DrawPreview(PolyShape& shape);
uint32_t TablePc() const;
Rom* rom_ = nullptr;
bool data_loaded_ = false;
bool dirty_ = false;
int selected_shape_ = 0;
int selected_vertex_ = 0;
std::vector<PolyShape> shapes_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_POLYHEDRAL_EDITOR_PANEL_H_

View File

@@ -5,7 +5,7 @@
#include <string>
#include "absl/strings/str_format.h"
#include "app/editor/system/editor_card_registry.h"
#include "app/editor/system/panel_manager.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/debug/performance/performance_profiler.h"
#include "app/gfx/resource/arena.h"
@@ -25,43 +25,70 @@ namespace editor {
constexpr uint32_t kRedPen = 0xFF0000FF;
void ScreenEditor::Initialize() {
if (!dependencies_.card_registry)
if (!dependencies_.panel_manager)
return;
auto* card_registry = dependencies_.card_registry;
auto* panel_manager = dependencies_.panel_manager;
card_registry->RegisterCard({.card_id = "screen.dungeon_maps",
.display_name = "Dungeon Maps",
.icon = ICON_MD_MAP,
.category = "Screen",
.shortcut_hint = "Alt+1",
.priority = 10});
card_registry->RegisterCard({.card_id = "screen.inventory_menu",
.display_name = "Inventory Menu",
.icon = ICON_MD_INVENTORY,
.category = "Screen",
.shortcut_hint = "Alt+2",
.priority = 20});
card_registry->RegisterCard({.card_id = "screen.overworld_map",
.display_name = "Overworld Map",
.icon = ICON_MD_PUBLIC,
.category = "Screen",
.shortcut_hint = "Alt+3",
.priority = 30});
card_registry->RegisterCard({.card_id = "screen.title_screen",
.display_name = "Title Screen",
.icon = ICON_MD_TITLE,
.category = "Screen",
.shortcut_hint = "Alt+4",
.priority = 40});
card_registry->RegisterCard({.card_id = "screen.naming_screen",
.display_name = "Naming Screen",
.icon = ICON_MD_EDIT,
.category = "Screen",
.shortcut_hint = "Alt+5",
.priority = 50});
panel_manager->RegisterPanel({.card_id = "screen.dungeon_maps",
.display_name = "Dungeon Maps",
.window_title = " Dungeon Map Editor",
.icon = ICON_MD_MAP,
.category = "Screen",
.shortcut_hint = "Alt+1",
.priority = 10,
.enabled_condition = [this]() { return rom()->is_loaded(); },
.disabled_tooltip = "Load a ROM first"});
panel_manager->RegisterPanel({.card_id = "screen.inventory_menu",
.display_name = "Inventory Menu",
.window_title = " Inventory Menu",
.icon = ICON_MD_INVENTORY,
.category = "Screen",
.shortcut_hint = "Alt+2",
.priority = 20,
.enabled_condition = [this]() { return rom()->is_loaded(); },
.disabled_tooltip = "Load a ROM first"});
panel_manager->RegisterPanel({.card_id = "screen.overworld_map",
.display_name = "Overworld Map",
.window_title = " Overworld Map",
.icon = ICON_MD_PUBLIC,
.category = "Screen",
.shortcut_hint = "Alt+3",
.priority = 30,
.enabled_condition = [this]() { return rom()->is_loaded(); },
.disabled_tooltip = "Load a ROM first"});
panel_manager->RegisterPanel({.card_id = "screen.title_screen",
.display_name = "Title Screen",
.window_title = " Title Screen",
.icon = ICON_MD_TITLE,
.category = "Screen",
.shortcut_hint = "Alt+4",
.priority = 40,
.enabled_condition = [this]() { return rom()->is_loaded(); },
.disabled_tooltip = "Load a ROM first"});
panel_manager->RegisterPanel({.card_id = "screen.naming_screen",
.display_name = "Naming Screen",
.window_title = " Naming Screen",
.icon = ICON_MD_EDIT,
.category = "Screen",
.shortcut_hint = "Alt+5",
.priority = 50,
.enabled_condition = [this]() { return rom()->is_loaded(); },
.disabled_tooltip = "Load a ROM first"});
// Register EditorPanel implementations
panel_manager->RegisterEditorPanel(std::make_unique<DungeonMapsPanel>(
[this]() { DrawDungeonMapsEditor(); }));
panel_manager->RegisterEditorPanel(std::make_unique<InventoryMenuPanel>(
[this]() { DrawInventoryMenuEditor(); }));
panel_manager->RegisterEditorPanel(std::make_unique<OverworldMapScreenPanel>(
[this]() { DrawOverworldMapEditor(); }));
panel_manager->RegisterEditorPanel(std::make_unique<TitleScreenPanel>(
[this]() { DrawTitleScreenEditor(); }));
panel_manager->RegisterEditorPanel(std::make_unique<NamingScreenPanel>(
[this]() { DrawNamingScreenEditor(); }));
// Show title screen by default
card_registry->ShowCard("screen.title_screen");
panel_manager->ShowPanel("screen.title_screen");
}
absl::Status ScreenEditor::Load() {
@@ -70,19 +97,20 @@ absl::Status ScreenEditor::Load() {
ASSIGN_OR_RETURN(dungeon_maps_,
zelda3::LoadDungeonMaps(*rom(), dungeon_map_labels_));
RETURN_IF_ERROR(zelda3::LoadDungeonMapTile16(
tile16_blockset_, *rom(), rom()->graphics_buffer(), false));
tile16_blockset_, *rom(), game_data(), game_data()->graphics_buffer,
false));
// Load graphics sheets and apply dungeon palette
sheets_.try_emplace(0, gfx::Arena::Get().gfx_sheets()[212]);
sheets_.try_emplace(1, gfx::Arena::Get().gfx_sheets()[213]);
sheets_.try_emplace(2, gfx::Arena::Get().gfx_sheets()[214]);
sheets_.try_emplace(3, gfx::Arena::Get().gfx_sheets()[215]);
sheets_[0] = std::make_unique<gfx::Bitmap>(gfx::Arena::Get().gfx_sheets()[212]);
sheets_[1] = std::make_unique<gfx::Bitmap>(gfx::Arena::Get().gfx_sheets()[213]);
sheets_[2] = std::make_unique<gfx::Bitmap>(gfx::Arena::Get().gfx_sheets()[214]);
sheets_[3] = std::make_unique<gfx::Bitmap>(gfx::Arena::Get().gfx_sheets()[215]);
// Apply dungeon palette to all sheets
for (int i = 0; i < 4; i++) {
sheets_[i].SetPalette(*rom()->mutable_dungeon_palette(3));
sheets_[i]->SetPalette(*game_data()->palette_groups.dungeon_main.mutable_palette(3));
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &sheets_[i]);
gfx::Arena::TextureCommandType::CREATE, sheets_[i].get());
}
// Create a single tilemap for tile8 graphics with on-demand texture creation
@@ -94,7 +122,7 @@ absl::Status ScreenEditor::Load() {
// Copy data from all 4 sheets into the combined bitmap
for (int sheet_idx = 0; sheet_idx < 4; sheet_idx++) {
const auto& sheet = sheets_[sheet_idx];
const auto& sheet = *sheets_[sheet_idx];
int dest_y_offset = sheet_idx * 32; // Each sheet is 32 pixels tall
for (int y = 0; y < 32; y++) {
@@ -113,7 +141,7 @@ absl::Status ScreenEditor::Load() {
tile8_tilemap_.tile_size = {8, 8};
tile8_tilemap_.map_size = {256, 256}; // Logical size for tile count
tile8_tilemap_.atlas.Create(tile8_width, tile8_height, 8, tile8_data);
tile8_tilemap_.atlas.SetPalette(*rom()->mutable_dungeon_palette(3));
tile8_tilemap_.atlas.SetPalette(*game_data()->palette_groups.dungeon_main.mutable_palette(3));
// Queue single texture creation for the atlas (not individual tiles)
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
@@ -122,79 +150,9 @@ absl::Status ScreenEditor::Load() {
}
absl::Status ScreenEditor::Update() {
if (!dependencies_.card_registry)
return absl::OkStatus();
auto* card_registry = dependencies_.card_registry;
static gui::EditorCard dungeon_maps_card("Dungeon Maps", ICON_MD_MAP);
static gui::EditorCard inventory_menu_card("Inventory Menu",
ICON_MD_INVENTORY);
static gui::EditorCard overworld_map_card("Overworld Map", ICON_MD_PUBLIC);
static gui::EditorCard title_screen_card("Title Screen", ICON_MD_TITLE);
static gui::EditorCard naming_screen_card("Naming Screen",
ICON_MD_EDIT_ATTRIBUTES);
dungeon_maps_card.SetDefaultSize(800, 600);
inventory_menu_card.SetDefaultSize(800, 600);
overworld_map_card.SetDefaultSize(600, 500);
title_screen_card.SetDefaultSize(600, 500);
naming_screen_card.SetDefaultSize(500, 400);
// Dungeon Maps Card - Check visibility flag exists and is true before
// rendering
bool* dungeon_maps_visible =
card_registry->GetVisibilityFlag("screen.dungeon_maps");
if (dungeon_maps_visible && *dungeon_maps_visible) {
if (dungeon_maps_card.Begin(dungeon_maps_visible)) {
DrawDungeonMapsEditor();
}
dungeon_maps_card.End();
}
// Inventory Menu Card - Check visibility flag exists and is true before
// rendering
bool* inventory_menu_visible =
card_registry->GetVisibilityFlag("screen.inventory_menu");
if (inventory_menu_visible && *inventory_menu_visible) {
if (inventory_menu_card.Begin(inventory_menu_visible)) {
DrawInventoryMenuEditor();
}
inventory_menu_card.End();
}
// Overworld Map Card - Check visibility flag exists and is true before
// rendering
bool* overworld_map_visible =
card_registry->GetVisibilityFlag("screen.overworld_map");
if (overworld_map_visible && *overworld_map_visible) {
if (overworld_map_card.Begin(overworld_map_visible)) {
DrawOverworldMapEditor();
}
overworld_map_card.End();
}
// Title Screen Card - Check visibility flag exists and is true before
// rendering
bool* title_screen_visible =
card_registry->GetVisibilityFlag("screen.title_screen");
if (title_screen_visible && *title_screen_visible) {
if (title_screen_card.Begin(title_screen_visible)) {
DrawTitleScreenEditor();
}
title_screen_card.End();
}
// Naming Screen Card - Check visibility flag exists and is true before
// rendering
bool* naming_screen_visible =
card_registry->GetVisibilityFlag("screen.naming_screen");
if (naming_screen_visible && *naming_screen_visible) {
if (naming_screen_card.Begin(naming_screen_visible)) {
DrawNamingScreenEditor();
}
naming_screen_card.End();
}
// Panel drawing is handled centrally by PanelManager::DrawAllVisiblePanels()
// via the EditorPanel implementations registered in Initialize().
// No local drawing needed here - this fixes duplicate panel rendering.
return status_;
}
@@ -205,8 +163,8 @@ void ScreenEditor::DrawToolset() {
void ScreenEditor::DrawInventoryMenuEditor() {
static bool create = false;
if (!create && rom()->is_loaded()) {
status_ = inventory_.Create(rom());
if (!create && rom()->is_loaded() && game_data()) {
status_ = inventory_.Create(rom(), game_data());
if (status_.ok()) {
palette_ = inventory_.palette();
create = true;
@@ -227,18 +185,27 @@ void ScreenEditor::DrawInventoryMenuEditor() {
ImGui::TableHeadersRow();
ImGui::TableNextColumn();
screen_canvas_.DrawBackground();
screen_canvas_.DrawContextMenu();
screen_canvas_.DrawBitmap(inventory_.bitmap(), 2, create);
screen_canvas_.DrawGrid(32.0f);
screen_canvas_.DrawOverlay();
{
gui::CanvasFrameOptions frame_opts;
frame_opts.draw_grid = true;
frame_opts.grid_step = 32.0f;
frame_opts.render_popups = true;
auto runtime = gui::BeginCanvas(screen_canvas_, frame_opts);
gui::DrawBitmap(runtime, inventory_.bitmap(), 2, create ? 1.0f : 0.0f);
gui::EndCanvas(screen_canvas_, runtime, frame_opts);
}
ImGui::TableNextColumn();
tilesheet_canvas_.DrawBackground(ImVec2(128 * 2 + 2, (192 * 2) + 4));
tilesheet_canvas_.DrawContextMenu();
tilesheet_canvas_.DrawBitmap(inventory_.tilesheet(), 2, create);
tilesheet_canvas_.DrawGrid(16.0f);
tilesheet_canvas_.DrawOverlay();
{
gui::CanvasFrameOptions frame_opts;
frame_opts.canvas_size = ImVec2(128 * 2 + 2, (192 * 2) + 4);
frame_opts.draw_grid = true;
frame_opts.grid_step = 16.0f;
frame_opts.render_popups = true;
auto runtime = gui::BeginCanvas(tilesheet_canvas_, frame_opts);
gui::DrawBitmap(runtime, inventory_.tilesheet(), 2, create ? 1.0f : 0.0f);
gui::EndCanvas(tilesheet_canvas_, runtime, frame_opts);
}
ImGui::TableNextColumn();
DrawInventoryItemIcons();
@@ -536,24 +503,42 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() {
gfx::ScopedTimer timer("screen_editor_draw_dungeon_maps_room_gfx");
if (ImGui::BeginChild("##DungeonMapTiles", ImVec2(0, 0), true)) {
// Enhanced tilesheet canvas with improved tile selection
tilesheet_canvas_.DrawBackground(ImVec2((256 * 2) + 2, (192 * 2) + 4));
tilesheet_canvas_.DrawContextMenu();
// Enhanced tilesheet canvas with BeginCanvas/EndCanvas pattern
{
gui::CanvasFrameOptions tilesheet_opts;
tilesheet_opts.canvas_size = ImVec2((256 * 2) + 2, (192 * 2) + 4);
tilesheet_opts.draw_grid = true;
tilesheet_opts.grid_step = 32.0f;
tilesheet_opts.render_popups = true;
// Interactive tile16 selector with grid snapping
if (tilesheet_canvas_.DrawTileSelector(32.f)) {
selected_tile16_ = tilesheet_canvas_.points().front().x / 32 +
(tilesheet_canvas_.points().front().y / 32) * 16;
auto tilesheet_rt = gui::BeginCanvas(tilesheet_canvas_, tilesheet_opts);
// Render selected tile16 and cache tile metadata
gfx::RenderTile16(nullptr, tile16_blockset_, selected_tile16_);
std::ranges::copy(tile16_blockset_.tile_info[selected_tile16_],
current_tile16_info.begin());
// Interactive tile16 selector with grid snapping
ImVec2 selected_pos;
if (gui::DrawTileSelector(tilesheet_rt, 32, 0, &selected_pos)) {
// Double-click detected - handle tile confirmation if needed
}
// Check for single-click selection (legacy compatibility)
if (tilesheet_canvas_.IsMouseHovering() &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (!tilesheet_canvas_.points().empty()) {
selected_tile16_ =
static_cast<int>(tilesheet_canvas_.points().front().x / 32 +
(tilesheet_canvas_.points().front().y / 32) * 16);
// Render selected tile16 and cache tile metadata
gfx::RenderTile16(nullptr, tile16_blockset_, selected_tile16_);
std::ranges::copy(tile16_blockset_.tile_info[selected_tile16_],
current_tile16_info.begin());
}
}
// Use stateless bitmap rendering for tilesheet
gui::DrawBitmap(tilesheet_rt, tile16_blockset_.atlas, 1, 1, 2.0F, 255);
gui::EndCanvas(tilesheet_canvas_, tilesheet_rt, tilesheet_opts);
}
// Use direct bitmap rendering for tilesheet
tilesheet_canvas_.DrawBitmap(tile16_blockset_.atlas, 1, 1, 2.0F, 255);
tilesheet_canvas_.DrawGrid(32.f);
tilesheet_canvas_.DrawOverlay();
if (!tilesheet_canvas_.points().empty() &&
!screen_canvas_.points().empty()) {
@@ -563,73 +548,84 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() {
}
ImGui::Separator();
current_tile_canvas_.DrawBackground(); // ImVec2(64 * 2 + 2, 64 * 2 + 4));
current_tile_canvas_.DrawContextMenu();
// Get tile8 from cache on-demand (only create texture when needed)
if (selected_tile8_ >= 0 && selected_tile8_ < 256) {
auto* cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
// Current tile canvas with BeginCanvas/EndCanvas pattern
{
gui::CanvasFrameOptions current_tile_opts;
current_tile_opts.draw_grid = true;
current_tile_opts.grid_step = 16.0f;
current_tile_opts.render_popups = true;
if (!cached_tile8) {
// Extract tile from atlas and cache it
const int tiles_per_row =
tile8_tilemap_.atlas.width() / 8; // 128 / 8 = 16
const int tile_x = (selected_tile8_ % tiles_per_row) * 8;
const int tile_y = (selected_tile8_ / tiles_per_row) * 8;
auto current_tile_rt =
gui::BeginCanvas(current_tile_canvas_, current_tile_opts);
// Extract 8x8 tile data from atlas
std::vector<uint8_t> tile_data(64);
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
int src_x = tile_x + px;
int src_y = tile_y + py;
int src_index = src_y * tile8_tilemap_.atlas.width() + src_x;
int dst_index = py * 8 + px;
// Get tile8 from cache on-demand (only create texture when needed)
if (selected_tile8_ >= 0 && selected_tile8_ < 256) {
auto* cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
if (src_index < tile8_tilemap_.atlas.size() && dst_index < 64) {
tile_data[dst_index] = tile8_tilemap_.atlas.data()[src_index];
if (!cached_tile8) {
// Extract tile from atlas and cache it
const int tiles_per_row =
tile8_tilemap_.atlas.width() / 8; // 128 / 8 = 16
const int tile_x = (selected_tile8_ % tiles_per_row) * 8;
const int tile_y = (selected_tile8_ / tiles_per_row) * 8;
// Extract 8x8 tile data from atlas
std::vector<uint8_t> tile_data(64);
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
int src_x = tile_x + px;
int src_y = tile_y + py;
int src_index = src_y * tile8_tilemap_.atlas.width() + src_x;
int dst_index = py * 8 + px;
if (src_index < tile8_tilemap_.atlas.size() && dst_index < 64) {
tile_data[dst_index] = tile8_tilemap_.atlas.data()[src_index];
}
}
}
gfx::Bitmap new_tile8(8, 8, 8, tile_data);
new_tile8.SetPalette(tile8_tilemap_.atlas.palette());
tile8_tilemap_.tile_cache.CacheTile(selected_tile8_,
std::move(new_tile8));
cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
}
gfx::Bitmap new_tile8(8, 8, 8, tile_data);
new_tile8.SetPalette(tile8_tilemap_.atlas.palette());
tile8_tilemap_.tile_cache.CacheTile(selected_tile8_,
std::move(new_tile8));
cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
if (cached_tile8 && cached_tile8->is_active()) {
// Create texture on-demand only when needed
if (!cached_tile8->texture()) {
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, cached_tile8);
}
// DrawTilePainter still uses member function (not yet migrated)
if (current_tile_canvas_.DrawTilePainter(*cached_tile8, 16)) {
// Modify the tile16 based on the selected tile and
// current_tile16_info
gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
current_tile16_info[0], current_tile16_info[1],
current_tile16_info[2], current_tile16_info[3],
212, selected_tile16_);
gfx::UpdateTile16(nullptr, tile16_blockset_, selected_tile16_);
}
}
}
if (cached_tile8 && cached_tile8->is_active()) {
// Create texture on-demand only when needed
if (!cached_tile8->texture()) {
// Get selected tile from cache and draw with stateless helper
auto* selected_tile =
tile16_blockset_.tile_cache.GetTile(selected_tile16_);
if (selected_tile && selected_tile->is_active()) {
// Ensure the selected tile has a valid texture
if (!selected_tile->texture()) {
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, cached_tile8);
gfx::Arena::TextureCommandType::CREATE, selected_tile);
}
gui::DrawBitmap(current_tile_rt, *selected_tile, 2, 2, 4.0f, 255);
}
if (current_tile_canvas_.DrawTilePainter(*cached_tile8, 16)) {
// Modify the tile16 based on the selected tile and
// current_tile16_info
gfx::ModifyTile16(tile16_blockset_, rom()->graphics_buffer(),
current_tile16_info[0], current_tile16_info[1],
current_tile16_info[2], current_tile16_info[3], 212,
selected_tile16_);
gfx::UpdateTile16(nullptr, tile16_blockset_, selected_tile16_);
}
}
gui::EndCanvas(current_tile_canvas_, current_tile_rt, current_tile_opts);
}
// Get selected tile from cache
auto* selected_tile = tile16_blockset_.tile_cache.GetTile(selected_tile16_);
if (selected_tile && selected_tile->is_active()) {
// Ensure the selected tile has a valid texture
if (!selected_tile->texture()) {
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, selected_tile);
}
current_tile_canvas_.DrawBitmap(*selected_tile, 2, 2, 4.0f, 255);
}
current_tile_canvas_.DrawGrid(16.f);
current_tile_canvas_.DrawOverlay();
gui::InputTileInfo("TL", &current_tile16_info[0]);
ImGui::SameLine();
@@ -639,7 +635,7 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() {
gui::InputTileInfo("BR", &current_tile16_info[3]);
if (ImGui::Button("Modify Tile16")) {
gfx::ModifyTile16(tile16_blockset_, rom()->graphics_buffer(),
gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
current_tile16_info[0], current_tile16_info[1],
current_tile16_info[2], current_tile16_info[3], 212,
selected_tile16_);
@@ -742,19 +738,19 @@ void ScreenEditor::LoadBinaryGfx() {
std::vector<uint8_t> bin_data((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
if (auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4);
zelda3::LoadDungeonMapTile16(tile16_blockset_, *rom(), converted_bin,
true)
zelda3::LoadDungeonMapTile16(tile16_blockset_, *rom(), game_data(),
converted_bin, true)
.ok()) {
sheets_.clear();
std::vector<std::vector<uint8_t>> gfx_sheets;
for (int i = 0; i < 4; i++) {
gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000),
converted_bin.begin() + ((i + 1) * 0x1000));
sheets_.emplace(i, gfx::Bitmap(128, 32, 8, gfx_sheets[i]));
sheets_[i].SetPalette(*rom()->mutable_dungeon_palette(3));
sheets_[i] = std::make_unique<gfx::Bitmap>(128, 32, 8, gfx_sheets[i]);
sheets_[i]->SetPalette(*game_data()->palette_groups.dungeon_main.mutable_palette(3));
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &sheets_[i]);
gfx::Arena::TextureCommandType::CREATE, sheets_[i].get());
}
binary_gfx_loaded_ = true;
} else {
@@ -767,8 +763,8 @@ void ScreenEditor::LoadBinaryGfx() {
void ScreenEditor::DrawTitleScreenEditor() {
// Initialize title screen on first draw
if (!title_screen_loaded_ && rom()->is_loaded()) {
status_ = title_screen_.Create(rom());
if (!title_screen_loaded_ && rom()->is_loaded() && game_data()) {
status_ = title_screen_.Create(rom(), game_data());
if (!status_.ok()) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error loading title screen: %s",
status_.message().data());

View File

@@ -5,12 +5,13 @@
#include "absl/status/status.h"
#include "app/editor/editor.h"
#include "app/editor/graphics/panels/screen_editor_panels.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/render/tilemap.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/app/editor_layout.h"
#include "app/gui/canvas/canvas.h"
#include "app/rom.h"
#include "rom/rom.h"
#include "imgui/imgui.h"
#include "zelda3/screen/dungeon_map.h"
#include "zelda3/screen/inventory.h"

View File

@@ -0,0 +1,236 @@
#include "app/editor/graphics/sheet_browser_panel.h"
#include <cstdlib>
#include <cstring>
#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 "imgui/imgui.h"
namespace yaze {
namespace editor {
void SheetBrowserPanel::Initialize() {
// Initialize with sensible defaults
thumbnail_scale_ = 2.0f;
columns_ = 2;
}
void SheetBrowserPanel::Draw(bool* p_open) {
// EditorPanel interface - delegate to existing Update() logic
DrawSearchBar();
ImGui::Separator();
DrawBatchOperations();
ImGui::Separator();
DrawSheetGrid();
}
absl::Status SheetBrowserPanel::Update() {
DrawSearchBar();
ImGui::Separator();
DrawBatchOperations();
ImGui::Separator();
DrawSheetGrid();
return absl::OkStatus();
}
void SheetBrowserPanel::DrawSearchBar() {
ImGui::Text("Search:");
ImGui::SameLine();
ImGui::SetNextItemWidth(80);
if (ImGui::InputText("##SheetSearch", search_buffer_, sizeof(search_buffer_),
ImGuiInputTextFlags_CharsHexadecimal)) {
// Parse hex input for sheet number
if (strlen(search_buffer_) > 0) {
int value = static_cast<int>(strtol(search_buffer_, nullptr, 16));
if (value >= 0 && value <= 222) {
state_->SelectSheet(static_cast<uint16_t>(value));
}
}
}
HOVER_HINT("Enter hex sheet number (00-DE)");
ImGui::SameLine();
ImGui::SetNextItemWidth(60);
ImGui::DragInt("##FilterMin", &filter_min_, 1.0f, 0, 222, "%02X");
ImGui::SameLine();
ImGui::Text("-");
ImGui::SameLine();
ImGui::SetNextItemWidth(60);
ImGui::DragInt("##FilterMax", &filter_max_, 1.0f, 0, 222, "%02X");
ImGui::SameLine();
ImGui::Checkbox("Modified", &show_only_modified_);
HOVER_HINT("Show only modified sheets");
}
void SheetBrowserPanel::DrawBatchOperations() {
if (ImGui::Button(ICON_MD_SELECT_ALL " Select All")) {
for (int i = filter_min_; i <= filter_max_; i++) {
state_->selected_sheets.insert(static_cast<uint16_t>(i));
}
}
ImGui::SameLine();
if (ImGui::Button(ICON_MD_DESELECT " Clear")) {
state_->selected_sheets.clear();
}
if (!state_->selected_sheets.empty()) {
ImGui::SameLine();
ImGui::Text("(%zu selected)", state_->selected_sheets.size());
}
// Thumbnail size slider
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
ImGui::SliderFloat("##Scale", &thumbnail_scale_, 1.0f, 4.0f, "%.1fx");
}
void SheetBrowserPanel::DrawSheetGrid() {
ImGui::BeginChild("##SheetGridChild", ImVec2(0, 0), true,
ImGuiWindowFlags_AlwaysVerticalScrollbar);
auto& sheets = gfx::Arena::Get().gfx_sheets();
// Calculate thumbnail size
const float thumb_width = 128 * thumbnail_scale_;
const float thumb_height = 32 * thumbnail_scale_;
const float padding = 4.0f;
// Calculate columns based on available width
float available_width = ImGui::GetContentRegionAvail().x;
columns_ = std::max(1, static_cast<int>(available_width / (thumb_width + padding * 2)));
int col = 0;
for (int i = filter_min_; i <= filter_max_ && i < zelda3::kNumGfxSheets; i++) {
// Filter by modification state if enabled
if (show_only_modified_ &&
state_->modified_sheets.find(static_cast<uint16_t>(i)) ==
state_->modified_sheets.end()) {
continue;
}
if (col > 0) {
ImGui::SameLine();
}
ImGui::PushID(i);
DrawSheetThumbnail(i, sheets[i]);
ImGui::PopID();
col++;
if (col >= columns_) {
col = 0;
}
}
ImGui::EndChild();
}
void SheetBrowserPanel::DrawSheetThumbnail(int sheet_id, gfx::Bitmap& bitmap) {
const float thumb_width = 128 * thumbnail_scale_;
const float thumb_height = 32 * thumbnail_scale_;
bool is_selected = state_->current_sheet_id == static_cast<uint16_t>(sheet_id);
bool is_multi_selected =
state_->selected_sheets.count(static_cast<uint16_t>(sheet_id)) > 0;
bool is_modified =
state_->modified_sheets.count(static_cast<uint16_t>(sheet_id)) > 0;
// Selection highlight
if (is_selected) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.3f, 0.5f, 0.8f, 0.3f));
} else if (is_multi_selected) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.5f, 0.5f, 0.2f, 0.3f));
}
ImGui::BeginChild(absl::StrFormat("##Sheet%02X", sheet_id).c_str(),
ImVec2(thumb_width + 8, thumb_height + 24), true,
ImGuiWindowFlags_NoScrollbar);
gui::BitmapPreviewOptions preview_opts;
preview_opts.canvas_size = ImVec2(thumb_width + 1, thumb_height + 1);
preview_opts.dest_pos = ImVec2(2, 2);
preview_opts.dest_size = ImVec2(thumb_width - 2, thumb_height - 2);
preview_opts.grid_step = 8.0f * thumbnail_scale_;
preview_opts.draw_context_menu = false;
preview_opts.ensure_texture = true;
gui::CanvasFrameOptions frame_opts;
frame_opts.canvas_size = preview_opts.canvas_size;
frame_opts.draw_context_menu = preview_opts.draw_context_menu;
frame_opts.draw_grid = preview_opts.draw_grid;
frame_opts.grid_step = preview_opts.grid_step;
frame_opts.draw_overlay = preview_opts.draw_overlay;
frame_opts.render_popups = preview_opts.render_popups;
{
auto rt = gui::BeginCanvas(thumbnail_canvas_, frame_opts);
gui::DrawBitmapPreview(rt, bitmap, preview_opts);
// Sheet label with modification indicator
std::string label = absl::StrFormat("%02X", sheet_id);
if (is_modified) {
label += "*";
}
// Draw label with background
ImVec2 text_pos = ImGui::GetCursorScreenPos();
ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
thumbnail_canvas_.AddRectFilledAt(
ImVec2(2, 2), ImVec2(text_size.x + 4, text_size.y + 2),
is_modified ? IM_COL32(180, 100, 0, 200) : IM_COL32(0, 100, 0, 180));
thumbnail_canvas_.AddTextAt(ImVec2(4, 2), label,
is_modified ? IM_COL32(255, 200, 100, 255)
: IM_COL32(150, 255, 150, 255));
gui::EndCanvas(thumbnail_canvas_, rt, frame_opts);
}
// Click handling
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
if (ImGui::GetIO().KeyCtrl) {
// Ctrl+click for multi-select
if (is_multi_selected) {
state_->selected_sheets.erase(static_cast<uint16_t>(sheet_id));
} else {
state_->selected_sheets.insert(static_cast<uint16_t>(sheet_id));
}
} else {
// Normal click to select
state_->SelectSheet(static_cast<uint16_t>(sheet_id));
}
}
// Double-click to open in new tab
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
state_->open_sheets.insert(static_cast<uint16_t>(sheet_id));
}
ImGui::EndChild();
if (is_selected || is_multi_selected) {
ImGui::PopStyleColor();
}
// Tooltip with sheet info
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Sheet: 0x%02X (%d)", sheet_id, sheet_id);
if (bitmap.is_active()) {
ImGui::Text("Size: %dx%d", bitmap.width(), bitmap.height());
ImGui::Text("Depth: %d bpp", bitmap.depth());
} else {
ImGui::Text("(Inactive)");
}
if (is_modified) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "Modified");
}
ImGui::EndTooltip();
}
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,94 @@
#ifndef YAZE_APP_EDITOR_GRAPHICS_SHEET_BROWSER_PANEL_H
#define YAZE_APP_EDITOR_GRAPHICS_SHEET_BROWSER_PANEL_H
#include "absl/status/status.h"
#include "app/editor/graphics/graphics_editor_state.h"
#include "app/editor/system/editor_panel.h"
#include "app/gfx/core/bitmap.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/core/icons.h"
namespace yaze {
namespace editor {
/**
* @brief EditorPanel for browsing and selecting graphics sheets
*
* Displays a grid view of all 223 graphics sheets from the ROM.
* Supports single/multi-select, search/filter, and batch operations.
*/
class SheetBrowserPanel : public EditorPanel {
public:
explicit SheetBrowserPanel(GraphicsEditorState* state) : state_(state) {}
// ==========================================================================
// EditorPanel Identity
// ==========================================================================
std::string GetId() const override { return "graphics.sheet_browser_v2"; }
std::string GetDisplayName() const override { return "Sheet Browser"; }
std::string GetIcon() const override { return ICON_MD_VIEW_LIST; }
std::string GetEditorCategory() const override { return "Graphics"; }
int GetPriority() const override { return 10; }
// ==========================================================================
// EditorPanel Lifecycle
// ==========================================================================
/**
* @brief Initialize the panel
*/
void Initialize();
/**
* @brief Draw the sheet browser UI
*/
void Draw(bool* p_open) override;
/**
* @brief Legacy Update method for backward compatibility
* @return Status of the render operation
*/
absl::Status Update();
private:
/**
* @brief Draw the search/filter bar
*/
void DrawSearchBar();
/**
* @brief Draw the sheet grid view
*/
void DrawSheetGrid();
/**
* @brief Draw a single sheet thumbnail
* @param sheet_id Sheet index (0-222)
* @param bitmap The bitmap to display
*/
void DrawSheetThumbnail(int sheet_id, gfx::Bitmap& bitmap);
/**
* @brief Draw batch operation buttons
*/
void DrawBatchOperations();
GraphicsEditorState* state_;
gui::Canvas thumbnail_canvas_;
// Search/filter state
char search_buffer_[16] = {0};
int filter_min_ = 0;
int filter_max_ = 222;
bool show_only_modified_ = false;
// Grid layout
float thumbnail_scale_ = 2.0f;
int columns_ = 2;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_GRAPHICS_SHEET_BROWSER_PANEL_H