feat(palette): implement centralized PaletteManager for improved color management
- Introduced PaletteManager to handle all palette-related operations, including color modifications, undo/redo functionality, and batch processing. - Updated PaletteEditor and PaletteGroupCard to utilize PaletteManager for managing palette states and modifications, streamlining the editing process. - Enhanced user interface with confirmation popups for discard actions and error notifications for save failures. Benefits: - Centralizes palette management, improving consistency and reducing code duplication across editors. - Enhances user experience by providing clear feedback on unsaved changes and simplifying color operations.
This commit is contained in:
@@ -308,6 +308,9 @@ void AgentEditor::DrawDashboard() {
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Pop the TitleBgActive color pushed at the beginning of DrawDashboard
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void AgentEditor::DrawConfigurationPanel() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "app/gfx/palette_manager.h"
|
||||
#include "app/gfx/performance/performance_profiler.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gui/color.h"
|
||||
@@ -314,6 +315,10 @@ absl::Status PaletteEditor::Load() {
|
||||
std::string(kPaletteGroupNames[i]));
|
||||
}
|
||||
|
||||
// Initialize the centralized PaletteManager with ROM data
|
||||
// This must be done before creating any palette cards
|
||||
gfx::PaletteManager::Get().Initialize(rom_);
|
||||
|
||||
// Initialize palette card instances NOW (after ROM is loaded)
|
||||
ow_main_card_ = std::make_unique<OverworldMainPaletteCard>(rom_);
|
||||
ow_animated_card_ = std::make_unique<OverworldAnimatedPaletteCard>(rom_);
|
||||
@@ -847,42 +852,74 @@ void PaletteEditor::DrawControlPanel() {
|
||||
|
||||
// Quick actions
|
||||
ImGui::Text("Quick Actions:");
|
||||
if (ImGui::Button("Save All Modified", ImVec2(-1, 0))) {
|
||||
if (ow_main_card_ && ow_main_card_->HasUnsavedChanges()) {
|
||||
ow_main_card_->SaveToRom();
|
||||
|
||||
// Use centralized PaletteManager for global operations
|
||||
bool has_unsaved = gfx::PaletteManager::Get().HasUnsavedChanges();
|
||||
size_t modified_count = gfx::PaletteManager::Get().GetModifiedColorCount();
|
||||
|
||||
ImGui::BeginDisabled(!has_unsaved);
|
||||
if (ImGui::Button(absl::StrFormat("Save All (%zu colors)", modified_count).c_str(),
|
||||
ImVec2(-1, 0))) {
|
||||
auto status = gfx::PaletteManager::Get().SaveAllToRom();
|
||||
if (!status.ok()) {
|
||||
// TODO: Show error toast/notification
|
||||
ImGui::OpenPopup("SaveError");
|
||||
}
|
||||
if (ow_animated_card_ && ow_animated_card_->HasUnsavedChanges()) {
|
||||
ow_animated_card_->SaveToRom();
|
||||
}
|
||||
if (dungeon_main_card_ && dungeon_main_card_->HasUnsavedChanges()) {
|
||||
dungeon_main_card_->SaveToRom();
|
||||
}
|
||||
if (sprite_card_ && sprite_card_->HasUnsavedChanges()) {
|
||||
sprite_card_->SaveToRom();
|
||||
}
|
||||
if (sprites_aux1_card_ && sprites_aux1_card_->HasUnsavedChanges()) {
|
||||
sprites_aux1_card_->SaveToRom();
|
||||
}
|
||||
if (sprites_aux2_card_ && sprites_aux2_card_->HasUnsavedChanges()) {
|
||||
sprites_aux2_card_->SaveToRom();
|
||||
}
|
||||
if (sprites_aux3_card_ && sprites_aux3_card_->HasUnsavedChanges()) {
|
||||
sprites_aux3_card_->SaveToRom();
|
||||
}
|
||||
if (equipment_card_ && equipment_card_->HasUnsavedChanges()) {
|
||||
equipment_card_->SaveToRom();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (has_unsaved) {
|
||||
ImGui::SetTooltip("Save all modified colors to ROM");
|
||||
} else {
|
||||
ImGui::SetTooltip("No unsaved changes");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(!has_unsaved);
|
||||
if (ImGui::Button("Discard All Changes", ImVec2(-1, 0))) {
|
||||
if (ow_main_card_) ow_main_card_->DiscardChanges();
|
||||
if (ow_animated_card_) ow_animated_card_->DiscardChanges();
|
||||
if (dungeon_main_card_) dungeon_main_card_->DiscardChanges();
|
||||
if (sprite_card_) sprite_card_->DiscardChanges();
|
||||
if (sprites_aux1_card_) sprites_aux1_card_->DiscardChanges();
|
||||
if (sprites_aux2_card_) sprites_aux2_card_->DiscardChanges();
|
||||
if (sprites_aux3_card_) sprites_aux3_card_->DiscardChanges();
|
||||
if (equipment_card_) equipment_card_->DiscardChanges();
|
||||
ImGui::OpenPopup("ConfirmDiscardAll");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (has_unsaved) {
|
||||
ImGui::SetTooltip("Discard all unsaved changes");
|
||||
} else {
|
||||
ImGui::SetTooltip("No changes to discard");
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation popup for discard
|
||||
if (ImGui::BeginPopupModal("ConfirmDiscardAll", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::Text("Discard all unsaved changes?");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
|
||||
"This will revert %zu modified colors.", modified_count);
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Discard", ImVec2(120, 0))) {
|
||||
gfx::PaletteManager::Get().DiscardAllChanges();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Error popup for save failures
|
||||
if (ImGui::BeginPopupModal("SaveError", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Failed to save changes");
|
||||
ImGui::Text("An error occurred while saving to ROM.");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("OK", ImVec2(120, 0))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <chrono>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gfx/palette_manager.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gui/color.h"
|
||||
#include "app/gui/icons.h"
|
||||
@@ -36,15 +37,8 @@ void PaletteGroupCard::Draw() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy load original palettes on first draw (after derived class is fully constructed)
|
||||
if (original_palettes_.empty()) {
|
||||
auto* palette_group = GetPaletteGroup();
|
||||
if (palette_group) {
|
||||
for (size_t i = 0; i < palette_group->size(); i++) {
|
||||
original_palettes_.push_back(palette_group->palette(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
// PaletteManager handles initialization of original palettes
|
||||
// No need for local snapshot management anymore
|
||||
|
||||
// Main card window
|
||||
if (ImGui::Begin(display_name_.c_str(), &show_)) {
|
||||
@@ -93,7 +87,8 @@ void PaletteGroupCard::Draw() {
|
||||
}
|
||||
|
||||
void PaletteGroupCard::DrawToolbar() {
|
||||
bool has_changes = HasUnsavedChanges();
|
||||
// Query PaletteManager for group-specific modification status
|
||||
bool has_changes = gfx::PaletteManager::Get().IsGroupModified(group_name_);
|
||||
|
||||
// Save button (primary action)
|
||||
ImGui::BeginDisabled(!has_changes);
|
||||
@@ -116,27 +111,34 @@ void PaletteGroupCard::DrawToolbar() {
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Modified indicator badge
|
||||
// Modified indicator badge (show modified color count for this group)
|
||||
if (has_changes) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
|
||||
"%s %zu modified",
|
||||
ICON_MD_EDIT,
|
||||
modified_palettes_.size());
|
||||
size_t modified_count = 0;
|
||||
auto* group = GetPaletteGroup();
|
||||
if (group) {
|
||||
for (int p = 0; p < group->size(); p++) {
|
||||
if (gfx::PaletteManager::Get().IsPaletteModified(group_name_, p)) {
|
||||
modified_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s %zu modified",
|
||||
ICON_MD_EDIT, modified_count);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(20, 0)); // Spacer
|
||||
ImGui::SameLine();
|
||||
|
||||
// Undo/Redo
|
||||
ImGui::BeginDisabled(!CanUndo());
|
||||
// Undo/Redo (global operations via PaletteManager)
|
||||
ImGui::BeginDisabled(!gfx::PaletteManager::Get().CanUndo());
|
||||
if (ThemedIconButton(ICON_MD_UNDO, "Undo")) {
|
||||
Undo();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!CanRedo());
|
||||
ImGui::BeginDisabled(!gfx::PaletteManager::Get().CanRedo());
|
||||
if (ThemedIconButton(ICON_MD_REDO, "Redo")) {
|
||||
Redo();
|
||||
}
|
||||
@@ -388,178 +390,89 @@ void PaletteGroupCard::DrawBatchOperationsPopup() {
|
||||
|
||||
void PaletteGroupCard::SetColor(int palette_index, int color_index,
|
||||
const gfx::SnesColor& new_color) {
|
||||
auto* palette = GetMutablePalette(palette_index);
|
||||
if (!palette) return;
|
||||
|
||||
auto original_color = (*palette)[color_index];
|
||||
|
||||
// Update in-memory palette
|
||||
(*palette)[color_index] = new_color;
|
||||
|
||||
// Track modification
|
||||
MarkModified(palette_index, color_index);
|
||||
|
||||
// Record for undo
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch())
|
||||
.count();
|
||||
|
||||
undo_stack_.push_back(
|
||||
{palette_index, color_index, original_color, new_color, static_cast<uint64_t>(timestamp)});
|
||||
|
||||
// Limit undo history
|
||||
if (undo_stack_.size() > kMaxUndoHistory) {
|
||||
undo_stack_.erase(undo_stack_.begin());
|
||||
// Delegate to PaletteManager for centralized tracking and undo/redo
|
||||
auto status = gfx::PaletteManager::Get().SetColor(group_name_, palette_index,
|
||||
color_index, new_color);
|
||||
if (!status.ok()) {
|
||||
// TODO: Show error notification
|
||||
return;
|
||||
}
|
||||
|
||||
redo_stack_.clear();
|
||||
|
||||
// Auto-save if enabled
|
||||
// Auto-save if enabled (PaletteManager doesn't handle this)
|
||||
if (auto_save_enabled_) {
|
||||
WriteColorToRom(palette_index, color_index, new_color);
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status PaletteGroupCard::SaveToRom() {
|
||||
auto* palette_group = GetPaletteGroup();
|
||||
if (!palette_group) {
|
||||
return absl::NotFoundError("Palette group not found");
|
||||
}
|
||||
|
||||
// Save each modified palette
|
||||
for (int palette_idx : modified_palettes_) {
|
||||
auto* palette = palette_group->mutable_palette(palette_idx);
|
||||
|
||||
// Write each modified color in this palette
|
||||
for (int color_idx : modified_colors_[palette_idx]) {
|
||||
RETURN_IF_ERROR(WriteColorToRom(palette_idx, color_idx, (*palette)[color_idx]));
|
||||
}
|
||||
}
|
||||
|
||||
// Clear modified flags after successful save
|
||||
modified_palettes_.clear();
|
||||
modified_colors_.clear();
|
||||
|
||||
// Update original palettes
|
||||
original_palettes_.clear();
|
||||
for (size_t i = 0; i < palette_group->size(); i++) {
|
||||
original_palettes_.push_back(palette_group->palette(i));
|
||||
}
|
||||
|
||||
rom_->set_dirty(true);
|
||||
return absl::OkStatus();
|
||||
// Delegate to PaletteManager for centralized save operation
|
||||
return gfx::PaletteManager::Get().SaveGroup(group_name_);
|
||||
}
|
||||
|
||||
void PaletteGroupCard::DiscardChanges() {
|
||||
auto* palette_group = GetPaletteGroup();
|
||||
if (!palette_group) return;
|
||||
|
||||
// Restore all palettes from original
|
||||
for (int palette_idx : modified_palettes_) {
|
||||
if (palette_idx < original_palettes_.size()) {
|
||||
*palette_group->mutable_palette(palette_idx) = original_palettes_[palette_idx];
|
||||
}
|
||||
}
|
||||
|
||||
// Clear modified tracking
|
||||
modified_palettes_.clear();
|
||||
modified_colors_.clear();
|
||||
|
||||
// Clear undo/redo
|
||||
ClearHistory();
|
||||
// Delegate to PaletteManager for centralized discard operation
|
||||
gfx::PaletteManager::Get().DiscardGroup(group_name_);
|
||||
|
||||
// Reset selection
|
||||
selected_color_ = -1;
|
||||
}
|
||||
|
||||
void PaletteGroupCard::ResetPalette(int palette_index) {
|
||||
auto* palette_group = GetPaletteGroup();
|
||||
if (!palette_group || palette_index >= original_palettes_.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore from original
|
||||
*palette_group->mutable_palette(palette_index) = original_palettes_[palette_index];
|
||||
|
||||
// Clear modified flags for this palette
|
||||
ClearModified(palette_index);
|
||||
// Delegate to PaletteManager for centralized reset operation
|
||||
gfx::PaletteManager::Get().ResetPalette(group_name_, palette_index);
|
||||
}
|
||||
|
||||
void PaletteGroupCard::ResetColor(int palette_index, int color_index) {
|
||||
auto original = GetOriginalColor(palette_index, color_index);
|
||||
SetColor(palette_index, color_index, original);
|
||||
|
||||
// Remove from modified tracking
|
||||
if (modified_colors_.contains(palette_index)) {
|
||||
modified_colors_[palette_index].erase(color_index);
|
||||
if (modified_colors_[palette_index].empty()) {
|
||||
modified_palettes_.erase(palette_index);
|
||||
}
|
||||
}
|
||||
// Delegate to PaletteManager for centralized reset operation
|
||||
gfx::PaletteManager::Get().ResetColor(group_name_, palette_index,
|
||||
color_index);
|
||||
}
|
||||
|
||||
// ========== History Management ==========
|
||||
|
||||
void PaletteGroupCard::Undo() {
|
||||
if (!CanUndo()) return;
|
||||
|
||||
auto change = undo_stack_.back();
|
||||
undo_stack_.pop_back();
|
||||
|
||||
// Restore original color
|
||||
auto* palette = GetMutablePalette(change.palette_index);
|
||||
if (palette) {
|
||||
(*palette)[change.color_index] = change.original_color;
|
||||
}
|
||||
|
||||
// Update ROM if auto-save enabled
|
||||
if (auto_save_enabled_) {
|
||||
WriteColorToRom(change.palette_index, change.color_index, change.original_color);
|
||||
}
|
||||
|
||||
// Move to redo stack
|
||||
redo_stack_.push_back(change);
|
||||
// Delegate to PaletteManager's global undo system
|
||||
gfx::PaletteManager::Get().Undo();
|
||||
}
|
||||
|
||||
void PaletteGroupCard::Redo() {
|
||||
if (!CanRedo()) return;
|
||||
|
||||
auto change = redo_stack_.back();
|
||||
redo_stack_.pop_back();
|
||||
|
||||
// Reapply new color
|
||||
auto* palette = GetMutablePalette(change.palette_index);
|
||||
if (palette) {
|
||||
(*palette)[change.color_index] = change.new_color;
|
||||
}
|
||||
|
||||
// Update ROM if auto-save enabled
|
||||
if (auto_save_enabled_) {
|
||||
WriteColorToRom(change.palette_index, change.color_index, change.new_color);
|
||||
}
|
||||
|
||||
// Move back to undo stack
|
||||
undo_stack_.push_back(change);
|
||||
// Delegate to PaletteManager's global redo system
|
||||
gfx::PaletteManager::Get().Redo();
|
||||
}
|
||||
|
||||
void PaletteGroupCard::ClearHistory() {
|
||||
undo_stack_.clear();
|
||||
redo_stack_.clear();
|
||||
// Delegate to PaletteManager's global history
|
||||
gfx::PaletteManager::Get().ClearHistory();
|
||||
}
|
||||
|
||||
// ========== State Queries ==========
|
||||
|
||||
bool PaletteGroupCard::IsPaletteModified(int palette_index) const {
|
||||
return modified_palettes_.contains(palette_index);
|
||||
// Query PaletteManager for modification status
|
||||
return gfx::PaletteManager::Get().IsPaletteModified(group_name_,
|
||||
palette_index);
|
||||
}
|
||||
|
||||
bool PaletteGroupCard::IsColorModified(int palette_index, int color_index) const {
|
||||
auto it = modified_colors_.find(palette_index);
|
||||
if (it == modified_colors_.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second.contains(color_index);
|
||||
bool PaletteGroupCard::IsColorModified(int palette_index,
|
||||
int color_index) const {
|
||||
// Query PaletteManager for modification status
|
||||
return gfx::PaletteManager::Get().IsColorModified(group_name_, palette_index,
|
||||
color_index);
|
||||
}
|
||||
|
||||
bool PaletteGroupCard::HasUnsavedChanges() const {
|
||||
// Query PaletteManager for group-specific modification status
|
||||
return gfx::PaletteManager::Get().IsGroupModified(group_name_);
|
||||
}
|
||||
|
||||
bool PaletteGroupCard::CanUndo() const {
|
||||
// Query PaletteManager for global undo availability
|
||||
return gfx::PaletteManager::Get().CanUndo();
|
||||
}
|
||||
|
||||
bool PaletteGroupCard::CanRedo() const {
|
||||
// Query PaletteManager for global redo availability
|
||||
return gfx::PaletteManager::Get().CanRedo();
|
||||
}
|
||||
|
||||
// ========== Helper Methods ==========
|
||||
@@ -574,10 +487,9 @@ gfx::SnesPalette* PaletteGroupCard::GetMutablePalette(int index) {
|
||||
|
||||
gfx::SnesColor PaletteGroupCard::GetOriginalColor(int palette_index,
|
||||
int color_index) const {
|
||||
if (palette_index >= original_palettes_.size()) {
|
||||
return gfx::SnesColor();
|
||||
}
|
||||
return original_palettes_[palette_index][color_index];
|
||||
// Get original color from PaletteManager's snapshots
|
||||
return gfx::PaletteManager::Get().GetColor(group_name_, palette_index,
|
||||
color_index);
|
||||
}
|
||||
|
||||
absl::Status PaletteGroupCard::WriteColorToRom(int palette_index, int color_index,
|
||||
@@ -586,15 +498,7 @@ absl::Status PaletteGroupCard::WriteColorToRom(int palette_index, int color_inde
|
||||
return rom_->WriteColor(address, color);
|
||||
}
|
||||
|
||||
void PaletteGroupCard::MarkModified(int palette_index, int color_index) {
|
||||
modified_palettes_.insert(palette_index);
|
||||
modified_colors_[palette_index].insert(color_index);
|
||||
}
|
||||
|
||||
void PaletteGroupCard::ClearModified(int palette_index) {
|
||||
modified_palettes_.erase(palette_index);
|
||||
modified_colors_.erase(palette_index);
|
||||
}
|
||||
// MarkModified and ClearModified removed - PaletteManager handles tracking now
|
||||
|
||||
// ========== Export/Import ==========
|
||||
|
||||
|
||||
@@ -124,13 +124,13 @@ class PaletteGroupCard {
|
||||
|
||||
void Undo();
|
||||
void Redo();
|
||||
bool CanUndo() const { return !undo_stack_.empty(); }
|
||||
bool CanRedo() const { return !redo_stack_.empty(); }
|
||||
bool CanUndo() const;
|
||||
bool CanRedo() const;
|
||||
void ClearHistory();
|
||||
|
||||
// ========== State Queries ==========
|
||||
|
||||
bool HasUnsavedChanges() const { return !modified_palettes_.empty(); }
|
||||
bool HasUnsavedChanges() const;
|
||||
bool IsPaletteModified(int palette_index) const;
|
||||
bool IsColorModified(int palette_index, int color_index) const;
|
||||
|
||||
@@ -256,23 +256,11 @@ class PaletteGroupCard {
|
||||
int selected_color_ = -1; // Currently selected color (-1 = none)
|
||||
gfx::SnesColor editing_color_; // Color being edited in picker
|
||||
|
||||
// Modified tracking
|
||||
std::unordered_set<int> modified_palettes_;
|
||||
std::unordered_map<int, std::unordered_set<int>> modified_colors_;
|
||||
|
||||
// Undo/Redo
|
||||
std::vector<ColorChange> undo_stack_;
|
||||
std::vector<ColorChange> redo_stack_;
|
||||
static constexpr size_t kMaxUndoHistory = 100;
|
||||
|
||||
// Settings
|
||||
bool auto_save_enabled_ = false; // Auto-save to ROM on every change
|
||||
bool show_snes_format_ = true; // Show SNES $xxxx format in info
|
||||
bool show_hex_format_ = true; // Show #xxxxxx hex in info
|
||||
|
||||
// Original palettes (loaded from ROM for reset/comparison)
|
||||
std::vector<gfx::SnesPalette> original_palettes_;
|
||||
|
||||
// Card registration
|
||||
gui::CardRegistration card_registration_;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ set(
|
||||
app/gfx/tilemap.cc
|
||||
app/gfx/graphics_optimizer.cc
|
||||
app/gfx/bpp_format_manager.cc
|
||||
app/gfx/palette_manager.cc
|
||||
app/gfx/backend/sdl2_renderer.cc
|
||||
)
|
||||
|
||||
|
||||
534
src/app/gfx/palette_manager.cc
Normal file
534
src/app/gfx/palette_manager.cc
Normal file
@@ -0,0 +1,534 @@
|
||||
#include "palette_manager.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
|
||||
void PaletteManager::Initialize(Rom* rom) {
|
||||
if (!rom) {
|
||||
return;
|
||||
}
|
||||
|
||||
rom_ = rom;
|
||||
|
||||
// Load original palette snapshots for all groups
|
||||
auto* palette_groups = rom_->mutable_palette_group();
|
||||
|
||||
// Snapshot all palette groups
|
||||
const char* group_names[] = {
|
||||
"ow_main", "ow_aux", "ow_animated", "hud", "global_sprites",
|
||||
"armors", "swords", "shields", "sprites_aux1", "sprites_aux2",
|
||||
"sprites_aux3", "dungeon_main", "grass", "3d_object", "ow_mini_map"
|
||||
};
|
||||
|
||||
for (const auto& group_name : group_names) {
|
||||
try {
|
||||
auto* group = palette_groups->get_group(group_name);
|
||||
if (group) {
|
||||
std::vector<SnesPalette> originals;
|
||||
for (size_t i = 0; i < group->size(); i++) {
|
||||
originals.push_back(group->palette(i));
|
||||
}
|
||||
original_palettes_[group_name] = originals;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Group doesn't exist, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any existing state
|
||||
modified_palettes_.clear();
|
||||
modified_colors_.clear();
|
||||
ClearHistory();
|
||||
}
|
||||
|
||||
// ========== Color Operations ==========
|
||||
|
||||
SnesColor PaletteManager::GetColor(const std::string& group_name,
|
||||
int palette_index,
|
||||
int color_index) const {
|
||||
const auto* group = GetGroup(group_name);
|
||||
if (!group || palette_index < 0 || palette_index >= group->size()) {
|
||||
return SnesColor();
|
||||
}
|
||||
|
||||
const auto& palette = group->palette_ref(palette_index);
|
||||
if (color_index < 0 || color_index >= palette.size()) {
|
||||
return SnesColor();
|
||||
}
|
||||
|
||||
return palette[color_index];
|
||||
}
|
||||
|
||||
absl::Status PaletteManager::SetColor(const std::string& group_name,
|
||||
int palette_index, int color_index,
|
||||
const SnesColor& new_color) {
|
||||
if (!IsInitialized()) {
|
||||
return absl::FailedPreconditionError("PaletteManager not initialized");
|
||||
}
|
||||
|
||||
auto* group = GetMutableGroup(group_name);
|
||||
if (!group) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Palette group '%s' not found", group_name));
|
||||
}
|
||||
|
||||
if (palette_index < 0 || palette_index >= group->size()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Palette index %d out of range [0, %d)", palette_index,
|
||||
group->size()));
|
||||
}
|
||||
|
||||
auto* palette = group->mutable_palette(palette_index);
|
||||
if (color_index < 0 || color_index >= palette->size()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Color index %d out of range [0, %d)", color_index,
|
||||
palette->size()));
|
||||
}
|
||||
|
||||
// Get original color
|
||||
SnesColor original_color = (*palette)[color_index];
|
||||
|
||||
// Update in-memory palette
|
||||
(*palette)[color_index] = new_color;
|
||||
|
||||
// Track modification
|
||||
MarkModified(group_name, palette_index, color_index);
|
||||
|
||||
// Record for undo (unless in batch mode - batch changes recorded separately)
|
||||
if (!InBatch()) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch())
|
||||
.count();
|
||||
|
||||
PaletteColorChange change{group_name, palette_index, color_index,
|
||||
original_color, new_color,
|
||||
static_cast<uint64_t>(timestamp_ms)};
|
||||
RecordChange(change);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kColorChanged,
|
||||
group_name, palette_index, color_index};
|
||||
NotifyListeners(event);
|
||||
} else {
|
||||
// Store in batch buffer
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch())
|
||||
.count();
|
||||
batch_changes_.push_back(
|
||||
{group_name, palette_index, color_index, original_color, new_color,
|
||||
static_cast<uint64_t>(timestamp_ms)});
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status PaletteManager::ResetColor(const std::string& group_name,
|
||||
int palette_index, int color_index) {
|
||||
SnesColor original = GetOriginalColor(group_name, palette_index, color_index);
|
||||
return SetColor(group_name, palette_index, color_index, original);
|
||||
}
|
||||
|
||||
absl::Status PaletteManager::ResetPalette(const std::string& group_name,
|
||||
int palette_index) {
|
||||
if (!IsInitialized()) {
|
||||
return absl::FailedPreconditionError("PaletteManager not initialized");
|
||||
}
|
||||
|
||||
// Check if original snapshot exists
|
||||
auto it = original_palettes_.find(group_name);
|
||||
if (it == original_palettes_.end() ||
|
||||
palette_index >= it->second.size()) {
|
||||
return absl::NotFoundError("Original palette not found");
|
||||
}
|
||||
|
||||
auto* group = GetMutableGroup(group_name);
|
||||
if (!group || palette_index >= group->size()) {
|
||||
return absl::NotFoundError("Palette group or index not found");
|
||||
}
|
||||
|
||||
// Restore from original
|
||||
*group->mutable_palette(palette_index) = it->second[palette_index];
|
||||
|
||||
// Clear modified flags for this palette
|
||||
modified_palettes_[group_name].erase(palette_index);
|
||||
modified_colors_[group_name].erase(palette_index);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kPaletteReset,
|
||||
group_name, palette_index, -1};
|
||||
NotifyListeners(event);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ========== Dirty Tracking ==========
|
||||
|
||||
bool PaletteManager::HasUnsavedChanges() const {
|
||||
return !modified_palettes_.empty();
|
||||
}
|
||||
|
||||
std::vector<std::string> PaletteManager::GetModifiedGroups() const {
|
||||
std::vector<std::string> groups;
|
||||
for (const auto& [group_name, _] : modified_palettes_) {
|
||||
groups.push_back(group_name);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
bool PaletteManager::IsGroupModified(const std::string& group_name) const {
|
||||
auto it = modified_palettes_.find(group_name);
|
||||
return it != modified_palettes_.end() && !it->second.empty();
|
||||
}
|
||||
|
||||
bool PaletteManager::IsPaletteModified(const std::string& group_name,
|
||||
int palette_index) const {
|
||||
auto it = modified_palettes_.find(group_name);
|
||||
if (it == modified_palettes_.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second.contains(palette_index);
|
||||
}
|
||||
|
||||
bool PaletteManager::IsColorModified(const std::string& group_name,
|
||||
int palette_index,
|
||||
int color_index) const {
|
||||
auto group_it = modified_colors_.find(group_name);
|
||||
if (group_it == modified_colors_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto pal_it = group_it->second.find(palette_index);
|
||||
if (pal_it == group_it->second.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pal_it->second.contains(color_index);
|
||||
}
|
||||
|
||||
size_t PaletteManager::GetModifiedColorCount() const {
|
||||
size_t count = 0;
|
||||
for (const auto& [_, palette_map] : modified_colors_) {
|
||||
for (const auto& [__, color_set] : palette_map) {
|
||||
count += color_set.size();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ========== Persistence ==========
|
||||
|
||||
absl::Status PaletteManager::SaveGroup(const std::string& group_name) {
|
||||
if (!IsInitialized()) {
|
||||
return absl::FailedPreconditionError("PaletteManager not initialized");
|
||||
}
|
||||
|
||||
auto* group = GetMutableGroup(group_name);
|
||||
if (!group) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Palette group '%s' not found", group_name));
|
||||
}
|
||||
|
||||
// Get modified palettes for this group
|
||||
auto pal_it = modified_palettes_.find(group_name);
|
||||
if (pal_it == modified_palettes_.end() || pal_it->second.empty()) {
|
||||
// No changes to save
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Write each modified palette
|
||||
for (int palette_idx : pal_it->second) {
|
||||
auto* palette = group->mutable_palette(palette_idx);
|
||||
|
||||
// Get modified colors for this palette
|
||||
auto color_it = modified_colors_[group_name].find(palette_idx);
|
||||
if (color_it != modified_colors_[group_name].end()) {
|
||||
for (int color_idx : color_it->second) {
|
||||
// Calculate ROM address
|
||||
uint32_t address =
|
||||
GetPaletteAddress(group_name, palette_idx, color_idx);
|
||||
|
||||
// Write color to ROM
|
||||
RETURN_IF_ERROR(rom_->WriteColor(address, (*palette)[color_idx]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update original snapshots
|
||||
auto& originals = original_palettes_[group_name];
|
||||
for (size_t i = 0; i < group->size() && i < originals.size(); i++) {
|
||||
originals[i] = group->palette(i);
|
||||
}
|
||||
|
||||
// Clear modified flags for this group
|
||||
ClearModifiedFlags(group_name);
|
||||
|
||||
// Mark ROM as dirty
|
||||
rom_->set_dirty(true);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kGroupSaved, group_name,
|
||||
-1, -1};
|
||||
NotifyListeners(event);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status PaletteManager::SaveAllToRom() {
|
||||
if (!IsInitialized()) {
|
||||
return absl::FailedPreconditionError("PaletteManager not initialized");
|
||||
}
|
||||
|
||||
// Save all modified groups
|
||||
for (const auto& group_name : GetModifiedGroups()) {
|
||||
RETURN_IF_ERROR(SaveGroup(group_name));
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kAllSaved, "", -1, -1};
|
||||
NotifyListeners(event);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void PaletteManager::DiscardGroup(const std::string& group_name) {
|
||||
if (!IsInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* group = GetMutableGroup(group_name);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get modified palettes
|
||||
auto pal_it = modified_palettes_.find(group_name);
|
||||
if (pal_it == modified_palettes_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore from original snapshots
|
||||
auto orig_it = original_palettes_.find(group_name);
|
||||
if (orig_it != original_palettes_.end()) {
|
||||
for (int palette_idx : pal_it->second) {
|
||||
if (palette_idx < orig_it->second.size()) {
|
||||
*group->mutable_palette(palette_idx) = orig_it->second[palette_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear modified flags
|
||||
ClearModifiedFlags(group_name);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kGroupDiscarded,
|
||||
group_name, -1, -1};
|
||||
NotifyListeners(event);
|
||||
}
|
||||
|
||||
void PaletteManager::DiscardAllChanges() {
|
||||
if (!IsInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Discard all modified groups
|
||||
for (const auto& group_name : GetModifiedGroups()) {
|
||||
DiscardGroup(group_name);
|
||||
}
|
||||
|
||||
// Clear undo/redo
|
||||
ClearHistory();
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kAllDiscarded, "", -1,
|
||||
-1};
|
||||
NotifyListeners(event);
|
||||
}
|
||||
|
||||
// ========== Undo/Redo ==========
|
||||
|
||||
void PaletteManager::Undo() {
|
||||
if (!CanUndo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto change = undo_stack_.back();
|
||||
undo_stack_.pop_back();
|
||||
|
||||
// Restore original color
|
||||
auto* group = GetMutableGroup(change.group_name);
|
||||
if (group && change.palette_index < group->size()) {
|
||||
auto* palette = group->mutable_palette(change.palette_index);
|
||||
if (change.color_index < palette->size()) {
|
||||
(*palette)[change.color_index] = change.original_color;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to redo stack
|
||||
redo_stack_.push_back(change);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kColorChanged,
|
||||
change.group_name, change.palette_index,
|
||||
change.color_index};
|
||||
NotifyListeners(event);
|
||||
}
|
||||
|
||||
void PaletteManager::Redo() {
|
||||
if (!CanRedo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto change = redo_stack_.back();
|
||||
redo_stack_.pop_back();
|
||||
|
||||
// Reapply new color
|
||||
auto* group = GetMutableGroup(change.group_name);
|
||||
if (group && change.palette_index < group->size()) {
|
||||
auto* palette = group->mutable_palette(change.palette_index);
|
||||
if (change.color_index < palette->size()) {
|
||||
(*palette)[change.color_index] = change.new_color;
|
||||
}
|
||||
}
|
||||
|
||||
// Move back to undo stack
|
||||
undo_stack_.push_back(change);
|
||||
|
||||
// Notify listeners
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kColorChanged,
|
||||
change.group_name, change.palette_index,
|
||||
change.color_index};
|
||||
NotifyListeners(event);
|
||||
}
|
||||
|
||||
void PaletteManager::ClearHistory() {
|
||||
undo_stack_.clear();
|
||||
redo_stack_.clear();
|
||||
}
|
||||
|
||||
// ========== Change Notifications ==========
|
||||
|
||||
int PaletteManager::RegisterChangeListener(ChangeCallback callback) {
|
||||
int id = next_callback_id_++;
|
||||
change_listeners_[id] = callback;
|
||||
return id;
|
||||
}
|
||||
|
||||
void PaletteManager::UnregisterChangeListener(int callback_id) {
|
||||
change_listeners_.erase(callback_id);
|
||||
}
|
||||
|
||||
// ========== Batch Operations ==========
|
||||
|
||||
void PaletteManager::BeginBatch() {
|
||||
batch_depth_++;
|
||||
if (batch_depth_ == 1) {
|
||||
batch_changes_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PaletteManager::EndBatch() {
|
||||
if (batch_depth_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
batch_depth_--;
|
||||
|
||||
if (batch_depth_ == 0 && !batch_changes_.empty()) {
|
||||
// Commit all batch changes as a single undo step
|
||||
for (const auto& change : batch_changes_) {
|
||||
RecordChange(change);
|
||||
|
||||
// Notify listeners for each change
|
||||
PaletteChangeEvent event{PaletteChangeEvent::Type::kColorChanged,
|
||||
change.group_name, change.palette_index,
|
||||
change.color_index};
|
||||
NotifyListeners(event);
|
||||
}
|
||||
|
||||
batch_changes_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Private Helpers ==========
|
||||
|
||||
PaletteGroup* PaletteManager::GetMutableGroup(const std::string& group_name) {
|
||||
if (!IsInitialized()) {
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
return rom_->mutable_palette_group()->get_group(group_name);
|
||||
} catch (const std::exception&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const PaletteGroup* PaletteManager::GetGroup(
|
||||
const std::string& group_name) const {
|
||||
if (!IsInitialized()) {
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
// Need to const_cast because get_group() is not const
|
||||
return const_cast<Rom*>(rom_)->mutable_palette_group()->get_group(
|
||||
group_name);
|
||||
} catch (const std::exception&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
SnesColor PaletteManager::GetOriginalColor(const std::string& group_name,
|
||||
int palette_index,
|
||||
int color_index) const {
|
||||
auto it = original_palettes_.find(group_name);
|
||||
if (it == original_palettes_.end() || palette_index >= it->second.size()) {
|
||||
return SnesColor();
|
||||
}
|
||||
|
||||
const auto& palette = it->second[palette_index];
|
||||
if (color_index >= palette.size()) {
|
||||
return SnesColor();
|
||||
}
|
||||
|
||||
return palette[color_index];
|
||||
}
|
||||
|
||||
void PaletteManager::RecordChange(const PaletteColorChange& change) {
|
||||
undo_stack_.push_back(change);
|
||||
|
||||
// Limit history size
|
||||
if (undo_stack_.size() > kMaxUndoHistory) {
|
||||
undo_stack_.pop_front();
|
||||
}
|
||||
|
||||
// Clear redo stack (can't redo after a new change)
|
||||
redo_stack_.clear();
|
||||
}
|
||||
|
||||
void PaletteManager::NotifyListeners(const PaletteChangeEvent& event) {
|
||||
for (const auto& [_, callback] : change_listeners_) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
|
||||
void PaletteManager::MarkModified(const std::string& group_name,
|
||||
int palette_index, int color_index) {
|
||||
modified_palettes_[group_name].insert(palette_index);
|
||||
modified_colors_[group_name][palette_index].insert(color_index);
|
||||
}
|
||||
|
||||
void PaletteManager::ClearModifiedFlags(const std::string& group_name) {
|
||||
modified_palettes_.erase(group_name);
|
||||
modified_colors_.erase(group_name);
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
319
src/app/gfx/palette_manager.h
Normal file
319
src/app/gfx/palette_manager.h
Normal file
@@ -0,0 +1,319 @@
|
||||
#ifndef YAZE_APP_GFX_PALETTE_MANAGER_H
|
||||
#define YAZE_APP_GFX_PALETTE_MANAGER_H
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "app/gfx/snes_color.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
|
||||
/**
|
||||
* @brief Represents a single color change operation
|
||||
*/
|
||||
struct PaletteColorChange {
|
||||
std::string group_name; ///< Palette group name (e.g., "ow_main")
|
||||
int palette_index; ///< Index of palette within group
|
||||
int color_index; ///< Index of color within palette
|
||||
SnesColor original_color; ///< Original color before change
|
||||
SnesColor new_color; ///< New color after change
|
||||
uint64_t timestamp_ms; ///< Timestamp in milliseconds
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Event notification for palette changes
|
||||
*/
|
||||
struct PaletteChangeEvent {
|
||||
enum class Type {
|
||||
kColorChanged, ///< Single color was modified
|
||||
kPaletteReset, ///< Entire palette was reset
|
||||
kGroupSaved, ///< Palette group was saved to ROM
|
||||
kGroupDiscarded, ///< Palette group changes were discarded
|
||||
kAllSaved, ///< All changes saved to ROM
|
||||
kAllDiscarded ///< All changes discarded
|
||||
};
|
||||
|
||||
Type type;
|
||||
std::string group_name;
|
||||
int palette_index = -1;
|
||||
int color_index = -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Centralized palette management system
|
||||
*
|
||||
* Singleton coordinator for ALL palette editing operations.
|
||||
* Provides:
|
||||
* - Global dirty tracking across all palette groups
|
||||
* - Transaction-based editing with automatic ROM synchronization
|
||||
* - Unified undo/redo stack shared across all editors
|
||||
* - Batch operations (save all, discard all)
|
||||
* - Change notifications via observer pattern
|
||||
* - Conflict resolution when multiple editors modify same palette
|
||||
*
|
||||
* Thread-safety: This class is NOT thread-safe. All operations must
|
||||
* be called from the main UI thread.
|
||||
*/
|
||||
class PaletteManager {
|
||||
public:
|
||||
using ChangeCallback = std::function<void(const PaletteChangeEvent&)>;
|
||||
|
||||
/// Get the singleton instance
|
||||
static PaletteManager& Get() {
|
||||
static PaletteManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Delete copy/move constructors and assignment operators
|
||||
PaletteManager(const PaletteManager&) = delete;
|
||||
PaletteManager& operator=(const PaletteManager&) = delete;
|
||||
PaletteManager(PaletteManager&&) = delete;
|
||||
PaletteManager& operator=(PaletteManager&&) = delete;
|
||||
|
||||
// ========== Initialization ==========
|
||||
|
||||
/**
|
||||
* @brief Initialize the palette manager with ROM data
|
||||
* @param rom Pointer to ROM instance (must outlive PaletteManager)
|
||||
*/
|
||||
void Initialize(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Check if manager is initialized
|
||||
*/
|
||||
bool IsInitialized() const { return rom_ != nullptr; }
|
||||
|
||||
// ========== Color Operations ==========
|
||||
|
||||
/**
|
||||
* @brief Get a color from a palette
|
||||
* @param group_name Palette group name
|
||||
* @param palette_index Palette index within group
|
||||
* @param color_index Color index within palette
|
||||
* @return The color, or default SnesColor if invalid indices
|
||||
*/
|
||||
SnesColor GetColor(const std::string& group_name, int palette_index,
|
||||
int color_index) const;
|
||||
|
||||
/**
|
||||
* @brief Set a color in a palette (records change for undo)
|
||||
* @param group_name Palette group name
|
||||
* @param palette_index Palette index within group
|
||||
* @param color_index Color index within palette
|
||||
* @param new_color The new color value
|
||||
* @return Status of the operation
|
||||
*/
|
||||
absl::Status SetColor(const std::string& group_name, int palette_index,
|
||||
int color_index, const SnesColor& new_color);
|
||||
|
||||
/**
|
||||
* @brief Reset a single color to its original ROM value
|
||||
*/
|
||||
absl::Status ResetColor(const std::string& group_name, int palette_index,
|
||||
int color_index);
|
||||
|
||||
/**
|
||||
* @brief Reset an entire palette to original ROM values
|
||||
*/
|
||||
absl::Status ResetPalette(const std::string& group_name, int palette_index);
|
||||
|
||||
// ========== Dirty Tracking ==========
|
||||
|
||||
/**
|
||||
* @brief Check if there are ANY unsaved changes
|
||||
*/
|
||||
bool HasUnsavedChanges() const;
|
||||
|
||||
/**
|
||||
* @brief Get list of modified palette group names
|
||||
*/
|
||||
std::vector<std::string> GetModifiedGroups() const;
|
||||
|
||||
/**
|
||||
* @brief Check if a specific palette group has modifications
|
||||
*/
|
||||
bool IsGroupModified(const std::string& group_name) const;
|
||||
|
||||
/**
|
||||
* @brief Check if a specific palette is modified
|
||||
*/
|
||||
bool IsPaletteModified(const std::string& group_name,
|
||||
int palette_index) const;
|
||||
|
||||
/**
|
||||
* @brief Check if a specific color is modified
|
||||
*/
|
||||
bool IsColorModified(const std::string& group_name, int palette_index,
|
||||
int color_index) const;
|
||||
|
||||
/**
|
||||
* @brief Get count of modified colors across all groups
|
||||
*/
|
||||
size_t GetModifiedColorCount() const;
|
||||
|
||||
// ========== Persistence ==========
|
||||
|
||||
/**
|
||||
* @brief Save a specific palette group to ROM
|
||||
*/
|
||||
absl::Status SaveGroup(const std::string& group_name);
|
||||
|
||||
/**
|
||||
* @brief Save ALL modified palettes to ROM
|
||||
*/
|
||||
absl::Status SaveAllToRom();
|
||||
|
||||
/**
|
||||
* @brief Discard changes for a specific group
|
||||
*/
|
||||
void DiscardGroup(const std::string& group_name);
|
||||
|
||||
/**
|
||||
* @brief Discard ALL unsaved changes
|
||||
*/
|
||||
void DiscardAllChanges();
|
||||
|
||||
// ========== Undo/Redo ==========
|
||||
|
||||
/**
|
||||
* @brief Undo the most recent change
|
||||
*/
|
||||
void Undo();
|
||||
|
||||
/**
|
||||
* @brief Redo the most recently undone change
|
||||
*/
|
||||
void Redo();
|
||||
|
||||
/**
|
||||
* @brief Check if undo is available
|
||||
*/
|
||||
bool CanUndo() const { return !undo_stack_.empty(); }
|
||||
|
||||
/**
|
||||
* @brief Check if redo is available
|
||||
*/
|
||||
bool CanRedo() const { return !redo_stack_.empty(); }
|
||||
|
||||
/**
|
||||
* @brief Get size of undo stack
|
||||
*/
|
||||
size_t GetUndoStackSize() const { return undo_stack_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Get size of redo stack
|
||||
*/
|
||||
size_t GetRedoStackSize() const { return redo_stack_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Clear undo/redo history
|
||||
*/
|
||||
void ClearHistory();
|
||||
|
||||
// ========== Change Notifications ==========
|
||||
|
||||
/**
|
||||
* @brief Register a callback for palette change events
|
||||
* @return Unique ID for this callback (use to unregister)
|
||||
*/
|
||||
int RegisterChangeListener(ChangeCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Unregister a change listener
|
||||
*/
|
||||
void UnregisterChangeListener(int callback_id);
|
||||
|
||||
// ========== Batch Operations ==========
|
||||
|
||||
/**
|
||||
* @brief Begin a batch operation (groups multiple changes into one undo step)
|
||||
* @note Must be paired with EndBatch()
|
||||
*/
|
||||
void BeginBatch();
|
||||
|
||||
/**
|
||||
* @brief End a batch operation
|
||||
*/
|
||||
void EndBatch();
|
||||
|
||||
/**
|
||||
* @brief Check if currently in a batch operation
|
||||
*/
|
||||
bool InBatch() const { return batch_depth_ > 0; }
|
||||
|
||||
private:
|
||||
PaletteManager() = default;
|
||||
~PaletteManager() = default;
|
||||
|
||||
/// Helper: Get mutable palette group
|
||||
PaletteGroup* GetMutableGroup(const std::string& group_name);
|
||||
|
||||
/// Helper: Get const palette group
|
||||
const PaletteGroup* GetGroup(const std::string& group_name) const;
|
||||
|
||||
/// Helper: Get original color from snapshot
|
||||
SnesColor GetOriginalColor(const std::string& group_name, int palette_index,
|
||||
int color_index) const;
|
||||
|
||||
/// Helper: Record a change for undo
|
||||
void RecordChange(const PaletteColorChange& change);
|
||||
|
||||
/// Helper: Notify all listeners of an event
|
||||
void NotifyListeners(const PaletteChangeEvent& event);
|
||||
|
||||
/// Helper: Mark a color as modified
|
||||
void MarkModified(const std::string& group_name, int palette_index,
|
||||
int color_index);
|
||||
|
||||
/// Helper: Clear modified flags for a group
|
||||
void ClearModifiedFlags(const std::string& group_name);
|
||||
|
||||
// ========== Member Variables ==========
|
||||
|
||||
/// ROM instance (not owned)
|
||||
Rom* rom_ = nullptr;
|
||||
|
||||
/// Original palette snapshots (loaded from ROM for reset/comparison)
|
||||
/// Key: group_name, Value: vector of original palettes
|
||||
std::unordered_map<std::string, std::vector<SnesPalette>>
|
||||
original_palettes_;
|
||||
|
||||
/// Modified tracking
|
||||
/// Key: group_name, Value: set of modified palette indices
|
||||
std::unordered_map<std::string, std::unordered_set<int>>
|
||||
modified_palettes_;
|
||||
|
||||
/// Detailed color modification tracking
|
||||
/// Key: group_name, Value: map of palette_index -> set of color indices
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<int, std::unordered_set<int>>>
|
||||
modified_colors_;
|
||||
|
||||
/// Undo/redo stacks
|
||||
std::deque<PaletteColorChange> undo_stack_;
|
||||
std::deque<PaletteColorChange> redo_stack_;
|
||||
static constexpr size_t kMaxUndoHistory = 500;
|
||||
|
||||
/// Change listeners
|
||||
std::unordered_map<int, ChangeCallback> change_listeners_;
|
||||
int next_callback_id_ = 1;
|
||||
|
||||
/// Batch operation support
|
||||
int batch_depth_ = 0;
|
||||
std::vector<PaletteColorChange> batch_changes_;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_GFX_PALETTE_MANAGER_H
|
||||
@@ -99,13 +99,19 @@ std::vector<SnesColor> GetColFileData(uint8_t* data) {
|
||||
}
|
||||
|
||||
void SnesColor::set_rgb(const ImVec4 val) {
|
||||
rgb_.x = val.x / kColorByteMax;
|
||||
rgb_.y = val.y / kColorByteMax;
|
||||
rgb_.z = val.z / kColorByteMax;
|
||||
// ImGui ColorPicker returns colors in 0-1 range, but internally we store 0-255
|
||||
// Convert from 0-1 normalized to 0-255 range
|
||||
rgb_.x = val.x * kColorByteMax;
|
||||
rgb_.y = val.y * kColorByteMax;
|
||||
rgb_.z = val.z * kColorByteMax;
|
||||
rgb_.w = kColorByteMaxF; // Alpha always 255
|
||||
|
||||
// Create snes_color struct for ROM/SNES conversion (expects 0-255 range)
|
||||
snes_color color;
|
||||
color.red = val.x;
|
||||
color.green = val.y;
|
||||
color.blue = val.z;
|
||||
color.red = static_cast<uint16_t>(rgb_.x);
|
||||
color.green = static_cast<uint16_t>(rgb_.y);
|
||||
color.blue = static_cast<uint16_t>(rgb_.z);
|
||||
|
||||
rom_color_ = color;
|
||||
snes_ = ConvertRgbToSnes(color);
|
||||
modified = true;
|
||||
@@ -114,7 +120,9 @@ void SnesColor::set_rgb(const ImVec4 val) {
|
||||
void SnesColor::set_snes(uint16_t val) {
|
||||
snes_ = val;
|
||||
snes_color col = ConvertSnesToRgb(val);
|
||||
// ConvertSnesToRgb returns 0-255 range, store directly (not normalized 0-1)
|
||||
rgb_ = ImVec4(col.red, col.green, col.blue, kColorByteMaxF);
|
||||
rom_color_ = col;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,17 +40,26 @@ class SnesColor {
|
||||
constexpr SnesColor()
|
||||
: rgb_({0.f, 0.f, 0.f, 0.f}), snes_(0), rom_color_({0, 0, 0}) {}
|
||||
|
||||
explicit SnesColor(const ImVec4 val) : rgb_(val) {
|
||||
explicit SnesColor(const ImVec4 val) {
|
||||
// ImVec4 from ImGui is in 0-1 range, convert to 0-255 for internal storage
|
||||
rgb_.x = val.x * kColorByteMax;
|
||||
rgb_.y = val.y * kColorByteMax;
|
||||
rgb_.z = val.z * kColorByteMax;
|
||||
rgb_.w = kColorByteMaxF; // Alpha always 255
|
||||
|
||||
snes_color color;
|
||||
color.red = static_cast<uint16_t>(val.x * kColorByteMax);
|
||||
color.green = static_cast<uint16_t>(val.y * kColorByteMax);
|
||||
color.blue = static_cast<uint16_t>(val.z * kColorByteMax);
|
||||
color.red = static_cast<uint16_t>(rgb_.x);
|
||||
color.green = static_cast<uint16_t>(rgb_.y);
|
||||
color.blue = static_cast<uint16_t>(rgb_.z);
|
||||
rom_color_ = color;
|
||||
snes_ = ConvertRgbToSnes(color);
|
||||
}
|
||||
|
||||
explicit SnesColor(const uint16_t val) : snes_(val) {
|
||||
snes_color color = ConvertSnesToRgb(val);
|
||||
rgb_ = ImVec4(color.red, color.green, color.blue, 0.f);
|
||||
// ConvertSnesToRgb returns 0-255 range, store directly
|
||||
rgb_ = ImVec4(color.red, color.green, color.blue, kColorByteMaxF);
|
||||
rom_color_ = color;
|
||||
}
|
||||
|
||||
explicit SnesColor(const snes_color val)
|
||||
|
||||
@@ -35,16 +35,21 @@ IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor& color,
|
||||
|
||||
IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor* color,
|
||||
ImGuiColorEditFlags flags) {
|
||||
// Convert from internal 0-255 storage to 0-1 for ImGui
|
||||
ImVec4 displayColor = ConvertSnesColorToImVec4(*color);
|
||||
|
||||
// Call the original ImGui::ColorEdit4 with the converted color
|
||||
bool pressed =
|
||||
bool changed =
|
||||
ImGui::ColorEdit4(label.data(), (float*)&displayColor.x, flags);
|
||||
|
||||
color->set_rgb(displayColor);
|
||||
color->set_snes(gfx::ConvertRgbToSnes(displayColor));
|
||||
// Only update if the user actually changed the color
|
||||
if (changed) {
|
||||
// set_rgb() handles conversion from 0-1 (ImGui) to 0-255 (internal)
|
||||
// and automatically calculates snes_ value - no need to call set_snes separately
|
||||
color->set_rgb(displayColor);
|
||||
}
|
||||
|
||||
return pressed;
|
||||
return changed;
|
||||
}
|
||||
|
||||
IMGUI_API bool DisplayPalette(gfx::SnesPalette& palette, bool loaded) {
|
||||
@@ -222,17 +227,17 @@ absl::Status DisplayEditablePalette(gfx::SnesPalette& palette,
|
||||
|
||||
if (ImGui::MenuItem("Copy as RGB")) {
|
||||
auto rgb = palette[n].rgb();
|
||||
// rgb is already in 0-255 range, no need to multiply
|
||||
std::string clipboard =
|
||||
absl::StrFormat("(%d,%d,%d)", (int)(rgb.x * 255),
|
||||
(int)(rgb.y * 255), (int)(rgb.z * 255));
|
||||
absl::StrFormat("(%d,%d,%d)", (int)rgb.x, (int)rgb.y, (int)rgb.z);
|
||||
ImGui::SetClipboardText(clipboard.c_str());
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("Copy as Hex")) {
|
||||
auto rgb = palette[n].rgb();
|
||||
// rgb is already in 0-255 range, no need to multiply
|
||||
std::string clipboard =
|
||||
absl::StrFormat("#%02X%02X%02X", (int)(rgb.x * 255),
|
||||
(int)(rgb.y * 255), (int)(rgb.z * 255));
|
||||
absl::StrFormat("#%02X%02X%02X", (int)rgb.x, (int)rgb.y, (int)rgb.z);
|
||||
ImGui::SetClipboardText(clipboard.c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ if(YAZE_BUILD_TESTS)
|
||||
unit/gfx/snes_tile_test.cc
|
||||
unit/gfx/compression_test.cc
|
||||
unit/gfx/snes_palette_test.cc
|
||||
unit/snes_color_test.cc
|
||||
unit/gui/tile_selector_widget_test.cc
|
||||
unit/gui/canvas_automation_api_test.cc
|
||||
unit/zelda3/overworld_test.cc
|
||||
@@ -98,6 +99,7 @@ if(YAZE_BUILD_TESTS)
|
||||
integration/zelda3/dungeon_room_test.cc
|
||||
integration/zelda3/sprite_position_test.cc
|
||||
integration/zelda3/message_test.cc
|
||||
integration/palette_manager_test.cc
|
||||
)
|
||||
yaze_add_test_suite(yaze_test_stable "stable" OFF ${STABLE_TEST_SOURCES})
|
||||
|
||||
|
||||
360
test/integration/palette_manager_test.cc
Normal file
360
test/integration/palette_manager_test.cc
Normal file
@@ -0,0 +1,360 @@
|
||||
#include "app/gfx/palette_manager.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/snes_color.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
namespace {
|
||||
|
||||
// Test fixture for PaletteManager integration tests
|
||||
class PaletteManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// PaletteManager is a singleton, so we need to reset it between tests
|
||||
// Note: In a real scenario, we'd need a way to reset the singleton
|
||||
// For now, we'll work with the existing instance
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up any test state
|
||||
PaletteManager::Get().ClearHistory();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Initialization Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, InitializationState) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Before initialization, should not be initialized
|
||||
// Note: This might fail if other tests have already initialized it
|
||||
// In production, we'd need a Reset() method for testing
|
||||
|
||||
// After initialization with null ROM, should handle gracefully
|
||||
manager.Initialize(nullptr);
|
||||
EXPECT_FALSE(manager.IsInitialized());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, HasNoUnsavedChangesInitially) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should have no unsaved changes initially
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dirty Tracking Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, TracksModifiedGroups) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, no groups should be modified
|
||||
auto modified_groups = manager.GetModifiedGroups();
|
||||
EXPECT_TRUE(modified_groups.empty());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, GetModifiedColorCount) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, no colors modified
|
||||
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
|
||||
|
||||
// After initialization and making changes, count should increase
|
||||
// (This would require a valid ROM to test properly)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Undo/Redo Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, UndoRedoInitialState) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, should not be able to undo or redo
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
EXPECT_EQ(manager.GetUndoStackSize(), 0);
|
||||
EXPECT_EQ(manager.GetRedoStackSize(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ClearHistoryResetsStacks) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Clear history should reset both stacks
|
||||
manager.ClearHistory();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
EXPECT_EQ(manager.GetUndoStackSize(), 0);
|
||||
EXPECT_EQ(manager.GetRedoStackSize(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, UndoWithoutChangesIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
|
||||
// Should not crash
|
||||
manager.Undo();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, RedoWithoutUndoIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
|
||||
// Should not crash
|
||||
manager.Redo();
|
||||
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Operations Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, BatchModeTracking) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch();
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, NestedBatchOperations) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch();
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch(); // Nested
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_TRUE(manager.InBatch()); // Still in batch (outer)
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_FALSE(manager.InBatch()); // Now out of batch
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, EndBatchWithoutBeginIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
// Should not crash
|
||||
manager.EndBatch();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Change Notification Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, RegisterAndUnregisterListener) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
int callback_count = 0;
|
||||
auto callback = [&callback_count](const PaletteChangeEvent& event) {
|
||||
callback_count++;
|
||||
};
|
||||
|
||||
// Register listener
|
||||
int id = manager.RegisterChangeListener(callback);
|
||||
EXPECT_GT(id, 0);
|
||||
|
||||
// Unregister listener
|
||||
manager.UnregisterChangeListener(id);
|
||||
|
||||
// After unregistering, callback should not be called
|
||||
// (Would need to trigger an event to test this properly)
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, MultipleListeners) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
int callback1_count = 0;
|
||||
int callback2_count = 0;
|
||||
|
||||
auto callback1 = [&callback1_count](const PaletteChangeEvent& event) {
|
||||
callback1_count++;
|
||||
};
|
||||
|
||||
auto callback2 = [&callback2_count](const PaletteChangeEvent& event) {
|
||||
callback2_count++;
|
||||
};
|
||||
|
||||
int id1 = manager.RegisterChangeListener(callback1);
|
||||
int id2 = manager.RegisterChangeListener(callback2);
|
||||
|
||||
EXPECT_NE(id1, id2);
|
||||
|
||||
// Clean up
|
||||
manager.UnregisterChangeListener(id1);
|
||||
manager.UnregisterChangeListener(id2);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Color Query Tests (without ROM)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, GetColorWithoutInitialization) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Getting color without initialization should return default color
|
||||
SnesColor color = manager.GetColor("ow_main", 0, 0);
|
||||
|
||||
// Default SnesColor should have zero values
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, SetColorWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor new_color(0x7FFF);
|
||||
auto status = manager.SetColor("ow_main", 0, 0, new_color);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ResetColorWithoutInitializationReturnsError) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.ResetColor("ow_main", 0, 0);
|
||||
|
||||
// Should return an error or default color
|
||||
// Exact behavior depends on implementation
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ResetPaletteWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.ResetPalette("ow_main", 0);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save/Discard Tests (without ROM)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, SaveGroupWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.SaveGroup("ow_main");
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, SaveAllWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.SaveAllToRom();
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, DiscardGroupWithoutInitializationIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should not crash
|
||||
manager.DiscardGroup("ow_main");
|
||||
|
||||
// No unsaved changes
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, DiscardAllWithoutInitializationIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should not crash
|
||||
manager.DiscardAllChanges();
|
||||
|
||||
// No unsaved changes
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Group Modification Query Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, IsGroupModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsGroupModified("ow_main"));
|
||||
EXPECT_FALSE(manager.IsGroupModified("dungeon_main"));
|
||||
EXPECT_FALSE(manager.IsGroupModified("global_sprites"));
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsPaletteModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 0));
|
||||
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 5));
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsColorModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 0));
|
||||
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 7));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invalid Input Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, SetColorInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor color(0x7FFF);
|
||||
auto status = manager.SetColor("invalid_group", 0, 0, color);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, GetColorInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor color = manager.GetColor("invalid_group", 0, 0);
|
||||
|
||||
// Should return default color
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsGroupModifiedInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsGroupModified("invalid_group"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
258
test/unit/snes_color_test.cc
Normal file
258
test/unit/snes_color_test.cc
Normal file
@@ -0,0 +1,258 @@
|
||||
#include "app/gfx/snes_color.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
namespace {
|
||||
|
||||
// Test fixture for SnesColor tests
|
||||
class SnesColorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Common setup if needed
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RGB Format Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbFromImGuiNormalizedValues) {
|
||||
SnesColor color;
|
||||
|
||||
// ImGui ColorPicker returns values in 0-1 range
|
||||
ImVec4 imgui_color(0.5f, 0.75f, 1.0f, 1.0f);
|
||||
color.set_rgb(imgui_color);
|
||||
|
||||
// Internal storage should be in 0-255 range
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 127.5f); // 0.5 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.y, 191.25f); // 0.75 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f); // 1.0 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f); // Alpha always 255
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbBlackColor) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 black(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(black);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbWhiteColor) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
color.set_rgb(white);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbMidRangeColor) {
|
||||
SnesColor color;
|
||||
|
||||
// Test a mid-range color (medium gray)
|
||||
ImVec4 gray(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
color.set_rgb(gray);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_NEAR(rgb.x, 127.5f, 0.01f);
|
||||
EXPECT_NEAR(rgb.y, 127.5f, 0.01f);
|
||||
EXPECT_NEAR(rgb.z, 127.5f, 0.01f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constructor Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromImVec4) {
|
||||
// ImGui color in 0-1 range
|
||||
ImVec4 imgui_color(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
|
||||
SnesColor color(imgui_color);
|
||||
|
||||
// Should be converted to 0-255 range
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_NEAR(rgb.x, 63.75f, 0.01f); // 0.25 * 255
|
||||
EXPECT_NEAR(rgb.y, 127.5f, 0.01f); // 0.5 * 255
|
||||
EXPECT_NEAR(rgb.z, 191.25f, 0.01f); // 0.75 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromSnesValue) {
|
||||
// SNES BGR555 format: 0x7FFF = white (all bits set in 15-bit color)
|
||||
SnesColor white(0x7FFF);
|
||||
|
||||
auto rgb = white.rgb();
|
||||
// All channels should be max (after BGR555 conversion)
|
||||
EXPECT_GT(rgb.x, 240.0f); // Close to 255
|
||||
EXPECT_GT(rgb.y, 240.0f);
|
||||
EXPECT_GT(rgb.z, 240.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromSnesBlack) {
|
||||
// SNES BGR555 format: 0x0000 = black
|
||||
SnesColor black(0x0000);
|
||||
|
||||
auto rgb = black.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SNES Format Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, SetSnesUpdatesRgb) {
|
||||
SnesColor color;
|
||||
|
||||
// Set a SNES color value
|
||||
color.set_snes(0x7FFF); // White in BGR555
|
||||
|
||||
// RGB should be updated
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_GT(rgb.x, 240.0f);
|
||||
EXPECT_GT(rgb.y, 240.0f);
|
||||
EXPECT_GT(rgb.z, 240.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, RgbToSnesConversion) {
|
||||
SnesColor color;
|
||||
|
||||
// Set pure red in RGB (0-1 range for ImGui)
|
||||
ImVec4 red(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(red);
|
||||
|
||||
// SNES value should be set (BGR555 format)
|
||||
uint16_t snes = color.snes();
|
||||
EXPECT_NE(snes, 0x0000); // Should not be black
|
||||
|
||||
// Extract red component from BGR555 (bits 0-4)
|
||||
uint16_t snes_red = snes & 0x1F;
|
||||
EXPECT_EQ(snes_red, 0x1F); // Max red in 5-bit
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Round-Trip Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, RoundTripImGuiToSnesColor) {
|
||||
// Start with ImGui color
|
||||
ImVec4 original(0.6f, 0.4f, 0.8f, 1.0f);
|
||||
|
||||
// Convert to SnesColor
|
||||
SnesColor color(original);
|
||||
|
||||
// Convert back to ImVec4 (normalized)
|
||||
auto rgb = color.rgb();
|
||||
ImVec4 converted(rgb.x / 255.0f, rgb.y / 255.0f, rgb.z / 255.0f, 1.0f);
|
||||
|
||||
// Should be approximately equal (within floating point precision)
|
||||
EXPECT_NEAR(converted.x, original.x, 0.01f);
|
||||
EXPECT_NEAR(converted.y, original.y, 0.01f);
|
||||
EXPECT_NEAR(converted.z, original.z, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, MultipleSetRgbCalls) {
|
||||
SnesColor color;
|
||||
|
||||
// First color
|
||||
ImVec4 color1(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
color.set_rgb(color1);
|
||||
|
||||
auto rgb1 = color.rgb();
|
||||
EXPECT_NEAR(rgb1.x, 51.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb1.y, 102.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb1.z, 153.0f, 1.0f);
|
||||
|
||||
// Second color (should completely replace)
|
||||
ImVec4 color2(0.8f, 0.6f, 0.4f, 1.0f);
|
||||
color.set_rgb(color2);
|
||||
|
||||
auto rgb2 = color.rgb();
|
||||
EXPECT_NEAR(rgb2.x, 204.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb2.y, 153.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb2.z, 102.0f, 1.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Case Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, HandlesMaxValues) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 max(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
color.set_rgb(max);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, HandlesMinValues) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 min(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(min);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, AlphaAlwaysMaximum) {
|
||||
SnesColor color;
|
||||
|
||||
// Try setting alpha to different values (should always be ignored)
|
||||
ImVec4 color_with_alpha(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
color.set_rgb(color_with_alpha);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f); // Alpha should always be 255
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Modified Flag Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, ModifiedFlagSetOnRgbChange) {
|
||||
SnesColor color;
|
||||
|
||||
EXPECT_FALSE(color.is_modified());
|
||||
|
||||
ImVec4 new_color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
color.set_rgb(new_color);
|
||||
|
||||
EXPECT_TRUE(color.is_modified());
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ModifiedFlagSetOnSnesChange) {
|
||||
SnesColor color;
|
||||
|
||||
EXPECT_FALSE(color.is_modified());
|
||||
|
||||
color.set_snes(0x7FFF);
|
||||
|
||||
EXPECT_TRUE(color.is_modified());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user