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:
@@ -103,6 +103,9 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
|
|||||||
if (rooms_ && rom_->is_loaded()) {
|
if (rooms_ && rom_->is_loaded()) {
|
||||||
auto& room = (*rooms_)[room_id];
|
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!)
|
// Check if THIS ROOM's buffers need rendering (not global arena!)
|
||||||
auto& bg1_bitmap = room.bg1_buffer().bitmap();
|
auto& bg1_bitmap = room.bg1_buffer().bitmap();
|
||||||
bool needs_render = !bg1_bitmap.is_active() || bg1_bitmap.width() == 0;
|
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
|
// Render sprites as simple 16x16 squares with labels
|
||||||
RenderSprites(room);
|
RenderSprites(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle object interaction if enabled
|
||||||
|
if (object_interaction_enabled_) {
|
||||||
|
object_interaction_.HandleCanvasMouseInput();
|
||||||
|
object_interaction_.CheckForObjectSelection();
|
||||||
|
object_interaction_.DrawSelectBox();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas_.DrawGrid();
|
canvas_.DrawGrid();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "app/zelda3/dungeon/object_renderer.h"
|
#include "app/zelda3/dungeon/object_renderer.h"
|
||||||
#include "app/zelda3/dungeon/room.h"
|
#include "app/zelda3/dungeon/room.h"
|
||||||
#include "app/gfx/snes_palette.h"
|
#include "app/gfx/snes_palette.h"
|
||||||
|
#include "dungeon_object_interaction.h"
|
||||||
#include "imgui/imgui.h"
|
#include "imgui/imgui.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
@@ -21,7 +22,8 @@ namespace editor {
|
|||||||
*/
|
*/
|
||||||
class DungeonCanvasViewer {
|
class DungeonCanvasViewer {
|
||||||
public:
|
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 DrawDungeonTabView();
|
||||||
void DrawDungeonCanvas(int room_id);
|
void DrawDungeonCanvas(int room_id);
|
||||||
@@ -46,6 +48,21 @@ class DungeonCanvasViewer {
|
|||||||
// Canvas access
|
// Canvas access
|
||||||
gui::Canvas& canvas() { return canvas_; }
|
gui::Canvas& canvas() { return canvas_; }
|
||||||
const gui::Canvas& canvas() const { 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:
|
private:
|
||||||
void RenderObjectInCanvas(const zelda3::RoomObject &object,
|
void RenderObjectInCanvas(const zelda3::RoomObject &object,
|
||||||
@@ -76,12 +93,16 @@ class DungeonCanvasViewer {
|
|||||||
Rom* rom_ = nullptr;
|
Rom* rom_ = nullptr;
|
||||||
gui::Canvas canvas_{"##DungeonCanvas", ImVec2(0x200, 0x200)};
|
gui::Canvas canvas_{"##DungeonCanvas", ImVec2(0x200, 0x200)};
|
||||||
zelda3::ObjectRenderer object_renderer_;
|
zelda3::ObjectRenderer object_renderer_;
|
||||||
|
DungeonObjectInteraction object_interaction_;
|
||||||
|
|
||||||
// Room data
|
// Room data
|
||||||
std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
|
std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
|
||||||
ImVector<int> active_rooms_;
|
ImVector<int> active_rooms_;
|
||||||
int current_active_room_tab_ = 0;
|
int current_active_room_tab_ = 0;
|
||||||
|
|
||||||
|
// Object interaction state
|
||||||
|
bool object_interaction_enabled_ = false;
|
||||||
|
|
||||||
// Palette data
|
// Palette data
|
||||||
uint64_t current_palette_group_id_ = 0;
|
uint64_t current_palette_group_id_ = 0;
|
||||||
uint64_t current_palette_id_ = 0;
|
uint64_t current_palette_id_ = 0;
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
#include "absl/strings/str_format.h"
|
#include "absl/strings/str_format.h"
|
||||||
|
#include "app/gfx/arena.h"
|
||||||
#include "app/gfx/snes_palette.h"
|
#include "app/gfx/snes_palette.h"
|
||||||
#include "app/zelda3/dungeon/room.h"
|
#include "app/zelda3/dungeon/room.h"
|
||||||
#include "app/gui/icons.h"
|
#include "app/gui/icons.h"
|
||||||
#include "app/gui/ui_helpers.h"
|
#include "app/gui/input.h"
|
||||||
#include "imgui/imgui.h"
|
#include "imgui/imgui.h"
|
||||||
|
|
||||||
namespace yaze::editor {
|
namespace yaze::editor {
|
||||||
@@ -17,6 +18,10 @@ namespace yaze::editor {
|
|||||||
void DungeonEditorV2::Initialize() {
|
void DungeonEditorV2::Initialize() {
|
||||||
// Don't initialize emulator preview yet - ROM might not be loaded
|
// Don't initialize emulator preview yet - ROM might not be loaded
|
||||||
// Will be initialized in Load() instead
|
// 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() {
|
absl::Status DungeonEditorV2::Load() {
|
||||||
@@ -55,6 +60,9 @@ absl::Status DungeonEditorV2::Load() {
|
|||||||
// Initialize palette editor with loaded ROM
|
// Initialize palette editor with loaded ROM
|
||||||
palette_editor_.Initialize(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
|
// Wire palette changes to trigger room re-renders
|
||||||
palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
|
palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
|
||||||
// Re-render all active rooms when palette changes
|
// Re-render all active rooms when palette changes
|
||||||
@@ -134,9 +142,17 @@ void DungeonEditorV2::DrawToolset() {
|
|||||||
// Toggled
|
// Toggled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toolbar.AddToggle(ICON_MD_IMAGE, &show_room_graphics_, "Toggle Room Graphics")) {
|
||||||
|
// Toggled
|
||||||
|
}
|
||||||
|
|
||||||
toolbar.AddSeparator();
|
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
|
// Toggled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +180,13 @@ void DungeonEditorV2::DrawLayout() {
|
|||||||
if (show_entrances_list_) {
|
if (show_entrances_list_) {
|
||||||
DrawEntrancesListCard();
|
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_) {
|
if (show_object_selector_) {
|
||||||
gui::EditorCard object_card(
|
gui::EditorCard object_card(
|
||||||
MakeCardTitle("Object Selector").c_str(),
|
MakeCardTitle("Object Selector").c_str(),
|
||||||
@@ -176,6 +197,11 @@ void DungeonEditorV2::DrawLayout() {
|
|||||||
object_card.End();
|
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)
|
// 5. Palette Editor Card (independent, dockable)
|
||||||
if (show_palette_editor_) {
|
if (show_palette_editor_) {
|
||||||
gui::EditorCard palette_card(
|
gui::EditorCard palette_card(
|
||||||
@@ -207,9 +233,14 @@ void DungeonEditorV2::DrawLayout() {
|
|||||||
if (room_cards_.find(room_id) == room_cards_.end()) {
|
if (room_cards_.find(room_id) == room_cards_.end()) {
|
||||||
room_cards_[room_id] = std::make_shared<gui::EditorCard>(
|
room_cards_[room_id] = std::make_shared<gui::EditorCard>(
|
||||||
card_name_str.c_str(), ICON_MD_GRID_ON, &open);
|
card_name_str.c_str(), ICON_MD_GRID_ON, &open);
|
||||||
|
room_cards_[room_id]->SetDefaultSize(700, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& room_card = room_cards_[room_id];
|
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)) {
|
if (room_card->Begin(&open)) {
|
||||||
DrawRoomTab(room_id);
|
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::Text("Room %03X", room_id);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Load Graphics")) {
|
if (rooms_[room_id].IsLoaded()) {
|
||||||
(void)room_loader_.LoadAndRenderRoomGraphics(rooms_[room_id]);
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 0.4f, 1.0f), ICON_MD_CHECK " Loaded");
|
||||||
}
|
} else {
|
||||||
ImGui::SameLine();
|
ImGui::TextColored(ImVec4(0.8f, 0.4f, 0.4f, 1.0f), ICON_MD_PENDING " Not Loaded");
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -307,7 +332,15 @@ void DungeonEditorV2::DrawRoomsListCard() {
|
|||||||
MakeCardTitle("Rooms List").c_str(),
|
MakeCardTitle("Rooms List").c_str(),
|
||||||
ICON_MD_LIST, &show_room_selector_);
|
ICON_MD_LIST, &show_room_selector_);
|
||||||
|
|
||||||
|
selector_card.SetDefaultSize(350, 600);
|
||||||
|
|
||||||
if (selector_card.Begin()) {
|
if (selector_card.Begin()) {
|
||||||
|
if (!rom_ || !rom_->is_loaded()) {
|
||||||
|
ImGui::Text("ROM not loaded");
|
||||||
|
selector_card.End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add text filter
|
// Add text filter
|
||||||
static char room_filter[256] = "";
|
static char room_filter[256] = "";
|
||||||
ImGui::SetNextItemWidth(-1);
|
ImGui::SetNextItemWidth(-1);
|
||||||
@@ -318,31 +351,36 @@ void DungeonEditorV2::DrawRoomsListCard() {
|
|||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Scrollable room list with resource labels
|
// Scrollable room list - simple and reliable
|
||||||
if (ImGui::BeginChild("##RoomsList", ImVec2(0, 0), true)) {
|
if (ImGui::BeginChild("##RoomsList", ImVec2(0, 0), true,
|
||||||
|
ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
|
||||||
std::string filter_str = room_filter;
|
std::string filter_str = room_filter;
|
||||||
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
|
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
|
||||||
|
|
||||||
for (int i = 0; i < 0x128; i++) {
|
for (int i = 0; i < 0x128; i++) {
|
||||||
|
// Get room name
|
||||||
std::string room_name;
|
std::string room_name;
|
||||||
if (i < static_cast<int>(std::size(zelda3::kRoomNames))) {
|
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 {
|
} else {
|
||||||
room_name = absl::StrFormat("%03X - Room %d", i, i);
|
room_name = absl::StrFormat("Room %03X", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply filter
|
// Apply filter
|
||||||
if (!filter_str.empty()) {
|
if (!filter_str.empty()) {
|
||||||
std::string room_name_lower = room_name;
|
std::string name_lower = room_name;
|
||||||
std::transform(room_name_lower.begin(), room_name_lower.end(),
|
std::transform(name_lower.begin(), name_lower.end(),
|
||||||
room_name_lower.begin(), ::tolower);
|
name_lower.begin(), ::tolower);
|
||||||
if (room_name_lower.find(filter_str) == std::string::npos) {
|
if (name_lower.find(filter_str) == std::string::npos) {
|
||||||
continue;
|
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);
|
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);
|
OnRoomSelected(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,47 +396,96 @@ void DungeonEditorV2::DrawRoomsListCard() {
|
|||||||
|
|
||||||
void DungeonEditorV2::DrawEntrancesListCard() {
|
void DungeonEditorV2::DrawEntrancesListCard() {
|
||||||
gui::EditorCard entrances_card(
|
gui::EditorCard entrances_card(
|
||||||
MakeCardTitle("Entrances List").c_str(),
|
MakeCardTitle("Entrances").c_str(),
|
||||||
ICON_MD_DOOR_FRONT, &show_entrances_list_);
|
ICON_MD_DOOR_FRONT, &show_entrances_list_);
|
||||||
|
|
||||||
|
entrances_card.SetDefaultSize(400, 700);
|
||||||
|
|
||||||
if (entrances_card.Begin()) {
|
if (entrances_card.Begin()) {
|
||||||
// Add text filter
|
if (!rom_ || !rom_->is_loaded()) {
|
||||||
static char entrance_filter[256] = "";
|
ImGui::Text("ROM not loaded");
|
||||||
ImGui::SetNextItemWidth(-1);
|
entrances_card.End();
|
||||||
if (ImGui::InputTextWithHint("##EntranceFilter", ICON_MD_SEARCH " Filter entrances...",
|
return;
|
||||||
entrance_filter, sizeof(entrance_filter))) {
|
|
||||||
// Filter updated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<uint16_t*>(¤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();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Scrollable entrance list with associated room names
|
gui::InputHexWord("Player X ", ¤t_entrance.x_position_);
|
||||||
if (ImGui::BeginChild("##EntrancesList", ImVec2(0, 0), true)) {
|
ImGui::SameLine();
|
||||||
std::string filter_str = entrance_filter;
|
gui::InputHexWord("Player Y ", ¤t_entrance.y_position_);
|
||||||
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
|
|
||||||
|
gui::InputHexWord("Camera X", ¤t_entrance.camera_trigger_x_);
|
||||||
for (int i = 0; i < static_cast<int>(entrances_.size()); i++) {
|
ImGui::SameLine();
|
||||||
int room_id = entrances_[i].room_;
|
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<uint16_t*>(¤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";
|
std::string room_name = "Unknown";
|
||||||
if (room_id >= 0 && room_id < static_cast<int>(std::size(zelda3::kRoomNames))) {
|
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)",
|
std::string label = absl::StrFormat("[%02X] %s -> %s",
|
||||||
i, room_name.c_str(), room_id);
|
i, entrance_name.c_str(), room_name.c_str());
|
||||||
|
|
||||||
// Apply filter
|
bool is_selected = (current_entrance_id_ == i);
|
||||||
if (!filter_str.empty()) {
|
if (ImGui::Selectable(label.c_str(), is_selected)) {
|
||||||
std::string entrance_label_lower = entrance_label;
|
current_entrance_id_ = i;
|
||||||
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())) {
|
|
||||||
OnEntranceSelected(i);
|
OnEntranceSelected(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,77 +500,219 @@ void DungeonEditorV2::DrawRoomMatrixCard() {
|
|||||||
MakeCardTitle("Room Matrix").c_str(),
|
MakeCardTitle("Room Matrix").c_str(),
|
||||||
ICON_MD_GRID_VIEW, &show_room_matrix_);
|
ICON_MD_GRID_VIEW, &show_room_matrix_);
|
||||||
|
|
||||||
matrix_card.SetDefaultSize(600, 600);
|
matrix_card.SetDefaultSize(520, 620);
|
||||||
|
|
||||||
if (matrix_card.Begin()) {
|
if (matrix_card.Begin()) {
|
||||||
// Draw 8x8 grid of rooms (first 64 rooms)
|
// 16 wide x 19 tall = 304 cells (295 rooms + 9 empty)
|
||||||
constexpr int kRoomsPerRow = 8;
|
constexpr int kRoomsPerRow = 16;
|
||||||
constexpr int kRoomsPerCol = 8;
|
constexpr int kRoomsPerCol = 19;
|
||||||
constexpr float kRoomCellSize = 64.0f;
|
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();
|
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||||
ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
|
ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
|
||||||
|
|
||||||
|
int room_index = 0;
|
||||||
for (int row = 0; row < kRoomsPerCol; row++) {
|
for (int row = 0; row < kRoomsPerCol; row++) {
|
||||||
for (int col = 0; col < kRoomsPerRow; col++) {
|
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,
|
ImVec2 cell_min = ImVec2(
|
||||||
canvas_pos.y + row * kRoomCellSize);
|
canvas_pos.x + col * (kRoomCellSize + kCellSpacing),
|
||||||
ImVec2 cell_max = ImVec2(cell_min.x + kRoomCellSize,
|
canvas_pos.y + row * (kRoomCellSize + kCellSpacing));
|
||||||
cell_min.y + kRoomCellSize);
|
ImVec2 cell_max = ImVec2(
|
||||||
|
cell_min.x + kRoomCellSize,
|
||||||
|
cell_min.y + kRoomCellSize);
|
||||||
|
|
||||||
// Check if room is active
|
if (is_valid_room) {
|
||||||
bool is_active = false;
|
// ALWAYS use palette-based color (lazy load if needed)
|
||||||
for (int i = 0; i < active_rooms_.Size; i++) {
|
ImU32 bg_color = IM_COL32(60, 60, 70, 255); // Fallback
|
||||||
if (active_rooms_[i] == room_id) {
|
|
||||||
is_active = true;
|
// Try to get palette color
|
||||||
break;
|
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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Check if room is currently selected
|
||||||
// Draw cell background
|
bool is_current = (current_room_id_ == room_id);
|
||||||
ImU32 bg_color = is_active ? IM_COL32(100, 150, 255, 255)
|
|
||||||
: IM_COL32(50, 50, 50, 255);
|
// Check if room is open in a card
|
||||||
draw_list->AddRectFilled(cell_min, cell_max, bg_color);
|
bool is_open = false;
|
||||||
|
for (int i = 0; i < active_rooms_.Size; i++) {
|
||||||
// Draw cell border
|
if (active_rooms_[i] == room_id) {
|
||||||
draw_list->AddRect(cell_min, cell_max, IM_COL32(150, 150, 150, 255));
|
is_open = true;
|
||||||
|
break;
|
||||||
// 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,
|
// Draw cell background with palette color
|
||||||
cell_min.y + (kRoomCellSize - text_size.y) * 0.5f);
|
draw_list->AddRectFilled(cell_min, cell_max, bg_color);
|
||||||
draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), room_label.c_str());
|
|
||||||
|
// Draw outline ONLY for current/open rooms
|
||||||
// Handle clicks
|
if (is_current) {
|
||||||
ImGui::SetCursorScreenPos(cell_min);
|
// Light green for current room
|
||||||
ImGui::InvisibleButton(absl::StrFormat("##room%d", room_id).c_str(),
|
draw_list->AddRect(cell_min, cell_max,
|
||||||
ImVec2(kRoomCellSize, kRoomCellSize));
|
IM_COL32(144, 238, 144, 255), 0.0f, 0, 2.5f);
|
||||||
|
} else if (is_open) {
|
||||||
if (ImGui::IsItemClicked()) {
|
// Green for open rooms
|
||||||
OnRoomSelected(room_id);
|
draw_list->AddRect(cell_min, cell_max,
|
||||||
}
|
IM_COL32(0, 200, 0, 255), 0.0f, 0, 2.0f);
|
||||||
|
|
||||||
// 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());
|
|
||||||
} else {
|
} 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
|
// Advance cursor past the grid
|
||||||
ImGui::Dummy(ImVec2(kRoomsPerRow * kRoomCellSize, kRoomsPerCol * kRoomCellSize));
|
ImGui::Dummy(ImVec2(
|
||||||
|
kRoomsPerRow * (kRoomCellSize + kCellSpacing),
|
||||||
|
kRoomsPerCol * (kRoomCellSize + kCellSpacing)));
|
||||||
}
|
}
|
||||||
matrix_card.End();
|
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
|
} // namespace yaze::editor
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "dungeon_canvas_viewer.h"
|
#include "dungeon_canvas_viewer.h"
|
||||||
#include "dungeon_object_selector.h"
|
#include "dungeon_object_selector.h"
|
||||||
#include "dungeon_room_loader.h"
|
#include "dungeon_room_loader.h"
|
||||||
|
#include "object_editor_card.h"
|
||||||
#include "app/zelda3/dungeon/room.h"
|
#include "app/zelda3/dungeon/room.h"
|
||||||
#include "app/zelda3/dungeon/room_entrance.h"
|
#include "app/zelda3/dungeon/room_entrance.h"
|
||||||
#include "app/gui/editor_layout.h"
|
#include "app/gui/editor_layout.h"
|
||||||
@@ -92,6 +93,7 @@ class DungeonEditorV2 : public Editor {
|
|||||||
void DrawRoomMatrixCard();
|
void DrawRoomMatrixCard();
|
||||||
void DrawRoomsListCard();
|
void DrawRoomsListCard();
|
||||||
void DrawEntrancesListCard();
|
void DrawEntrancesListCard();
|
||||||
|
void DrawRoomGraphicsCard();
|
||||||
|
|
||||||
// Room selection callback
|
// Room selection callback
|
||||||
void OnRoomSelected(int room_id);
|
void OnRoomSelected(int room_id);
|
||||||
@@ -102,6 +104,9 @@ class DungeonEditorV2 : public Editor {
|
|||||||
std::array<zelda3::Room, 0x128> rooms_;
|
std::array<zelda3::Room, 0x128> rooms_;
|
||||||
std::array<zelda3::RoomEntrance, 0x8C> entrances_;
|
std::array<zelda3::RoomEntrance, 0x8C> entrances_;
|
||||||
|
|
||||||
|
// Current selection state
|
||||||
|
int current_entrance_id_ = 0;
|
||||||
|
|
||||||
// Active room tabs and card tracking for jump-to
|
// Active room tabs and card tracking for jump-to
|
||||||
ImVector<int> active_rooms_;
|
ImVector<int> active_rooms_;
|
||||||
std::unordered_map<int, std::shared_ptr<gui::EditorCard>> room_cards_;
|
std::unordered_map<int, std::shared_ptr<gui::EditorCard>> room_cards_;
|
||||||
@@ -111,7 +116,9 @@ class DungeonEditorV2 : public Editor {
|
|||||||
bool show_room_selector_ = true;
|
bool show_room_selector_ = true;
|
||||||
bool show_room_matrix_ = false;
|
bool show_room_matrix_ = false;
|
||||||
bool show_entrances_list_ = 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;
|
bool show_palette_editor_ = true;
|
||||||
|
|
||||||
// Palette management
|
// Palette management
|
||||||
@@ -127,8 +134,12 @@ class DungeonEditorV2 : public Editor {
|
|||||||
DungeonObjectSelector object_selector_;
|
DungeonObjectSelector object_selector_;
|
||||||
gui::DungeonObjectEmulatorPreview object_emulator_preview_;
|
gui::DungeonObjectEmulatorPreview object_emulator_preview_;
|
||||||
gui::PaletteEditorWidget palette_editor_;
|
gui::PaletteEditorWidget palette_editor_;
|
||||||
|
std::unique_ptr<ObjectEditorCard> object_editor_card_; // Unified object editor
|
||||||
|
|
||||||
bool is_loaded_ = false;
|
bool is_loaded_ = false;
|
||||||
|
|
||||||
|
// Docking class for room windows to dock together
|
||||||
|
ImGuiWindowClass room_window_class_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace editor
|
} // namespace editor
|
||||||
|
|||||||
294
src/app/editor/dungeon/object_editor_card.cc
Normal file
294
src/app/editor/dungeon/object_editor_card.cc
Normal file
@@ -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<int16_t>(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<int16_t>(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<int16_t>(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
|
||||||
83
src/app/editor/dungeon/object_editor_card.h
Normal file
83
src/app/editor/dungeon/object_editor_card.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#ifndef YAZE_APP_EDITOR_DUNGEON_OBJECT_EDITOR_CARD_H
|
||||||
|
#define YAZE_APP_EDITOR_DUNGEON_OBJECT_EDITOR_CARD_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#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<int, gui::Canvas> 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
|
||||||
Reference in New Issue
Block a user