backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
247
src/app/editor/graphics/graphics_editor_state.h
Normal file
247
src/app/editor/graphics/graphics_editor_state.h
Normal 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
|
||||
455
src/app/editor/graphics/link_sprite_panel.cc
Normal file
455
src/app/editor/graphics/link_sprite_panel.cc
Normal 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", ¤t, 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
|
||||
170
src/app/editor/graphics/link_sprite_panel.h
Normal file
170
src/app/editor/graphics/link_sprite_panel.h
Normal 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
|
||||
301
src/app/editor/graphics/palette_controls_panel.cc
Normal file
301
src/app/editor/graphics/palette_controls_panel.cc
Normal 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
|
||||
97
src/app/editor/graphics/palette_controls_panel.h
Normal file
97
src/app/editor/graphics/palette_controls_panel.h
Normal 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
|
||||
223
src/app/editor/graphics/paletteset_editor_panel.cc
Normal file
223
src/app/editor/graphics/paletteset_editor_panel.cc
Normal 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
|
||||
|
||||
51
src/app/editor/graphics/paletteset_editor_panel.h
Normal file
51
src/app/editor/graphics/paletteset_editor_panel.h
Normal 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_
|
||||
|
||||
239
src/app/editor/graphics/panels/graphics_editor_panels.h
Normal file
239
src/app/editor/graphics/panels/graphics_editor_panels.h
Normal 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_
|
||||
|
||||
150
src/app/editor/graphics/panels/screen_editor_panels.h
Normal file
150
src/app/editor/graphics/panels/screen_editor_panels.h
Normal 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_
|
||||
988
src/app/editor/graphics/pixel_editor_panel.cc
Normal file
988
src/app/editor/graphics/pixel_editor_panel.cc
Normal 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
|
||||
224
src/app/editor/graphics/pixel_editor_panel.h
Normal file
224
src/app/editor/graphics/pixel_editor_panel.h
Normal 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
|
||||
585
src/app/editor/graphics/polyhedral_editor_panel.cc
Normal file
585
src/app/editor/graphics/polyhedral_editor_panel.cc
Normal 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
|
||||
97
src/app/editor/graphics/polyhedral_editor_panel.h
Normal file
97
src/app/editor/graphics/polyhedral_editor_panel.h
Normal 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_
|
||||
@@ -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", ¤t_tile16_info[0]);
|
||||
ImGui::SameLine();
|
||||
@@ -639,7 +635,7 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() {
|
||||
gui::InputTileInfo("BR", ¤t_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());
|
||||
|
||||
@@ -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"
|
||||
|
||||
236
src/app/editor/graphics/sheet_browser_panel.cc
Normal file
236
src/app/editor/graphics/sheet_browser_panel.cc
Normal 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
|
||||
94
src/app/editor/graphics/sheet_browser_panel.h
Normal file
94
src/app/editor/graphics/sheet_browser_panel.h
Normal 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
|
||||
Reference in New Issue
Block a user