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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
176
src/app/gui/widgets/palette_editor_widget.cc
Normal file
176
src/app/gui/widgets/palette_editor_widget.cc
Normal file
@@ -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<int>(editing_color_.x * 31.0f);
|
||||
int g = static_cast<int>(editing_color_.y * 31.0f);
|
||||
int b = static_cast<int>(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
|
||||
|
||||
56
src/app/gui/widgets/palette_editor_widget.h
Normal file
56
src/app/gui/widgets/palette_editor_widget.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef YAZE_APP_GUI_WIDGETS_PALETTE_EDITOR_WIDGET_H
|
||||
#define YAZE_APP_GUI_WIDGETS_PALETTE_EDITOR_WIDGET_H
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#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<void(int palette_id)> 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<void(int palette_id)> 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
|
||||
|
||||
@@ -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<RoomObject&>(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<int>(std::sqrt(tiles.size()));
|
||||
if (sq * sq == static_cast<int>(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() {
|
||||
|
||||
Reference in New Issue
Block a user