diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index cf260190..22b57b66 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -103,6 +103,9 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { if (rooms_ && rom_->is_loaded()) { auto& room = (*rooms_)[room_id]; + // Update object interaction context + object_interaction_.SetCurrentRoom(rooms_, room_id); + // Check if THIS ROOM's buffers need rendering (not global arena!) auto& bg1_bitmap = room.bg1_buffer().bitmap(); bool needs_render = !bg1_bitmap.is_active() || bg1_bitmap.width() == 0; @@ -139,6 +142,13 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { // Render sprites as simple 16x16 squares with labels RenderSprites(room); } + + // Handle object interaction if enabled + if (object_interaction_enabled_) { + object_interaction_.HandleCanvasMouseInput(); + object_interaction_.CheckForObjectSelection(); + object_interaction_.DrawSelectBox(); + } } canvas_.DrawGrid(); diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.h b/src/app/editor/dungeon/dungeon_canvas_viewer.h index 7213ece5..177adf5f 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.h +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.h @@ -6,6 +6,7 @@ #include "app/zelda3/dungeon/object_renderer.h" #include "app/zelda3/dungeon/room.h" #include "app/gfx/snes_palette.h" +#include "dungeon_object_interaction.h" #include "imgui/imgui.h" namespace yaze { @@ -21,7 +22,8 @@ namespace editor { */ class DungeonCanvasViewer { public: - explicit DungeonCanvasViewer(Rom* rom = nullptr) : rom_(rom), object_renderer_(rom) {} + explicit DungeonCanvasViewer(Rom* rom = nullptr) + : rom_(rom), object_renderer_(rom), object_interaction_(&canvas_) {} void DrawDungeonTabView(); void DrawDungeonCanvas(int room_id); @@ -46,6 +48,21 @@ class DungeonCanvasViewer { // Canvas access gui::Canvas& canvas() { return canvas_; } const gui::Canvas& canvas() const { return canvas_; } + + // Object interaction access + DungeonObjectInteraction& object_interaction() { return object_interaction_; } + + // Enable/disable object interaction mode + void SetObjectInteractionEnabled(bool enabled) { object_interaction_enabled_ = enabled; } + bool IsObjectInteractionEnabled() const { return object_interaction_enabled_; } + + // Set the object to be placed + void SetPreviewObject(const zelda3::RoomObject& object) { + object_interaction_.SetPreviewObject(object, true); + } + void ClearPreviewObject() { + object_interaction_.SetPreviewObject(zelda3::RoomObject{0, 0, 0, 0, 0}, false); + } private: void RenderObjectInCanvas(const zelda3::RoomObject &object, @@ -76,12 +93,16 @@ class DungeonCanvasViewer { Rom* rom_ = nullptr; gui::Canvas canvas_{"##DungeonCanvas", ImVec2(0x200, 0x200)}; zelda3::ObjectRenderer object_renderer_; + DungeonObjectInteraction object_interaction_; // Room data std::array* rooms_ = nullptr; ImVector active_rooms_; int current_active_room_tab_ = 0; + // Object interaction state + bool object_interaction_enabled_ = false; + // Palette data uint64_t current_palette_group_id_ = 0; uint64_t current_palette_id_ = 0; diff --git a/src/app/editor/dungeon/dungeon_editor_v2.cc b/src/app/editor/dungeon/dungeon_editor_v2.cc index 75a52675..b2d1167a 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.cc +++ b/src/app/editor/dungeon/dungeon_editor_v2.cc @@ -4,10 +4,11 @@ #include #include "absl/strings/str_format.h" +#include "app/gfx/arena.h" #include "app/gfx/snes_palette.h" #include "app/zelda3/dungeon/room.h" #include "app/gui/icons.h" -#include "app/gui/ui_helpers.h" +#include "app/gui/input.h" #include "imgui/imgui.h" namespace yaze::editor { @@ -17,6 +18,10 @@ namespace yaze::editor { void DungeonEditorV2::Initialize() { // Don't initialize emulator preview yet - ROM might not be loaded // Will be initialized in Load() instead + + // Setup docking class for room windows + room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass"); + room_window_class_.DockingAllowUnclassed = false; // Room windows dock together } absl::Status DungeonEditorV2::Load() { @@ -55,6 +60,9 @@ absl::Status DungeonEditorV2::Load() { // Initialize palette editor with loaded ROM palette_editor_.Initialize(rom_); + // Initialize unified object editor card + object_editor_card_ = std::make_unique(rom_, &canvas_viewer_); + // Wire palette changes to trigger room re-renders palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) { // Re-render all active rooms when palette changes @@ -134,9 +142,17 @@ void DungeonEditorV2::DrawToolset() { // Toggled } + if (toolbar.AddToggle(ICON_MD_IMAGE, &show_room_graphics_, "Toggle Room Graphics")) { + // Toggled + } + toolbar.AddSeparator(); - if (toolbar.AddToggle(ICON_MD_CATEGORY, &show_object_selector_, "Toggle Object Selector")) { + if (toolbar.AddToggle(ICON_MD_CATEGORY, &show_object_selector_, "Toggle Object Selector (Legacy)")) { + // Toggled + } + + if (toolbar.AddToggle(ICON_MD_CONSTRUCTION, &show_object_editor_, "Toggle Object Editor (Unified)")) { // Toggled } @@ -164,8 +180,13 @@ void DungeonEditorV2::DrawLayout() { if (show_entrances_list_) { DrawEntrancesListCard(); } + + // 3b. Room Graphics Card + if (show_room_graphics_) { + DrawRoomGraphicsCard(); + } - // 4. Object Selector/Manager Card (independent, dockable) + // 4. Legacy Object Selector Card (independent, dockable) if (show_object_selector_) { gui::EditorCard object_card( MakeCardTitle("Object Selector").c_str(), @@ -176,6 +197,11 @@ void DungeonEditorV2::DrawLayout() { object_card.End(); } + // 4b. Unified Object Editor Card (new, combines selector + preview + interaction) + if (show_object_editor_ && object_editor_card_) { + object_editor_card_->Draw(&show_object_editor_); + } + // 5. Palette Editor Card (independent, dockable) if (show_palette_editor_) { gui::EditorCard palette_card( @@ -207,9 +233,14 @@ void DungeonEditorV2::DrawLayout() { if (room_cards_.find(room_id) == room_cards_.end()) { room_cards_[room_id] = std::make_shared( card_name_str.c_str(), ICON_MD_GRID_ON, &open); + room_cards_[room_id]->SetDefaultSize(700, 600); } auto& room_card = room_cards_[room_id]; + + // Use docking class to make room cards dock together + ImGui::SetNextWindowClass(&room_window_class_); + if (room_card->Begin(&open)) { DrawRoomTab(room_id); } @@ -239,19 +270,13 @@ void DungeonEditorV2::DrawRoomTab(int room_id) { } } - // Quick controls + // Room info header ImGui::Text("Room %03X", room_id); ImGui::SameLine(); - if (ImGui::Button("Load Graphics")) { - (void)room_loader_.LoadAndRenderRoomGraphics(rooms_[room_id]); - } - ImGui::SameLine(); - if (ImGui::Button("Save")) { - auto status = rooms_[room_id].SaveObjects(); - if (!status.ok()) { - ImGui::TextColored(ImVec4(1, 0, 0, 1), "Save failed: %s", - status.message().data()); - } + if (rooms_[room_id].IsLoaded()) { + ImGui::TextColored(ImVec4(0.4f, 0.8f, 0.4f, 1.0f), ICON_MD_CHECK " Loaded"); + } else { + ImGui::TextColored(ImVec4(0.8f, 0.4f, 0.4f, 1.0f), ICON_MD_PENDING " Not Loaded"); } ImGui::Separator(); @@ -307,7 +332,15 @@ void DungeonEditorV2::DrawRoomsListCard() { MakeCardTitle("Rooms List").c_str(), ICON_MD_LIST, &show_room_selector_); + selector_card.SetDefaultSize(350, 600); + if (selector_card.Begin()) { + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + selector_card.End(); + return; + } + // Add text filter static char room_filter[256] = ""; ImGui::SetNextItemWidth(-1); @@ -318,31 +351,36 @@ void DungeonEditorV2::DrawRoomsListCard() { ImGui::Separator(); - // Scrollable room list with resource labels - if (ImGui::BeginChild("##RoomsList", ImVec2(0, 0), true)) { + // Scrollable room list - simple and reliable + if (ImGui::BeginChild("##RoomsList", ImVec2(0, 0), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { std::string filter_str = room_filter; std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower); for (int i = 0; i < 0x128; i++) { + // Get room name std::string room_name; if (i < static_cast(std::size(zelda3::kRoomNames))) { - room_name = absl::StrFormat("%03X - %s", i, zelda3::kRoomNames[i].data()); + room_name = std::string(zelda3::kRoomNames[i]); } else { - room_name = absl::StrFormat("%03X - Room %d", i, i); + room_name = absl::StrFormat("Room %03X", i); } // Apply filter if (!filter_str.empty()) { - std::string room_name_lower = room_name; - std::transform(room_name_lower.begin(), room_name_lower.end(), - room_name_lower.begin(), ::tolower); - if (room_name_lower.find(filter_str) == std::string::npos) { + std::string name_lower = room_name; + std::transform(name_lower.begin(), name_lower.end(), + name_lower.begin(), ::tolower); + if (name_lower.find(filter_str) == std::string::npos) { continue; } } + // Simple selectable with room ID and name + std::string label = absl::StrFormat("[%03X] %s", i, room_name.c_str()); bool is_selected = (current_room_id_ == i); - if (ImGui::Selectable(room_name.c_str(), is_selected)) { + + if (ImGui::Selectable(label.c_str(), is_selected)) { OnRoomSelected(i); } @@ -358,47 +396,96 @@ void DungeonEditorV2::DrawRoomsListCard() { void DungeonEditorV2::DrawEntrancesListCard() { gui::EditorCard entrances_card( - MakeCardTitle("Entrances List").c_str(), + MakeCardTitle("Entrances").c_str(), ICON_MD_DOOR_FRONT, &show_entrances_list_); + entrances_card.SetDefaultSize(400, 700); + if (entrances_card.Begin()) { - // Add text filter - static char entrance_filter[256] = ""; - ImGui::SetNextItemWidth(-1); - if (ImGui::InputTextWithHint("##EntranceFilter", ICON_MD_SEARCH " Filter entrances...", - entrance_filter, sizeof(entrance_filter))) { - // Filter updated + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + entrances_card.End(); + return; } + // Full entrance configuration UI (matching dungeon_room_selector layout) + auto& current_entrance = entrances_[current_entrance_id_]; + + gui::InputHexWord("Entrance ID", ¤t_entrance.entrance_id_); + gui::InputHexWord("Room ID", reinterpret_cast(¤t_entrance.room_)); + ImGui::SameLine(); + gui::InputHexByte("Dungeon ID", ¤t_entrance.dungeon_id_, 50.f, true); + + gui::InputHexByte("Blockset", ¤t_entrance.blockset_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("Music", ¤t_entrance.music_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("Floor", ¤t_entrance.floor_); + ImGui::Separator(); - // Scrollable entrance list with associated room names - if (ImGui::BeginChild("##EntrancesList", ImVec2(0, 0), true)) { - std::string filter_str = entrance_filter; - std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower); - - for (int i = 0; i < static_cast(entrances_.size()); i++) { - int room_id = entrances_[i].room_; + gui::InputHexWord("Player X ", ¤t_entrance.x_position_); + ImGui::SameLine(); + gui::InputHexWord("Player Y ", ¤t_entrance.y_position_); + + gui::InputHexWord("Camera X", ¤t_entrance.camera_trigger_x_); + ImGui::SameLine(); + gui::InputHexWord("Camera Y", ¤t_entrance.camera_trigger_y_); + + gui::InputHexWord("Scroll X ", ¤t_entrance.camera_x_); + ImGui::SameLine(); + gui::InputHexWord("Scroll Y ", ¤t_entrance.camera_y_); + + gui::InputHexWord("Exit", reinterpret_cast(¤t_entrance.exit_), 50.f, true); + + ImGui::Separator(); + ImGui::Text("Camera Boundaries"); + ImGui::Separator(); + ImGui::Text("\t\t\t\t\tNorth East South West"); + + gui::InputHexByte("Quadrant", ¤t_entrance.camera_boundary_qn_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##QE", ¤t_entrance.camera_boundary_qe_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##QS", ¤t_entrance.camera_boundary_qs_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##QW", ¤t_entrance.camera_boundary_qw_, 50.f, true); + + gui::InputHexByte("Full room", ¤t_entrance.camera_boundary_fn_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##FE", ¤t_entrance.camera_boundary_fe_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##FS", ¤t_entrance.camera_boundary_fs_, 50.f, true); + ImGui::SameLine(); + gui::InputHexByte("##FW", ¤t_entrance.camera_boundary_fw_, 50.f, true); + + ImGui::Separator(); + + // Entrance list - simple and reliable + if (ImGui::BeginChild("##EntrancesList", ImVec2(0, 0), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + for (int i = 0; i < 0x8C; i++) { + // The last seven are spawn points + std::string entrance_name; + if (i < 0x85) { + entrance_name = std::string(zelda3::kEntranceNames[i]); + } else { + entrance_name = absl::StrFormat("Spawn Point %d", i - 0x85); + } + // Get associated room name + int room_id = entrances_[i].room_; std::string room_name = "Unknown"; if (room_id >= 0 && room_id < static_cast(std::size(zelda3::kRoomNames))) { - room_name = zelda3::kRoomNames[room_id].data(); + room_name = std::string(zelda3::kRoomNames[room_id]); } - std::string entrance_label = absl::StrFormat("%02X - %s (Room %03X)", - i, room_name.c_str(), room_id); + std::string label = absl::StrFormat("[%02X] %s -> %s", + i, entrance_name.c_str(), room_name.c_str()); - // Apply filter - if (!filter_str.empty()) { - std::string entrance_label_lower = entrance_label; - std::transform(entrance_label_lower.begin(), entrance_label_lower.end(), - entrance_label_lower.begin(), ::tolower); - if (entrance_label_lower.find(filter_str) == std::string::npos) { - continue; - } - } - - if (ImGui::Selectable(entrance_label.c_str())) { + bool is_selected = (current_entrance_id_ == i); + if (ImGui::Selectable(label.c_str(), is_selected)) { + current_entrance_id_ = i; OnEntranceSelected(i); } } @@ -413,77 +500,219 @@ void DungeonEditorV2::DrawRoomMatrixCard() { MakeCardTitle("Room Matrix").c_str(), ICON_MD_GRID_VIEW, &show_room_matrix_); - matrix_card.SetDefaultSize(600, 600); + matrix_card.SetDefaultSize(520, 620); if (matrix_card.Begin()) { - // Draw 8x8 grid of rooms (first 64 rooms) - constexpr int kRoomsPerRow = 8; - constexpr int kRoomsPerCol = 8; - constexpr float kRoomCellSize = 64.0f; + // 16 wide x 19 tall = 304 cells (295 rooms + 9 empty) + constexpr int kRoomsPerRow = 16; + constexpr int kRoomsPerCol = 19; + constexpr int kTotalRooms = 0x128; // 296 rooms (0x00-0x127) + constexpr float kRoomCellSize = 28.0f; // Compact cells + constexpr float kCellSpacing = 2.0f; ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); + int room_index = 0; for (int row = 0; row < kRoomsPerCol; row++) { for (int col = 0; col < kRoomsPerRow; col++) { - int room_id = row * kRoomsPerRow + col; + int room_id = room_index; + bool is_valid_room = (room_id < kTotalRooms); - ImVec2 cell_min = ImVec2(canvas_pos.x + col * kRoomCellSize, - canvas_pos.y + row * kRoomCellSize); - ImVec2 cell_max = ImVec2(cell_min.x + kRoomCellSize, - cell_min.y + kRoomCellSize); + ImVec2 cell_min = ImVec2( + canvas_pos.x + col * (kRoomCellSize + kCellSpacing), + canvas_pos.y + row * (kRoomCellSize + kCellSpacing)); + ImVec2 cell_max = ImVec2( + cell_min.x + kRoomCellSize, + cell_min.y + kRoomCellSize); - // Check if room is active - bool is_active = false; - for (int i = 0; i < active_rooms_.Size; i++) { - if (active_rooms_[i] == room_id) { - is_active = true; - break; + if (is_valid_room) { + // ALWAYS use palette-based color (lazy load if needed) + ImU32 bg_color = IM_COL32(60, 60, 70, 255); // Fallback + + // Try to get palette color + uint8_t palette = 0; + if (room_id < static_cast(rooms_.size())) { + // Lazy load room if needed to get palette + if (!rooms_[room_id].IsLoaded()) { + auto status = room_loader_.LoadRoom(room_id, rooms_[room_id]); + if (status.ok()) { + palette = rooms_[room_id].palette; + } + } else { + palette = rooms_[room_id].palette; + } + + // Create a color variation based on palette ID + float r = 0.3f + (palette % 4) * 0.15f; + float g = 0.3f + ((palette / 4) % 4) * 0.15f; + float b = 0.4f + ((palette / 16) % 2) * 0.2f; + bg_color = IM_COL32( + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), 255); } - } - - // Draw cell background - ImU32 bg_color = is_active ? IM_COL32(100, 150, 255, 255) - : IM_COL32(50, 50, 50, 255); - draw_list->AddRectFilled(cell_min, cell_max, bg_color); - - // Draw cell border - draw_list->AddRect(cell_min, cell_max, IM_COL32(150, 150, 150, 255)); - - // Draw room ID - std::string room_label = absl::StrFormat("%02X", room_id); - ImVec2 text_size = ImGui::CalcTextSize(room_label.c_str()); - ImVec2 text_pos = ImVec2(cell_min.x + (kRoomCellSize - text_size.x) * 0.5f, - cell_min.y + (kRoomCellSize - text_size.y) * 0.5f); - draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), room_label.c_str()); - - // Handle clicks - ImGui::SetCursorScreenPos(cell_min); - ImGui::InvisibleButton(absl::StrFormat("##room%d", room_id).c_str(), - ImVec2(kRoomCellSize, kRoomCellSize)); - - if (ImGui::IsItemClicked()) { - OnRoomSelected(room_id); - } - - // Hover preview (TODO: implement room bitmap preview) - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - if (room_id < static_cast(std::size(zelda3::kRoomNames))) { - ImGui::Text("%s", zelda3::kRoomNames[room_id].data()); + + // Check if room is currently selected + bool is_current = (current_room_id_ == room_id); + + // Check if room is open in a card + bool is_open = false; + for (int i = 0; i < active_rooms_.Size; i++) { + if (active_rooms_[i] == room_id) { + is_open = true; + break; + } + } + + // Draw cell background with palette color + draw_list->AddRectFilled(cell_min, cell_max, bg_color); + + // Draw outline ONLY for current/open rooms + if (is_current) { + // Light green for current room + draw_list->AddRect(cell_min, cell_max, + IM_COL32(144, 238, 144, 255), 0.0f, 0, 2.5f); + } else if (is_open) { + // Green for open rooms + draw_list->AddRect(cell_min, cell_max, + IM_COL32(0, 200, 0, 255), 0.0f, 0, 2.0f); } else { - ImGui::Text("Room %03X", room_id); + // Subtle gray border for all rooms + draw_list->AddRect(cell_min, cell_max, + IM_COL32(80, 80, 80, 200), 0.0f, 0, 1.0f); } - ImGui::EndTooltip(); + + // Draw room ID (small text) + std::string room_label = absl::StrFormat("%02X", room_id); + ImVec2 text_size = ImGui::CalcTextSize(room_label.c_str()); + ImVec2 text_pos = ImVec2( + cell_min.x + (kRoomCellSize - text_size.x) * 0.5f, + cell_min.y + (kRoomCellSize - text_size.y) * 0.5f); + + // Use smaller font if available + draw_list->AddText(text_pos, IM_COL32(220, 220, 220, 255), + room_label.c_str()); + + // Handle clicks + ImGui::SetCursorScreenPos(cell_min); + ImGui::InvisibleButton( + absl::StrFormat("##room%d", room_id).c_str(), + ImVec2(kRoomCellSize, kRoomCellSize)); + + if (ImGui::IsItemClicked()) { + OnRoomSelected(room_id); + } + + // Hover tooltip with room name + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + if (room_id < static_cast(std::size(zelda3::kRoomNames))) { + ImGui::Text("%s", zelda3::kRoomNames[room_id].data()); + } else { + ImGui::Text("Room %03X", room_id); + } + ImGui::Text("Palette: %02X", + room_id < static_cast(rooms_.size()) ? + rooms_[room_id].palette : 0); + ImGui::EndTooltip(); + } + } else { + // Empty cell + draw_list->AddRectFilled(cell_min, cell_max, + IM_COL32(30, 30, 30, 255)); + draw_list->AddRect(cell_min, cell_max, + IM_COL32(50, 50, 50, 255)); } + + room_index++; } } // Advance cursor past the grid - ImGui::Dummy(ImVec2(kRoomsPerRow * kRoomCellSize, kRoomsPerCol * kRoomCellSize)); + ImGui::Dummy(ImVec2( + kRoomsPerRow * (kRoomCellSize + kCellSpacing), + kRoomsPerCol * (kRoomCellSize + kCellSpacing))); } matrix_card.End(); } +void DungeonEditorV2::DrawRoomGraphicsCard() { + gui::EditorCard graphics_card( + MakeCardTitle("Room Graphics").c_str(), + ICON_MD_IMAGE, &show_room_graphics_); + + graphics_card.SetDefaultSize(350, 500); + graphics_card.SetPosition(gui::EditorCard::Position::Right); + + if (graphics_card.Begin()) { + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + graphics_card.End(); + return; + } + + // Show graphics for current room + if (current_room_id_ >= 0 && current_room_id_ < static_cast(rooms_.size())) { + auto& room = rooms_[current_room_id_]; + + ImGui::Text("Room %03X Graphics", current_room_id_); + ImGui::Text("Blockset: %02X", room.blockset); + ImGui::Separator(); + + // Create a canvas for displaying room graphics + static gui::Canvas room_gfx_canvas("##RoomGfxCanvas", ImVec2(0x100 + 1, 0x10 * 0x40 + 1)); + + room_gfx_canvas.DrawBackground(); + room_gfx_canvas.DrawContextMenu(); + room_gfx_canvas.DrawTileSelector(32); + + auto blocks = room.blocks(); + + // Load graphics for this room if not already loaded + if (blocks.empty()) { + room.LoadRoomGraphics(room.blockset); + blocks = room.blocks(); + } + + int current_block = 0; + constexpr int max_blocks_per_row = 2; + constexpr int block_width = 128; + constexpr int block_height = 32; + + for (int block : blocks) { + if (current_block >= 16) break; // Show first 16 blocks + + // Ensure the graphics sheet is loaded + if (block < static_cast(gfx::Arena::Get().gfx_sheets().size())) { + auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; + + // Calculate grid position + int row = current_block / max_blocks_per_row; + int col = current_block % max_blocks_per_row; + + int x = room_gfx_canvas.zero_point().x + 2 + (col * block_width); + int y = room_gfx_canvas.zero_point().y + 2 + (row * block_height); + + // Draw if texture is valid + if (gfx_sheet.texture() != 0) { + room_gfx_canvas.draw_list()->AddImage( + (ImTextureID)(intptr_t)gfx_sheet.texture(), + ImVec2(x, y), + ImVec2(x + block_width, y + block_height)); + } + } + current_block++; + } + + room_gfx_canvas.DrawGrid(32.0f); + room_gfx_canvas.DrawOverlay(); + } else { + ImGui::TextDisabled("No room selected"); + } + } + graphics_card.End(); +} + } // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_editor_v2.h b/src/app/editor/dungeon/dungeon_editor_v2.h index 16cc2bcf..7c2cce0d 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.h +++ b/src/app/editor/dungeon/dungeon_editor_v2.h @@ -13,6 +13,7 @@ #include "dungeon_canvas_viewer.h" #include "dungeon_object_selector.h" #include "dungeon_room_loader.h" +#include "object_editor_card.h" #include "app/zelda3/dungeon/room.h" #include "app/zelda3/dungeon/room_entrance.h" #include "app/gui/editor_layout.h" @@ -92,6 +93,7 @@ class DungeonEditorV2 : public Editor { void DrawRoomMatrixCard(); void DrawRoomsListCard(); void DrawEntrancesListCard(); + void DrawRoomGraphicsCard(); // Room selection callback void OnRoomSelected(int room_id); @@ -102,6 +104,9 @@ class DungeonEditorV2 : public Editor { std::array rooms_; std::array entrances_; + // Current selection state + int current_entrance_id_ = 0; + // Active room tabs and card tracking for jump-to ImVector active_rooms_; std::unordered_map> room_cards_; @@ -111,7 +116,9 @@ class DungeonEditorV2 : public Editor { bool show_room_selector_ = true; bool show_room_matrix_ = false; bool show_entrances_list_ = false; - bool show_object_selector_ = true; + bool show_room_graphics_ = false; // Room graphics card + bool show_object_selector_ = true; // Legacy object selector + bool show_object_editor_ = true; // New unified object editor card bool show_palette_editor_ = true; // Palette management @@ -127,8 +134,12 @@ class DungeonEditorV2 : public Editor { DungeonObjectSelector object_selector_; gui::DungeonObjectEmulatorPreview object_emulator_preview_; gui::PaletteEditorWidget palette_editor_; + std::unique_ptr object_editor_card_; // Unified object editor bool is_loaded_ = false; + + // Docking class for room windows to dock together + ImGuiWindowClass room_window_class_; }; } // namespace editor diff --git a/src/app/editor/dungeon/object_editor_card.cc b/src/app/editor/dungeon/object_editor_card.cc new file mode 100644 index 00000000..ecd10069 --- /dev/null +++ b/src/app/editor/dungeon/object_editor_card.cc @@ -0,0 +1,294 @@ +#include "object_editor_card.h" + +#include "absl/strings/str_format.h" +#include "app/gui/icons.h" +#include "app/gui/ui_helpers.h" +#include "imgui/imgui.h" + +namespace yaze::editor { + +ObjectEditorCard::ObjectEditorCard(Rom* rom, DungeonCanvasViewer* canvas_viewer) + : rom_(rom), canvas_viewer_(canvas_viewer), object_selector_(rom) { + emulator_preview_.Initialize(rom); +} + +void ObjectEditorCard::Draw(bool* p_open) { + gui::EditorCard card("Object Editor", ICON_MD_CONSTRUCTION, p_open); + card.SetDefaultSize(450, 750); + card.SetPosition(gui::EditorCard::Position::Right); + + if (card.Begin(p_open)) { + // Interaction mode controls at top (moved from tab) + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Mode:"); + ImGui::SameLine(); + + if (ImGui::RadioButton("None", interaction_mode_ == InteractionMode::None)) { + interaction_mode_ = InteractionMode::None; + canvas_viewer_->SetObjectInteractionEnabled(false); + canvas_viewer_->ClearPreviewObject(); + } + ImGui::SameLine(); + + if (ImGui::RadioButton("Place", interaction_mode_ == InteractionMode::Place)) { + interaction_mode_ = InteractionMode::Place; + canvas_viewer_->SetObjectInteractionEnabled(true); + if (has_preview_object_) { + canvas_viewer_->SetPreviewObject(preview_object_); + } + } + ImGui::SameLine(); + + if (ImGui::RadioButton("Select", interaction_mode_ == InteractionMode::Select)) { + interaction_mode_ = InteractionMode::Select; + canvas_viewer_->SetObjectInteractionEnabled(true); + canvas_viewer_->ClearPreviewObject(); + } + ImGui::SameLine(); + + if (ImGui::RadioButton("Delete", interaction_mode_ == InteractionMode::Delete)) { + interaction_mode_ = InteractionMode::Delete; + canvas_viewer_->SetObjectInteractionEnabled(true); + canvas_viewer_->ClearPreviewObject(); + } + + // Current object info + DrawSelectedObjectInfo(); + + ImGui::Separator(); + + // Tabbed interface for Browser and Preview + if (ImGui::BeginTabBar("##ObjectEditorTabs", ImGuiTabBarFlags_None)) { + + // Tab 1: Object Browser + if (ImGui::BeginTabItem(ICON_MD_LIST " Browser")) { + DrawObjectSelector(); + ImGui::EndTabItem(); + } + + // Tab 2: Emulator Preview (enhanced) + if (ImGui::BeginTabItem(ICON_MD_MONITOR " Preview")) { + DrawEmulatorPreview(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + } + card.End(); +} + +void ObjectEditorCard::DrawObjectSelector() { + ImGui::Text(ICON_MD_INFO " Select an object to place on the canvas"); + ImGui::Separator(); + + // Text filter for objects + static char object_filter[256] = ""; + ImGui::SetNextItemWidth(-1); + if (ImGui::InputTextWithHint("##ObjectFilter", + ICON_MD_SEARCH " Filter objects...", + object_filter, sizeof(object_filter))) { + // Filter updated + } + + ImGui::Separator(); + + // Object list with categories + if (ImGui::BeginChild("##ObjectList", ImVec2(0, -100), true)) { + // Floor objects + if (ImGui::CollapsingHeader(ICON_MD_GRID_ON " Floor Objects", + ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < 0x100; i++) { + std::string filter_str = object_filter; + if (!filter_str.empty()) { + // Simple name-based filtering + std::string object_name = absl::StrFormat("Object %02X", i); + std::transform(filter_str.begin(), filter_str.end(), + filter_str.begin(), ::tolower); + std::transform(object_name.begin(), object_name.end(), + object_name.begin(), ::tolower); + if (object_name.find(filter_str) == std::string::npos) { + continue; + } + } + + // Create preview icon with small canvas + ImGui::BeginGroup(); + + // Small preview canvas (32x32 pixels) + DrawObjectPreviewIcon(i, ImVec2(32, 32)); + + ImGui::SameLine(); + + // Object label and selection + std::string object_label = absl::StrFormat("%02X - Floor Object", i); + + if (ImGui::Selectable(object_label.c_str(), + has_preview_object_ && preview_object_.id_ == i, + 0, ImVec2(0, 32))) { // Match preview height + preview_object_ = zelda3::RoomObject{ + static_cast(i), 0, 0, 0, 0}; + has_preview_object_ = true; + canvas_viewer_->SetPreviewObject(preview_object_); + canvas_viewer_->SetObjectInteractionEnabled(true); + interaction_mode_ = InteractionMode::Place; + } + + ImGui::EndGroup(); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Object ID: 0x%02X", i); + ImGui::Text("Type: Floor Object"); + ImGui::Text("Click to select for placement"); + ImGui::EndTooltip(); + } + } + } + + // Wall objects + if (ImGui::CollapsingHeader(ICON_MD_BORDER_ALL " Wall Objects")) { + for (int i = 0; i < 0x50; i++) { + std::string object_label = absl::StrFormat( + "%s %02X - Wall Object", ICON_MD_BORDER_VERTICAL, i); + + if (ImGui::Selectable(object_label.c_str())) { + // Wall objects have special handling + preview_object_ = zelda3::RoomObject{ + static_cast(i), 0, 0, 0, 1}; // layer=1 for walls + has_preview_object_ = true; + canvas_viewer_->SetPreviewObject(preview_object_); + canvas_viewer_->SetObjectInteractionEnabled(true); + interaction_mode_ = InteractionMode::Place; + } + } + } + + // Special objects + if (ImGui::CollapsingHeader(ICON_MD_STAR " Special Objects")) { + const char* special_objects[] = { + "Stairs Down", "Stairs Up", "Chest", "Door", "Pot", "Block", + "Switch", "Torch" + }; + + for (int i = 0; i < IM_ARRAYSIZE(special_objects); i++) { + std::string object_label = absl::StrFormat( + "%s %s", ICON_MD_STAR, special_objects[i]); + + if (ImGui::Selectable(object_label.c_str())) { + // Special object IDs start at 0xF8 + preview_object_ = zelda3::RoomObject{ + static_cast(0xF8 + i), 0, 0, 0, 2}; + has_preview_object_ = true; + canvas_viewer_->SetPreviewObject(preview_object_); + canvas_viewer_->SetObjectInteractionEnabled(true); + interaction_mode_ = InteractionMode::Place; + } + } + } + + ImGui::EndChild(); + } + + // Quick actions at bottom + if (ImGui::Button(ICON_MD_CLEAR " Clear Selection", ImVec2(-1, 0))) { + has_preview_object_ = false; + canvas_viewer_->ClearPreviewObject(); + canvas_viewer_->SetObjectInteractionEnabled(false); + interaction_mode_ = InteractionMode::None; + } +} + +void ObjectEditorCard::DrawEmulatorPreview() { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), + ICON_MD_INFO " Real-time object rendering preview"); + ImGui::Separator(); + + // Toggle emulator preview visibility + ImGui::Checkbox("Enable Preview", &show_emulator_preview_); + ImGui::SameLine(); + gui::HelpMarker("Uses SNES emulation to render objects accurately.\n" + "May impact performance."); + + if (show_emulator_preview_) { + ImGui::Separator(); + + // Embed the emulator preview with improved layout + ImGui::BeginChild("##EmulatorPreviewRegion", ImVec2(0, 0), true); + + emulator_preview_.Render(); + + ImGui::EndChild(); + } else { + ImGui::Separator(); + ImGui::TextDisabled(ICON_MD_PREVIEW " Preview disabled for performance"); + ImGui::TextWrapped("Enable to see accurate object rendering using " + "SNES emulation."); + } +} + +// DrawInteractionControls removed - controls moved to top of card + +void ObjectEditorCard::DrawObjectPreviewIcon(int object_id, const ImVec2& size) { + // Create a small preview box for the object + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); + ImVec2 box_min = cursor_pos; + ImVec2 box_max = ImVec2(cursor_pos.x + size.x, cursor_pos.y + size.y); + + // Draw background + draw_list->AddRectFilled(box_min, box_max, IM_COL32(40, 40, 45, 255)); + draw_list->AddRect(box_min, box_max, IM_COL32(100, 100, 100, 255)); + + // Draw a simple representation based on object ID + // For now, use colored squares and icons as placeholders + // Later this can be replaced with actual object bitmaps + + // Color based on object ID for visual variety + float hue = (object_id % 16) / 16.0f; + ImU32 obj_color = ImGui::ColorConvertFloat4ToU32( + ImVec4(0.5f + hue * 0.3f, 0.4f, 0.6f - hue * 0.2f, 1.0f)); + + // Draw inner colored square (16x16 in the center) + ImVec2 inner_min = ImVec2(cursor_pos.x + 8, cursor_pos.y + 8); + ImVec2 inner_max = ImVec2(cursor_pos.x + 24, cursor_pos.y + 24); + draw_list->AddRectFilled(inner_min, inner_max, obj_color); + draw_list->AddRect(inner_min, inner_max, IM_COL32(200, 200, 200, 255)); + + // Draw object ID text (very small) + std::string id_text = absl::StrFormat("%02X", object_id); + ImVec2 text_size = ImGui::CalcTextSize(id_text.c_str()); + ImVec2 text_pos = ImVec2( + cursor_pos.x + (size.x - text_size.x) * 0.5f, + cursor_pos.y + size.y - text_size.y - 2); + draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 255), id_text.c_str()); + + // Advance cursor + ImGui::Dummy(size); +} + +void ObjectEditorCard::DrawSelectedObjectInfo() { + ImGui::BeginGroup(); + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), + ICON_MD_INFO " Current Object:"); + + if (has_preview_object_) { + ImGui::SameLine(); + ImGui::Text("ID: 0x%02X", preview_object_.id_); + ImGui::SameLine(); + ImGui::Text("Layer: %s", + preview_object_.layer_ == zelda3::RoomObject::BG1 ? "BG1" : + preview_object_.layer_ == zelda3::RoomObject::BG2 ? "BG2" : "BG3"); + ImGui::SameLine(); + ImGui::Text("Mode: %s", + interaction_mode_ == InteractionMode::Place ? "Place" : + interaction_mode_ == InteractionMode::Select ? "Select" : + interaction_mode_ == InteractionMode::Delete ? "Delete" : "None"); + } else { + ImGui::SameLine(); + ImGui::TextDisabled("None selected"); + } + + ImGui::EndGroup(); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/object_editor_card.h b/src/app/editor/dungeon/object_editor_card.h new file mode 100644 index 00000000..ad6200d6 --- /dev/null +++ b/src/app/editor/dungeon/object_editor_card.h @@ -0,0 +1,83 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_OBJECT_EDITOR_CARD_H +#define YAZE_APP_EDITOR_DUNGEON_OBJECT_EDITOR_CARD_H + +#include +#include + +#include "app/editor/dungeon/dungeon_canvas_viewer.h" +#include "app/gui/canvas.h" +#include "app/editor/dungeon/dungeon_object_selector.h" +#include "app/gui/editor_layout.h" +#include "app/gui/widgets/dungeon_object_emulator_preview.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/room_object.h" + +namespace yaze { +namespace editor { + +/** + * @brief Unified card combining object selection, emulator preview, and canvas interaction + * + * This card replaces three separate components: + * - Object Selector (choosing which object to place) + * - Emulator Preview (seeing how objects look in-game) + * - Object Interaction Controls (placing, selecting, deleting objects) + * + * It provides a complete workflow for managing dungeon objects in one place. + */ +class ObjectEditorCard { + public: + ObjectEditorCard(Rom* rom, DungeonCanvasViewer* canvas_viewer); + + // Main update function + void Draw(bool* p_open); + + // Access to components + DungeonObjectSelector& object_selector() { return object_selector_; } + gui::DungeonObjectEmulatorPreview& emulator_preview() { return emulator_preview_; } + + // Update current room context + void SetCurrentRoom(int room_id) { current_room_id_ = room_id; } + + private: + void DrawObjectSelector(); + void DrawEmulatorPreview(); + void DrawInteractionControls(); + void DrawSelectedObjectInfo(); + void DrawObjectPreviewIcon(int object_id, const ImVec2& size); + + Rom* rom_; + DungeonCanvasViewer* canvas_viewer_; + int current_room_id_ = 0; + + // Components + DungeonObjectSelector object_selector_; + gui::DungeonObjectEmulatorPreview emulator_preview_; + + // Object preview canvases (one per object type) + std::unordered_map object_preview_canvases_; + + // UI state + int selected_tab_ = 0; + bool show_emulator_preview_ = true; + bool show_object_list_ = true; + bool show_interaction_controls_ = true; + + // Object interaction mode + enum class InteractionMode { + None, + Place, + Select, + Delete + }; + InteractionMode interaction_mode_ = InteractionMode::None; + + // Selected object for placement + zelda3::RoomObject preview_object_{0, 0, 0, 0, 0}; + bool has_preview_object_ = false; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_DUNGEON_OBJECT_EDITOR_CARD_H