feat: Integrate Unified Object Editor for Enhanced Dungeon Object Management

- Introduced ObjectEditorCard to consolidate object selection, emulator preview, and interaction controls into a single interface.
- Updated DungeonCanvasViewer to support object interaction context and handle mouse input for object selection and placement.
- Enhanced DungeonEditorV2 with new room graphics card for visualizing room graphics and improved layout for better user experience.
- Refactored existing components to accommodate the new unified editor, streamlining the workflow for managing dungeon objects.
This commit is contained in:
scawful
2025-10-07 13:43:33 -04:00
parent 5e4e6f5d45
commit c4384a6d08
6 changed files with 751 additions and 103 deletions

View File

@@ -4,10 +4,11 @@
#include <cstdio>
#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<ObjectEditorCard>(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<gui::EditorCard>(
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<int>(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", &current_entrance.entrance_id_);
gui::InputHexWord("Room ID", reinterpret_cast<uint16_t*>(&current_entrance.room_));
ImGui::SameLine();
gui::InputHexByte("Dungeon ID", &current_entrance.dungeon_id_, 50.f, true);
gui::InputHexByte("Blockset", &current_entrance.blockset_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("Music", &current_entrance.music_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("Floor", &current_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<int>(entrances_.size()); i++) {
int room_id = entrances_[i].room_;
gui::InputHexWord("Player X ", &current_entrance.x_position_);
ImGui::SameLine();
gui::InputHexWord("Player Y ", &current_entrance.y_position_);
gui::InputHexWord("Camera X", &current_entrance.camera_trigger_x_);
ImGui::SameLine();
gui::InputHexWord("Camera Y", &current_entrance.camera_trigger_y_);
gui::InputHexWord("Scroll X ", &current_entrance.camera_x_);
ImGui::SameLine();
gui::InputHexWord("Scroll Y ", &current_entrance.camera_y_);
gui::InputHexWord("Exit", reinterpret_cast<uint16_t*>(&current_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", &current_entrance.camera_boundary_qn_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##QE", &current_entrance.camera_boundary_qe_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##QS", &current_entrance.camera_boundary_qs_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##QW", &current_entrance.camera_boundary_qw_, 50.f, true);
gui::InputHexByte("Full room", &current_entrance.camera_boundary_fn_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##FE", &current_entrance.camera_boundary_fe_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##FS", &current_entrance.camera_boundary_fs_, 50.f, true);
ImGui::SameLine();
gui::InputHexByte("##FW", &current_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<int>(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<int>(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<int>(r * 255),
static_cast<int>(g * 255),
static_cast<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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