1867 lines
72 KiB
C++
1867 lines
72 KiB
C++
#include <algorithm>
|
||
#include <cfloat>
|
||
#include <cstddef>
|
||
#include <cstdint>
|
||
#include <cstdio>
|
||
#include <optional>
|
||
#include <string>
|
||
#include <utility>
|
||
|
||
#include "absl/strings/str_format.h"
|
||
#include "app/editor/agent/agent_ui_theme.h"
|
||
#include "app/gfx/resource/arena.h"
|
||
#include "app/gfx/types/snes_palette.h"
|
||
#include "app/gui/core/input.h"
|
||
#include "dungeon_canvas_viewer.h"
|
||
#include "dungeon_coordinates.h"
|
||
#include "app/gui/canvas/canvas_menu.h"
|
||
#include "app/gui/core/icons.h"
|
||
#include "absl/status/status.h"
|
||
#include "editor/dungeon/object_selection.h"
|
||
#include "imgui/imgui.h"
|
||
#include "rom/rom.h"
|
||
#include "util/log.h"
|
||
#include "util/macro.h"
|
||
#include "zelda3/dungeon/object_dimensions.h"
|
||
#include "zelda3/dungeon/object_drawer.h"
|
||
#include "zelda3/dungeon/room.h"
|
||
#include "zelda3/dungeon/room_layer_manager.h"
|
||
#include "zelda3/dungeon/room_object.h"
|
||
#include "zelda3/resource_labels.h"
|
||
#include "zelda3/sprite/sprite.h"
|
||
|
||
namespace yaze::editor {
|
||
|
||
namespace {
|
||
|
||
constexpr int kRoomMatrixCols = 16;
|
||
constexpr int kRoomMatrixRows = 19;
|
||
constexpr int kRoomPropertyColumns = 2;
|
||
|
||
} // namespace
|
||
|
||
// Use shared GetObjectName() from zelda3/dungeon/room_object.h
|
||
using zelda3::GetObjectName;
|
||
using zelda3::GetObjectSubtype;
|
||
|
||
void DungeonCanvasViewer::Draw(int room_id) {
|
||
DrawDungeonCanvas(room_id);
|
||
}
|
||
|
||
void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
|
||
// Validate room_id and ROM
|
||
if (room_id < 0 || room_id >= 0x128) {
|
||
ImGui::Text("Invalid room ID: %d", room_id);
|
||
return;
|
||
}
|
||
|
||
if (!rom_ || !rom_->is_loaded()) {
|
||
ImGui::Text("ROM not loaded");
|
||
return;
|
||
}
|
||
|
||
// Handle pending scroll request
|
||
if (pending_scroll_target_.has_value()) {
|
||
auto [target_x, target_y] = pending_scroll_target_.value();
|
||
|
||
// Convert tile coordinates to pixels
|
||
float scale = canvas_.global_scale();
|
||
if (scale <= 0.0f)
|
||
scale = 1.0f;
|
||
|
||
float pixel_x = target_x * 8 * scale;
|
||
float pixel_y = target_y * 8 * scale;
|
||
|
||
// Center in view
|
||
ImVec2 view_size = ImGui::GetWindowSize();
|
||
float scroll_x = pixel_x - (view_size.x * 0.5f);
|
||
float scroll_y = pixel_y - (view_size.y * 0.5f);
|
||
|
||
// Account for canvas position offset if possible, but roughly centering is
|
||
// usually enough Ideally we'd add the cursor position y-offset to scroll_y
|
||
// to account for the UI above canvas but GetCursorPosY() might not be
|
||
// accurate before content is laid out. For X, canvas usually starts at
|
||
// left, so it's fine.
|
||
|
||
ImGui::SetScrollX(scroll_x);
|
||
ImGui::SetScrollY(scroll_y);
|
||
|
||
pending_scroll_target_.reset();
|
||
}
|
||
|
||
ImGui::BeginGroup();
|
||
|
||
// CRITICAL: Canvas coordinate system for dungeons
|
||
// The canvas system uses a two-stage scaling model:
|
||
// 1. Canvas size: UNSCALED content dimensions (512x512 for dungeon rooms)
|
||
// 2. Viewport size: canvas_size * global_scale (handles zoom)
|
||
// 3. Grid lines: grid_step * global_scale (auto-scales with zoom)
|
||
// 4. Bitmaps: drawn with scale = global_scale (matches viewport)
|
||
constexpr int kRoomPixelWidth = 512; // 64 tiles * 8 pixels (UNSCALED)
|
||
constexpr int kRoomPixelHeight = 512;
|
||
constexpr int kDungeonTileSize = 8; // Dungeon tiles are 8x8 pixels
|
||
|
||
// Configure canvas frame options for the new BeginCanvas/EndCanvas pattern
|
||
gui::CanvasFrameOptions frame_opts;
|
||
frame_opts.canvas_size = ImVec2(kRoomPixelWidth, kRoomPixelHeight);
|
||
frame_opts.draw_grid = show_grid_;
|
||
frame_opts.grid_step = static_cast<float>(custom_grid_size_);
|
||
frame_opts.draw_context_menu = true;
|
||
frame_opts.draw_overlay = true;
|
||
frame_opts.render_popups = true;
|
||
|
||
// Legacy configuration for context menu and interaction systems
|
||
canvas_.SetShowBuiltinContextMenu(false); // Hide default canvas debug items
|
||
|
||
// DEBUG: Log canvas configuration
|
||
static int debug_frame_count = 0;
|
||
if (debug_frame_count++ % 60 == 0) { // Log once per second (assuming 60fps)
|
||
LOG_DEBUG("[DungeonCanvas]",
|
||
"Canvas config: size=(%.0f,%.0f) scale=%.2f grid=%.0f",
|
||
canvas_.width(), canvas_.height(), canvas_.global_scale(),
|
||
canvas_.custom_step());
|
||
LOG_DEBUG(
|
||
"[DungeonCanvas]", "Canvas viewport: p0=(%.0f,%.0f) p1=(%.0f,%.0f)",
|
||
canvas_.zero_point().x, canvas_.zero_point().y,
|
||
canvas_.zero_point().x + canvas_.width() * canvas_.global_scale(),
|
||
canvas_.zero_point().y + canvas_.height() * canvas_.global_scale());
|
||
}
|
||
|
||
if (rooms_) {
|
||
auto& room = (*rooms_)[room_id];
|
||
|
||
// Check if critical properties changed and trigger reload
|
||
if (prev_blockset_ != room.blockset || prev_palette_ != room.palette ||
|
||
prev_layout_ != room.layout || prev_spriteset_ != room.spriteset) {
|
||
// Only reload if ROM is properly loaded
|
||
if (room.rom() && room.rom()->is_loaded()) {
|
||
// Force reload of room graphics
|
||
// Room buffers are now self-contained - no need for separate palette
|
||
// operations
|
||
room.LoadRoomGraphics(room.blockset);
|
||
room.RenderRoomGraphics(); // Applies palettes internally
|
||
}
|
||
|
||
prev_blockset_ = room.blockset;
|
||
prev_palette_ = room.palette;
|
||
prev_layout_ = room.layout;
|
||
prev_spriteset_ = room.spriteset;
|
||
}
|
||
ImGui::Separator();
|
||
|
||
auto draw_navigation = [&]() {
|
||
// Use swap callback (swaps room in current panel) if available,
|
||
// otherwise fall back to navigation callback (opens new panel)
|
||
if (!room_swap_callback_ && !room_navigation_callback_)
|
||
return;
|
||
|
||
const int col = room_id % kRoomMatrixCols;
|
||
const int row = room_id / kRoomMatrixCols;
|
||
|
||
auto room_if_valid = [](int candidate) -> std::optional<int> {
|
||
if (candidate < 0 || candidate >= zelda3::NumberOfRooms)
|
||
return std::nullopt;
|
||
return candidate;
|
||
};
|
||
|
||
const auto north =
|
||
room_if_valid(row > 0 ? room_id - kRoomMatrixCols : -1);
|
||
const auto south = room_if_valid(
|
||
row < kRoomMatrixRows - 1 ? room_id + kRoomMatrixCols : -1);
|
||
const auto west = room_if_valid(col > 0 ? room_id - 1 : -1);
|
||
const auto east =
|
||
room_if_valid(col < kRoomMatrixCols - 1 ? room_id + 1 : -1);
|
||
|
||
// Generate tooltip with target room info
|
||
auto make_tooltip = [](const std::optional<int>& target,
|
||
const char* direction) -> std::string {
|
||
if (!target.has_value())
|
||
return "";
|
||
auto label = zelda3::GetRoomLabel(*target);
|
||
return absl::StrFormat("%s: [%03X] %s", direction, *target, label);
|
||
};
|
||
|
||
auto nav_button = [&](const char* id, ImGuiDir dir,
|
||
const std::optional<int>& target,
|
||
const std::string& tooltip) {
|
||
const bool enabled = target.has_value();
|
||
if (!enabled)
|
||
ImGui::BeginDisabled();
|
||
if (ImGui::ArrowButton(id, dir) && enabled) {
|
||
// Prefer swap callback (swaps room in current panel)
|
||
if (room_swap_callback_) {
|
||
room_swap_callback_(room_id, *target);
|
||
} else if (room_navigation_callback_) {
|
||
room_navigation_callback_(*target);
|
||
}
|
||
}
|
||
if (!enabled)
|
||
ImGui::EndDisabled();
|
||
if (enabled && ImGui::IsItemHovered() && !tooltip.empty())
|
||
ImGui::SetTooltip("%s", tooltip.c_str());
|
||
};
|
||
|
||
// Compass-style cross layout:
|
||
// [N]
|
||
// [W] [E]
|
||
// [S]
|
||
float button_width = ImGui::GetFrameHeight();
|
||
float spacing = ImGui::GetStyle().ItemSpacing.x;
|
||
|
||
ImGui::BeginGroup();
|
||
// Row 1: North button centered
|
||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + button_width + spacing);
|
||
nav_button("RoomNavNorth", ImGuiDir_Up, north, make_tooltip(north, "North"));
|
||
|
||
// Row 2: West and East buttons
|
||
nav_button("RoomNavWest", ImGuiDir_Left, west, make_tooltip(west, "West"));
|
||
ImGui::SameLine();
|
||
ImGui::Dummy(ImVec2(button_width, 0)); // Spacer for center
|
||
ImGui::SameLine();
|
||
nav_button("RoomNavEast", ImGuiDir_Right, east, make_tooltip(east, "East"));
|
||
|
||
// Row 3: South button centered
|
||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + button_width + spacing);
|
||
nav_button("RoomNavSouth", ImGuiDir_Down, south, make_tooltip(south, "South"));
|
||
ImGui::EndGroup();
|
||
ImGui::SameLine();
|
||
};
|
||
|
||
auto& layer_mgr = GetRoomLayerManager(room_id);
|
||
// TODO(zelda3-hacking-expert): The SNES path allows BG merge flags and
|
||
// layer types to coexist (four object streams with BothBG routines); make
|
||
// sure UI toggles here don’t enforce mutual exclusivity. See
|
||
// docs/internal/agents/dungeon-object-rendering-spec.md for the expected
|
||
// layering/merge semantics from bank_01.asm.
|
||
layer_mgr.ApplyLayerMerging(room.layer_merging());
|
||
|
||
uint8_t blockset_val = room.blockset;
|
||
uint8_t spriteset_val = room.spriteset;
|
||
uint8_t palette_val = room.palette;
|
||
uint8_t floor1_val = room.floor1();
|
||
uint8_t floor2_val = room.floor2();
|
||
int effect_val = static_cast<int>(room.effect());
|
||
int tag1_val = static_cast<int>(room.tag1());
|
||
int tag2_val = static_cast<int>(room.tag2());
|
||
uint8_t layout_val = room.layout;
|
||
|
||
// Effect names matching RoomEffect array in room.cc (8 entries, 0-7)
|
||
const char* effect_names[] = {
|
||
"Nothing", // 0
|
||
"Nothing (1)", // 1 - unused but exists in ROM
|
||
"Moving Floor", // 2
|
||
"Moving Water", // 3
|
||
"Trinexx Shell", // 4
|
||
"Red Flashes", // 5
|
||
"Light Torch to See", // 6
|
||
"Ganon's Darkness" // 7
|
||
};
|
||
|
||
// Tag names matching RoomTag array in room.cc
|
||
const char* tag_names[] = {
|
||
"Nothing", // 0
|
||
"NW Kill Enemy to Open", // 1
|
||
"NE Kill Enemy to Open", // 2
|
||
"SW Kill Enemy to Open", // 3
|
||
"SE Kill Enemy to Open", // 4
|
||
"W Kill Enemy to Open", // 5
|
||
"E Kill Enemy to Open", // 6
|
||
"N Kill Enemy to Open", // 7
|
||
"S Kill Enemy to Open", // 8
|
||
"Clear Quadrant to Open", // 9
|
||
"Clear Full Tile to Open", // 10
|
||
"NW Push Block to Open", // 11
|
||
"NE Push Block to Open", // 12
|
||
"SW Push Block to Open", // 13
|
||
"SE Push Block to Open", // 14
|
||
"W Push Block to Open", // 15
|
||
"E Push Block to Open", // 16
|
||
"N Push Block to Open", // 17
|
||
"S Push Block to Open", // 18
|
||
"Push Block to Open", // 19
|
||
"Pull Lever to Open", // 20
|
||
"Collect Prize to Open", // 21
|
||
"Hold Switch Open Door", // 22
|
||
"Toggle Switch to Open", // 23
|
||
"Turn off Water", // 24
|
||
"Turn on Water", // 25
|
||
"Water Gate", // 26
|
||
"Water Twin", // 27
|
||
"Moving Wall Right", // 28
|
||
"Moving Wall Left", // 29
|
||
"Crash (30)", // 30
|
||
"Crash (31)", // 31
|
||
"Push Switch Exploding Wall", // 32
|
||
"Holes 0", // 33
|
||
"Open Chest (Holes 0)", // 34
|
||
"Holes 1", // 35
|
||
"Holes 2", // 36
|
||
"Defeat Boss for Prize", // 37
|
||
"SE Kill Enemy Push Block", // 38
|
||
"Trigger Switch Chest", // 39
|
||
"Pull Lever Exploding Wall", // 40
|
||
"NW Kill Enemy for Chest", // 41
|
||
"NE Kill Enemy for Chest", // 42
|
||
"SW Kill Enemy for Chest", // 43
|
||
"SE Kill Enemy for Chest", // 44
|
||
"W Kill Enemy for Chest", // 45
|
||
"E Kill Enemy for Chest", // 46
|
||
"N Kill Enemy for Chest", // 47
|
||
"S Kill Enemy for Chest", // 48
|
||
"Clear Quadrant for Chest", // 49
|
||
"Clear Full Tile for Chest", // 50
|
||
"Light Torches to Open", // 51
|
||
"Holes 3", // 52
|
||
"Holes 4", // 53
|
||
"Holes 5", // 54
|
||
"Holes 6", // 55
|
||
"Agahnim Room", // 56
|
||
"Holes 7", // 57
|
||
"Holes 8", // 58
|
||
"Open Chest for Holes 8", // 59
|
||
"Push Block for Chest", // 60
|
||
"Clear Room for Triforce", // 61
|
||
"Light Torches for Chest", // 62
|
||
"Kill Boss Again", // 63
|
||
"64 (Unused)" // 64
|
||
};
|
||
constexpr int kNumTags = IM_ARRAYSIZE(tag_names);
|
||
|
||
const char* merge_types[] = {"Off", "Parallax", "Dark",
|
||
"On top", "Translucent", "Addition",
|
||
"Normal", "Transparent", "Dark room"};
|
||
const char* blend_modes[] = {"Normal", "Trans", "Add", "Dark", "Off"};
|
||
|
||
// ========================================================================
|
||
// ROOM PROPERTIES TABLE - Compact layout for docking
|
||
// ========================================================================
|
||
// Minimal table flags: no padding, no borders between body cells
|
||
constexpr ImGuiTableFlags kPropsTableFlags =
|
||
ImGuiTableFlags_NoPadOuterX | ImGuiTableFlags_NoBordersInBody;
|
||
if (ImGui::BeginTable("##RoomPropsTable", 2, kPropsTableFlags)) {
|
||
ImGui::TableSetupColumn("NavCol", ImGuiTableColumnFlags_WidthFixed, 90);
|
||
ImGui::TableSetupColumn("PropsCol", ImGuiTableColumnFlags_WidthStretch);
|
||
|
||
// Row 1: Navigation + Room ID + Core properties (Blockset, Palette, Layout, Spriteset)
|
||
ImGui::TableNextRow();
|
||
ImGui::TableNextColumn();
|
||
draw_navigation();
|
||
ImGui::TableNextColumn();
|
||
// Room ID and hex property inputs with icons
|
||
ImGui::Text(ICON_MD_TUNE " %03X", room_id);
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_VIEW_MODULE);
|
||
ImGui::SameLine(0, 2);
|
||
// Blockset: max 81 (kNumRoomBlocksets = 82)
|
||
if (auto res = gui::InputHexByteEx("##Blockset", &blockset_val, 81, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.SetBlockset(blockset_val);
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Blockset (0-51)");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_PALETTE);
|
||
ImGui::SameLine(0, 2);
|
||
// Palette: max 71 (kNumPalettesets = 72)
|
||
if (auto res = gui::InputHexByteEx("##Palette", &palette_val, 71, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.SetPalette(palette_val);
|
||
SetCurrentPaletteId(palette_val);
|
||
if (game_data_ && rom_) {
|
||
if (palette_val < game_data_->paletteset_ids.size() &&
|
||
!game_data_->paletteset_ids[palette_val].empty()) {
|
||
auto palette_ptr = game_data_->paletteset_ids[palette_val][0];
|
||
if (auto palette_id_res = rom_->ReadWord(0xDEC4B + palette_ptr);
|
||
palette_id_res.ok()) {
|
||
current_palette_group_id_ = palette_id_res.value() / 180;
|
||
if (current_palette_group_id_ <
|
||
game_data_->palette_groups.dungeon_main.size()) {
|
||
auto full_palette =
|
||
game_data_->palette_groups
|
||
.dungeon_main[current_palette_group_id_];
|
||
if (auto res =
|
||
gfx::CreatePaletteGroupFromLargePalette(full_palette, 16);
|
||
res.ok()) {
|
||
current_palette_group_ = res.value();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Palette (0-47)");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_GRID_VIEW);
|
||
ImGui::SameLine(0, 2);
|
||
// Layout: 8 valid layouts (0-7)
|
||
if (auto res = gui::InputHexByteEx("##Layout", &layout_val, 7, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.layout = layout_val;
|
||
room.MarkLayoutDirty();
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Layout (0-7)");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_PEST_CONTROL);
|
||
ImGui::SameLine(0, 2);
|
||
// Spriteset: max 143 (kNumSpritesets = 144)
|
||
if (auto res = gui::InputHexByteEx("##Spriteset", &spriteset_val, 143, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.SetSpriteset(spriteset_val);
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Spriteset (0-8F)");
|
||
|
||
// Row 2: Floor graphics + Effect (using vertical space from compass)
|
||
ImGui::TableNextRow();
|
||
ImGui::TableNextColumn();
|
||
// Empty - compass takes vertical space
|
||
ImGui::TableNextColumn();
|
||
ImGui::TextDisabled(ICON_MD_SQUARE);
|
||
ImGui::SameLine(0, 2);
|
||
// Floor graphics: max 15 (4-bit value, 0-F)
|
||
if (auto res = gui::InputHexByteEx("##Floor1", &floor1_val, 15, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.set_floor1(floor1_val);
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Floor 1 (0-F)");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_SQUARE_FOOT);
|
||
ImGui::SameLine(0, 2);
|
||
// Floor graphics: max 15 (4-bit value, 0-F)
|
||
if (auto res = gui::InputHexByteEx("##Floor2", &floor2_val, 15, 32.f, true);
|
||
res.ShouldApply()) {
|
||
room.set_floor2(floor2_val);
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Floor 2 (0-F)");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_AUTO_AWESOME);
|
||
ImGui::SameLine(0, 2);
|
||
constexpr int kNumEffects = IM_ARRAYSIZE(effect_names);
|
||
if (effect_val < 0) effect_val = 0;
|
||
if (effect_val >= kNumEffects) effect_val = kNumEffects - 1;
|
||
ImGui::SetNextItemWidth(140);
|
||
if (ImGui::BeginCombo("##Effect", effect_names[effect_val])) {
|
||
for (int i = 0; i < kNumEffects; i++) {
|
||
if (ImGui::Selectable(effect_names[i], effect_val == i)) {
|
||
room.SetEffect(static_cast<zelda3::EffectKey>(i));
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Effect");
|
||
|
||
// Row 3: Tags (using vertical space from compass)
|
||
ImGui::TableNextRow();
|
||
ImGui::TableNextColumn();
|
||
// Empty - compass takes vertical space
|
||
ImGui::TableNextColumn();
|
||
ImGui::TextDisabled(ICON_MD_LABEL);
|
||
ImGui::SameLine(0, 2);
|
||
int tag1_idx = std::clamp(tag1_val, 0, kNumTags - 1);
|
||
ImGui::SetNextItemWidth(240);
|
||
if (ImGui::BeginCombo("##Tag1", tag_names[tag1_idx])) {
|
||
for (int i = 0; i < kNumTags; i++) {
|
||
if (ImGui::Selectable(tag_names[i], tag1_idx == i)) {
|
||
room.SetTag1(static_cast<zelda3::TagKey>(i));
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Tag 1");
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled(ICON_MD_LABEL_OUTLINE);
|
||
ImGui::SameLine(0, 2);
|
||
int tag2_idx = std::clamp(tag2_val, 0, kNumTags - 1);
|
||
ImGui::SetNextItemWidth(240);
|
||
if (ImGui::BeginCombo("##Tag2", tag_names[tag2_idx])) {
|
||
for (int i = 0; i < kNumTags; i++) {
|
||
if (ImGui::Selectable(tag_names[i], tag2_idx == i)) {
|
||
room.SetTag2(static_cast<zelda3::TagKey>(i));
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Tag 2");
|
||
|
||
// Row 3: Layer visibility + Blend/Merge
|
||
ImGui::TableNextRow();
|
||
ImGui::TableNextColumn();
|
||
ImGui::TextDisabled(ICON_MD_LAYERS " Layers");
|
||
ImGui::TableNextColumn();
|
||
bool bg1_layout = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout);
|
||
bool bg1_objects = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects);
|
||
bool bg2_layout = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout);
|
||
bool bg2_objects = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects);
|
||
|
||
// Helper to mark layer bitmaps as needing texture update
|
||
auto mark_layers_dirty = [&]() {
|
||
if (rooms_) {
|
||
auto& r = (*rooms_)[room_id];
|
||
r.bg1_buffer().bitmap().set_modified(true);
|
||
r.bg2_buffer().bitmap().set_modified(true);
|
||
r.object_bg1_buffer().bitmap().set_modified(true);
|
||
r.object_bg2_buffer().bitmap().set_modified(true);
|
||
r.MarkCompositeDirty();
|
||
}
|
||
};
|
||
|
||
if (ImGui::Checkbox("BG1##L", &bg1_layout)) {
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout, bg1_layout);
|
||
mark_layers_dirty();
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("BG1 Layout: Main floor tiles (rendered on top of BG2)");
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Checkbox("O1##O", &bg1_objects)) {
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Objects, bg1_objects);
|
||
mark_layers_dirty();
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("BG1 Objects: Walls, pots, interactive objects (topmost layer)");
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Checkbox("BG2##L2", &bg2_layout)) {
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout, bg2_layout);
|
||
mark_layers_dirty();
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("BG2 Layout: Background floor patterns (behind BG1)");
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Checkbox("O2##O2", &bg2_objects)) {
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Objects, bg2_objects);
|
||
mark_layers_dirty();
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("BG2 Objects: Background details (behind BG1)");
|
||
}
|
||
ImGui::SameLine();
|
||
ImGui::SetNextItemWidth(60);
|
||
int bg2_blend = static_cast<int>(
|
||
layer_mgr.GetLayerBlendMode(zelda3::LayerType::BG2_Layout));
|
||
if (ImGui::Combo("##Bld", &bg2_blend, blend_modes, IM_ARRAYSIZE(blend_modes))) {
|
||
auto mode = static_cast<zelda3::LayerBlendMode>(bg2_blend);
|
||
layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Layout, mode);
|
||
layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Objects, mode);
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(
|
||
"BG2 Blend Mode (color math effect):\n"
|
||
"- Normal: Opaque pixels\n"
|
||
"- Translucent: 50% alpha\n"
|
||
"- Addition: Additive blending\n"
|
||
"Does not change layer order (BG1 always on top)");
|
||
}
|
||
ImGui::SameLine();
|
||
ImGui::SetNextItemWidth(70);
|
||
int merge_val = room.layer_merging().ID;
|
||
if (ImGui::Combo("##Mrg", &merge_val, merge_types, IM_ARRAYSIZE(merge_types))) {
|
||
room.SetLayerMerging(zelda3::kLayerMergeTypeList[merge_val]);
|
||
layer_mgr.ApplyLayerMergingPreserveVisibility(room.layer_merging());
|
||
if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Merge type");
|
||
|
||
// Row 4: Selection filter
|
||
ImGui::TableNextRow();
|
||
ImGui::TableNextColumn();
|
||
ImGui::TextDisabled(ICON_MD_SELECT_ALL " Select");
|
||
ImGui::TableNextColumn();
|
||
object_interaction_.SetLayersMerged(layer_mgr.AreLayersMerged());
|
||
int current_filter = object_interaction_.GetLayerFilter();
|
||
if (ImGui::RadioButton("All", current_filter == ObjectSelection::kLayerAll))
|
||
object_interaction_.SetLayerFilter(ObjectSelection::kLayerAll);
|
||
ImGui::SameLine();
|
||
if (ImGui::RadioButton("L1", current_filter == ObjectSelection::kLayer1))
|
||
object_interaction_.SetLayerFilter(ObjectSelection::kLayer1);
|
||
ImGui::SameLine();
|
||
if (ImGui::RadioButton("L2", current_filter == ObjectSelection::kLayer2))
|
||
object_interaction_.SetLayerFilter(ObjectSelection::kLayer2);
|
||
ImGui::SameLine();
|
||
if (ImGui::RadioButton("L3", current_filter == ObjectSelection::kLayer3))
|
||
object_interaction_.SetLayerFilter(ObjectSelection::kLayer3);
|
||
ImGui::SameLine();
|
||
// Mask mode: filter to BG2/Layer 1 overlay objects only (platforms, statues, etc.)
|
||
bool is_mask_mode = current_filter == ObjectSelection::kMaskLayer;
|
||
if (is_mask_mode) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 1.0f, 1.0f));
|
||
if (ImGui::RadioButton("Mask", is_mask_mode))
|
||
object_interaction_.SetLayerFilter(ObjectSelection::kMaskLayer);
|
||
if (is_mask_mode) ImGui::PopStyleColor();
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(
|
||
"Mask Selection Mode\n"
|
||
"Only select BG2/Layer 1 overlay objects (platforms, statues, stairs)\n"
|
||
"These are the objects that create transparency holes in BG1");
|
||
}
|
||
if (object_interaction_.IsLayerFilterActive() && !is_mask_mode) {
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), ICON_MD_FILTER_ALT);
|
||
}
|
||
if (layer_mgr.AreLayersMerged()) {
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), ICON_MD_MERGE_TYPE);
|
||
}
|
||
|
||
ImGui::EndTable();
|
||
}
|
||
|
||
// === Quick Access Toolbar for Entity Pickers ===
|
||
ImGui::Spacing();
|
||
ImGui::BeginGroup();
|
||
ImGui::TextDisabled(ICON_MD_ADD_CIRCLE " Place:");
|
||
ImGui::SameLine();
|
||
|
||
// Object picker button
|
||
if (ImGui::Button(ICON_MD_WIDGETS " Object")) {
|
||
if (show_object_panel_callback_) {
|
||
show_object_panel_callback_();
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("Open Object Editor panel to select objects for placement");
|
||
}
|
||
ImGui::SameLine();
|
||
|
||
// Sprite picker button
|
||
if (ImGui::Button(ICON_MD_PERSON " Sprite")) {
|
||
if (show_sprite_panel_callback_) {
|
||
show_sprite_panel_callback_();
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("Open Sprite Editor panel to select sprites for placement");
|
||
}
|
||
ImGui::SameLine();
|
||
|
||
// Item picker button
|
||
if (ImGui::Button(ICON_MD_INVENTORY " Item")) {
|
||
if (show_item_panel_callback_) {
|
||
show_item_panel_callback_();
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("Open Item Editor panel to select items for placement");
|
||
}
|
||
ImGui::SameLine();
|
||
|
||
// Door placement toggle (inline)
|
||
bool door_mode = object_interaction_.IsDoorPlacementActive();
|
||
if (door_mode) {
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.6f, 0.9f, 1.0f));
|
||
}
|
||
if (ImGui::Button(ICON_MD_DOOR_FRONT " Door")) {
|
||
object_interaction_.SetDoorPlacementMode(!door_mode, zelda3::DoorType::NormalDoor);
|
||
}
|
||
if (door_mode) {
|
||
ImGui::PopStyleColor();
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(door_mode ? "Click to cancel door placement" : "Click to place doors");
|
||
}
|
||
ImGui::EndGroup();
|
||
ImGui::Separator();
|
||
}
|
||
|
||
ImGui::EndGroup();
|
||
|
||
// Set up context menu items BEFORE DrawBackground so DrawContextMenu can be
|
||
// called immediately after (OpenPopupOnItemClick requires this ordering)
|
||
canvas_.ClearContextMenuItems();
|
||
|
||
if (rooms_ && rom_->is_loaded()) {
|
||
auto& room = (*rooms_)[room_id];
|
||
|
||
// === Entity Placement Menu ===
|
||
gui::CanvasMenuItem place_menu;
|
||
place_menu.label = "Place Entity";
|
||
place_menu.icon = ICON_MD_ADD;
|
||
|
||
// Place Object option
|
||
place_menu.subitems.push_back(gui::CanvasMenuItem(
|
||
"Object", ICON_MD_WIDGETS,
|
||
[this]() {
|
||
if (show_object_panel_callback_) {
|
||
show_object_panel_callback_();
|
||
}
|
||
}));
|
||
|
||
// Place Sprite option
|
||
place_menu.subitems.push_back(gui::CanvasMenuItem(
|
||
"Sprite", ICON_MD_PERSON,
|
||
[this]() {
|
||
bool active = object_interaction_.IsSpritePlacementActive();
|
||
object_interaction_.SetSpritePlacementMode(!active, 0x09);
|
||
}));
|
||
|
||
// Place Item option
|
||
place_menu.subitems.push_back(gui::CanvasMenuItem(
|
||
"Item", ICON_MD_INVENTORY,
|
||
[this]() {
|
||
bool active = object_interaction_.IsItemPlacementActive();
|
||
object_interaction_.SetItemPlacementMode(!active, 1);
|
||
}));
|
||
|
||
// Place Door option
|
||
place_menu.subitems.push_back(gui::CanvasMenuItem(
|
||
"Door", ICON_MD_DOOR_FRONT,
|
||
[this]() {
|
||
bool active = object_interaction_.IsDoorPlacementActive();
|
||
object_interaction_.SetDoorPlacementMode(!active,
|
||
zelda3::DoorType::NormalDoor);
|
||
}));
|
||
|
||
canvas_.AddContextMenuItem(place_menu);
|
||
|
||
// Add room property quick toggles (4-way layer visibility)
|
||
gui::CanvasMenuItem layer_menu;
|
||
layer_menu.label = "Layer Visibility";
|
||
layer_menu.icon = ICON_MD_LAYERS;
|
||
|
||
layer_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("BG1 Layout", [this, room_id]() {
|
||
auto& mgr = GetRoomLayerManager(room_id);
|
||
mgr.SetLayerVisible(
|
||
zelda3::LayerType::BG1_Layout,
|
||
!mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout));
|
||
}));
|
||
layer_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("BG1 Objects", [this, room_id]() {
|
||
auto& mgr = GetRoomLayerManager(room_id);
|
||
mgr.SetLayerVisible(
|
||
zelda3::LayerType::BG1_Objects,
|
||
!mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects));
|
||
}));
|
||
layer_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("BG2 Layout", [this, room_id]() {
|
||
auto& mgr = GetRoomLayerManager(room_id);
|
||
mgr.SetLayerVisible(
|
||
zelda3::LayerType::BG2_Layout,
|
||
!mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout));
|
||
}));
|
||
layer_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("BG2 Objects", [this, room_id]() {
|
||
auto& mgr = GetRoomLayerManager(room_id);
|
||
mgr.SetLayerVisible(
|
||
zelda3::LayerType::BG2_Objects,
|
||
!mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects));
|
||
}));
|
||
|
||
canvas_.AddContextMenuItem(layer_menu);
|
||
|
||
// Entity Visibility menu
|
||
gui::CanvasMenuItem entity_menu;
|
||
entity_menu.label = "Entity Visibility";
|
||
entity_menu.icon = ICON_MD_PERSON;
|
||
|
||
entity_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Show Sprites", [this]() {
|
||
entity_visibility_.show_sprites = !entity_visibility_.show_sprites;
|
||
}));
|
||
entity_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Show Pot Items", [this]() {
|
||
entity_visibility_.show_pot_items = !entity_visibility_.show_pot_items;
|
||
}));
|
||
|
||
canvas_.AddContextMenuItem(entity_menu);
|
||
|
||
// Add re-render option
|
||
canvas_.AddContextMenuItem(gui::CanvasMenuItem(
|
||
"Re-render Room", ICON_MD_REFRESH,
|
||
[&room]() { room.RenderRoomGraphics(); }, "Ctrl+R"));
|
||
|
||
// Grid Options
|
||
gui::CanvasMenuItem grid_menu;
|
||
grid_menu.label = "Grid Options";
|
||
grid_menu.icon = ICON_MD_GRID_ON;
|
||
|
||
// Toggle grid visibility
|
||
gui::CanvasMenuItem toggle_grid_item(
|
||
show_grid_ ? "Hide Grid" : "Show Grid",
|
||
show_grid_ ? ICON_MD_GRID_OFF : ICON_MD_GRID_ON,
|
||
[this]() { show_grid_ = !show_grid_; }, "G");
|
||
grid_menu.subitems.push_back(toggle_grid_item);
|
||
|
||
// Grid size options (only show if grid is visible)
|
||
grid_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("8x8", [this]() { custom_grid_size_ = 8; show_grid_ = true; }));
|
||
grid_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("16x16", [this]() { custom_grid_size_ = 16; show_grid_ = true; }));
|
||
grid_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("32x32", [this]() { custom_grid_size_ = 32; show_grid_ = true; }));
|
||
|
||
canvas_.AddContextMenuItem(grid_menu);
|
||
|
||
// === DEBUG MENU ===
|
||
gui::CanvasMenuItem debug_menu;
|
||
debug_menu.label = "Debug";
|
||
debug_menu.icon = ICON_MD_BUG_REPORT;
|
||
|
||
// Show room info
|
||
gui::CanvasMenuItem room_info_item(
|
||
"Show Room Info", ICON_MD_INFO,
|
||
[this]() { show_room_debug_info_ = !show_room_debug_info_; });
|
||
debug_menu.subitems.push_back(room_info_item);
|
||
|
||
// Show texture info
|
||
gui::CanvasMenuItem texture_info_item(
|
||
"Show Texture Debug", ICON_MD_IMAGE,
|
||
[this]() { show_texture_debug_ = !show_texture_debug_; });
|
||
debug_menu.subitems.push_back(texture_info_item);
|
||
|
||
// Toggle coordinate overlay
|
||
gui::CanvasMenuItem coord_overlay_item(
|
||
show_coordinate_overlay_ ? "Hide Coordinates" : "Show Coordinates",
|
||
ICON_MD_MY_LOCATION,
|
||
[this]() { show_coordinate_overlay_ = !show_coordinate_overlay_; });
|
||
debug_menu.subitems.push_back(coord_overlay_item);
|
||
|
||
// Show object bounds with sub-menu for categories
|
||
gui::CanvasMenuItem object_bounds_menu;
|
||
object_bounds_menu.label = "Show Object Bounds";
|
||
object_bounds_menu.icon = ICON_MD_CROP_SQUARE;
|
||
object_bounds_menu.callback = [this]() {
|
||
show_object_bounds_ = !show_object_bounds_;
|
||
};
|
||
|
||
// Sub-menu for filtering by type
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Type 1 (0x00-0xFF)", [this]() {
|
||
object_outline_toggles_.show_type1_objects =
|
||
!object_outline_toggles_.show_type1_objects;
|
||
}));
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Type 2 (0x100-0x1FF)", [this]() {
|
||
object_outline_toggles_.show_type2_objects =
|
||
!object_outline_toggles_.show_type2_objects;
|
||
}));
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Type 3 (0xF00-0xFFF)", [this]() {
|
||
object_outline_toggles_.show_type3_objects =
|
||
!object_outline_toggles_.show_type3_objects;
|
||
}));
|
||
|
||
// Separator
|
||
gui::CanvasMenuItem sep;
|
||
sep.label = "---";
|
||
sep.enabled_condition = []() {
|
||
return false;
|
||
};
|
||
object_bounds_menu.subitems.push_back(sep);
|
||
|
||
// Sub-menu for filtering by layer
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Layer 0 (BG1)", [this]() {
|
||
object_outline_toggles_.show_layer0_objects =
|
||
!object_outline_toggles_.show_layer0_objects;
|
||
}));
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Layer 1 (BG2)", [this]() {
|
||
object_outline_toggles_.show_layer1_objects =
|
||
!object_outline_toggles_.show_layer1_objects;
|
||
}));
|
||
object_bounds_menu.subitems.push_back(
|
||
gui::CanvasMenuItem("Layer 2 (BG3)", [this]() {
|
||
object_outline_toggles_.show_layer2_objects =
|
||
!object_outline_toggles_.show_layer2_objects;
|
||
}));
|
||
|
||
debug_menu.subitems.push_back(object_bounds_menu);
|
||
|
||
// Show layer info
|
||
gui::CanvasMenuItem layer_info_item(
|
||
"Show Layer Info", ICON_MD_LAYERS,
|
||
[this]() { show_layer_info_ = !show_layer_info_; });
|
||
debug_menu.subitems.push_back(layer_info_item);
|
||
|
||
// Force reload room
|
||
gui::CanvasMenuItem force_reload_item(
|
||
"Force Reload", ICON_MD_REFRESH, [&room]() {
|
||
room.LoadObjects();
|
||
room.LoadRoomGraphics(room.blockset);
|
||
room.RenderRoomGraphics();
|
||
});
|
||
debug_menu.subitems.push_back(force_reload_item);
|
||
|
||
// Log room state
|
||
gui::CanvasMenuItem log_item(
|
||
"Log Room State", ICON_MD_PRINT, [&room, room_id]() {
|
||
LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id);
|
||
LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d",
|
||
room.blockset, room.palette, room.layout);
|
||
LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu",
|
||
room.GetTileObjects().size(), room.GetSprites().size());
|
||
LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d",
|
||
room.bg1_buffer().bitmap().width(),
|
||
room.bg1_buffer().bitmap().height(),
|
||
room.bg2_buffer().bitmap().width(),
|
||
room.bg2_buffer().bitmap().height());
|
||
});
|
||
debug_menu.subitems.push_back(log_item);
|
||
|
||
canvas_.AddContextMenuItem(debug_menu);
|
||
}
|
||
|
||
// Add object interaction menu items to canvas context menu
|
||
if (object_interaction_enabled_) {
|
||
auto& interaction = object_interaction_;
|
||
auto selected = interaction.GetSelectedObjectIndices();
|
||
const bool has_selection = !selected.empty();
|
||
const bool single_selection = selected.size() == 1;
|
||
const bool has_clipboard = interaction.HasClipboardData();
|
||
const bool placing_object = interaction.IsObjectLoaded();
|
||
|
||
if (single_selection && rooms_) {
|
||
auto& room = (*rooms_)[room_id];
|
||
const auto& objects = room.GetTileObjects();
|
||
if (selected[0] < objects.size()) {
|
||
const auto& obj = objects[selected[0]];
|
||
std::string name = GetObjectName(obj.id_);
|
||
canvas_.AddContextMenuItem(gui::CanvasMenuItem::Disabled(
|
||
absl::StrFormat("Object 0x%02X: %s", obj.id_, name.c_str())));
|
||
}
|
||
}
|
||
|
||
auto enabled_if = [](bool enabled) {
|
||
return [enabled]() {
|
||
return enabled;
|
||
};
|
||
};
|
||
|
||
gui::CanvasMenuItem cut_item(
|
||
"Cut", ICON_MD_CONTENT_CUT,
|
||
[&interaction]() {
|
||
interaction.HandleCopySelected();
|
||
interaction.HandleDeleteSelected();
|
||
},
|
||
"Ctrl+X");
|
||
cut_item.enabled_condition = enabled_if(has_selection);
|
||
canvas_.AddContextMenuItem(cut_item);
|
||
|
||
gui::CanvasMenuItem copy_item(
|
||
"Copy", ICON_MD_CONTENT_COPY,
|
||
[&interaction]() { interaction.HandleCopySelected(); }, "Ctrl+C");
|
||
copy_item.enabled_condition = enabled_if(has_selection);
|
||
canvas_.AddContextMenuItem(copy_item);
|
||
|
||
gui::CanvasMenuItem duplicate_item(
|
||
"Duplicate", ICON_MD_CONTENT_PASTE,
|
||
[&interaction]() {
|
||
interaction.HandleCopySelected();
|
||
interaction.HandlePasteObjects();
|
||
},
|
||
"Ctrl+D");
|
||
duplicate_item.enabled_condition = enabled_if(has_selection);
|
||
canvas_.AddContextMenuItem(duplicate_item);
|
||
|
||
gui::CanvasMenuItem delete_item(
|
||
"Delete", ICON_MD_DELETE,
|
||
[&interaction]() { interaction.HandleDeleteSelected(); }, "Del");
|
||
delete_item.enabled_condition = enabled_if(has_selection);
|
||
canvas_.AddContextMenuItem(delete_item);
|
||
|
||
gui::CanvasMenuItem paste_item(
|
||
"Paste", ICON_MD_CONTENT_PASTE,
|
||
[&interaction]() { interaction.HandlePasteObjects(); }, "Ctrl+V");
|
||
paste_item.enabled_condition = enabled_if(has_clipboard);
|
||
canvas_.AddContextMenuItem(paste_item);
|
||
|
||
gui::CanvasMenuItem cancel_item(
|
||
"Cancel Placement", ICON_MD_CANCEL,
|
||
[&interaction]() { interaction.CancelPlacement(); }, "Esc");
|
||
cancel_item.enabled_condition = enabled_if(placing_object);
|
||
canvas_.AddContextMenuItem(cancel_item);
|
||
|
||
// Send to Layer submenu
|
||
gui::CanvasMenuItem layer_menu;
|
||
layer_menu.label = "Send to Layer";
|
||
layer_menu.icon = ICON_MD_LAYERS;
|
||
layer_menu.enabled_condition = enabled_if(has_selection);
|
||
|
||
gui::CanvasMenuItem layer1_item(
|
||
"Layer 1 (BG1)", ICON_MD_LOOKS_ONE,
|
||
[&interaction]() { interaction.SendSelectedToLayer(0); }, "1");
|
||
layer1_item.enabled_condition = enabled_if(has_selection);
|
||
layer_menu.subitems.push_back(layer1_item);
|
||
|
||
gui::CanvasMenuItem layer2_item(
|
||
"Layer 2 (BG2)", ICON_MD_LOOKS_TWO,
|
||
[&interaction]() { interaction.SendSelectedToLayer(1); }, "2");
|
||
layer2_item.enabled_condition = enabled_if(has_selection);
|
||
layer_menu.subitems.push_back(layer2_item);
|
||
|
||
gui::CanvasMenuItem layer3_item(
|
||
"Layer 3 (BG3)", ICON_MD_LOOKS_3,
|
||
[&interaction]() { interaction.SendSelectedToLayer(2); }, "3");
|
||
layer3_item.enabled_condition = enabled_if(has_selection);
|
||
layer_menu.subitems.push_back(layer3_item);
|
||
|
||
canvas_.AddContextMenuItem(layer_menu);
|
||
|
||
// Arrange submenu (object draw order)
|
||
gui::CanvasMenuItem arrange_menu;
|
||
arrange_menu.label = "Arrange";
|
||
arrange_menu.icon = ICON_MD_SWAP_VERT;
|
||
arrange_menu.enabled_condition = enabled_if(has_selection);
|
||
|
||
gui::CanvasMenuItem bring_front_item(
|
||
"Bring to Front", ICON_MD_FLIP_TO_FRONT,
|
||
[&interaction]() { interaction.SendSelectedToFront(); }, "Ctrl+Shift+]");
|
||
bring_front_item.enabled_condition = enabled_if(has_selection);
|
||
arrange_menu.subitems.push_back(bring_front_item);
|
||
|
||
gui::CanvasMenuItem send_back_item(
|
||
"Send to Back", ICON_MD_FLIP_TO_BACK,
|
||
[&interaction]() { interaction.SendSelectedToBack(); }, "Ctrl+Shift+[");
|
||
send_back_item.enabled_condition = enabled_if(has_selection);
|
||
arrange_menu.subitems.push_back(send_back_item);
|
||
|
||
gui::CanvasMenuItem bring_forward_item(
|
||
"Bring Forward", ICON_MD_ARROW_UPWARD,
|
||
[&interaction]() { interaction.BringSelectedForward(); }, "Ctrl+]");
|
||
bring_forward_item.enabled_condition = enabled_if(has_selection);
|
||
arrange_menu.subitems.push_back(bring_forward_item);
|
||
|
||
gui::CanvasMenuItem send_backward_item(
|
||
"Send Backward", ICON_MD_ARROW_DOWNWARD,
|
||
[&interaction]() { interaction.SendSelectedBackward(); }, "Ctrl+[");
|
||
send_backward_item.enabled_condition = enabled_if(has_selection);
|
||
arrange_menu.subitems.push_back(send_backward_item);
|
||
|
||
canvas_.AddContextMenuItem(arrange_menu);
|
||
|
||
// === Entity Selection Actions (Doors, Sprites, Items) ===
|
||
const auto& selected_entity = interaction.GetSelectedEntity();
|
||
const bool has_entity_selection = interaction.HasEntitySelection();
|
||
|
||
if (has_entity_selection && rooms_) {
|
||
auto& room = (*rooms_)[room_id];
|
||
|
||
// Show selected entity info header
|
||
std::string entity_info;
|
||
switch (selected_entity.type) {
|
||
case EntityType::Door: {
|
||
const auto& doors = room.GetDoors();
|
||
if (selected_entity.index < doors.size()) {
|
||
const auto& door = doors[selected_entity.index];
|
||
entity_info = absl::StrFormat(ICON_MD_DOOR_FRONT " Door: %s",
|
||
std::string(zelda3::GetDoorTypeName(door.type)).c_str());
|
||
}
|
||
break;
|
||
}
|
||
case EntityType::Sprite: {
|
||
const auto& sprites = room.GetSprites();
|
||
if (selected_entity.index < sprites.size()) {
|
||
const auto& sprite = sprites[selected_entity.index];
|
||
entity_info = absl::StrFormat(ICON_MD_PERSON " Sprite: %s (0x%02X)",
|
||
zelda3::ResolveSpriteName(sprite.id()), sprite.id());
|
||
}
|
||
break;
|
||
}
|
||
case EntityType::Item: {
|
||
const auto& items = room.GetPotItems();
|
||
if (selected_entity.index < items.size()) {
|
||
const auto& item = items[selected_entity.index];
|
||
entity_info = absl::StrFormat(ICON_MD_INVENTORY " Item: 0x%02X", item.item);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (!entity_info.empty()) {
|
||
canvas_.AddContextMenuItem(gui::CanvasMenuItem::Disabled(entity_info));
|
||
|
||
// Delete entity action
|
||
gui::CanvasMenuItem delete_entity_item(
|
||
"Delete Entity", ICON_MD_DELETE,
|
||
[this, &room, selected_entity]() {
|
||
switch (selected_entity.type) {
|
||
case EntityType::Door: {
|
||
auto& doors = room.GetDoors();
|
||
if (selected_entity.index < doors.size()) {
|
||
doors.erase(doors.begin() +
|
||
static_cast<long>(selected_entity.index));
|
||
}
|
||
break;
|
||
}
|
||
case EntityType::Sprite: {
|
||
auto& sprites = room.GetSprites();
|
||
if (selected_entity.index < sprites.size()) {
|
||
sprites.erase(sprites.begin() +
|
||
static_cast<long>(selected_entity.index));
|
||
}
|
||
break;
|
||
}
|
||
case EntityType::Item: {
|
||
auto& items = room.GetPotItems();
|
||
if (selected_entity.index < items.size()) {
|
||
items.erase(items.begin() +
|
||
static_cast<long>(selected_entity.index));
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
object_interaction_.ClearEntitySelection();
|
||
},
|
||
"Del");
|
||
canvas_.AddContextMenuItem(delete_entity_item);
|
||
}
|
||
}
|
||
}
|
||
|
||
// CRITICAL: Begin canvas frame using modern BeginCanvas/EndCanvas pattern
|
||
// This replaces DrawBackground + DrawContextMenu with a unified frame
|
||
auto canvas_rt = gui::BeginCanvas(canvas_, frame_opts);
|
||
|
||
// Draw persistent debug overlays
|
||
if (show_room_debug_info_ && rooms_ && rom_->is_loaded()) {
|
||
auto& room = (*rooms_)[room_id];
|
||
ImGui::SetNextWindowPos(
|
||
ImVec2(canvas_.zero_point().x + 10, canvas_.zero_point().y + 10),
|
||
ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_FirstUseEver);
|
||
if (ImGui::Begin("Room Debug Info", &show_room_debug_info_,
|
||
ImGuiWindowFlags_NoCollapse)) {
|
||
ImGui::Text("Room: 0x%03X (%d)", room_id, room_id);
|
||
ImGui::Separator();
|
||
ImGui::Text("Graphics");
|
||
ImGui::Text(" Blockset: 0x%02X", room.blockset);
|
||
ImGui::Text(" Palette: 0x%02X", room.palette);
|
||
ImGui::Text(" Layout: 0x%02X", room.layout);
|
||
ImGui::Text(" Spriteset: 0x%02X", room.spriteset);
|
||
ImGui::Separator();
|
||
ImGui::Text("Content");
|
||
ImGui::Text(" Objects: %zu", room.GetTileObjects().size());
|
||
ImGui::Text(" Sprites: %zu", room.GetSprites().size());
|
||
ImGui::Separator();
|
||
ImGui::Text("Buffers");
|
||
auto& bg1 = room.bg1_buffer().bitmap();
|
||
auto& bg2 = room.bg2_buffer().bitmap();
|
||
ImGui::Text(" BG1: %dx%d %s", bg1.width(), bg1.height(),
|
||
bg1.texture() ? "(has texture)" : "(NO TEXTURE)");
|
||
ImGui::Text(" BG2: %dx%d %s", bg2.width(), bg2.height(),
|
||
bg2.texture() ? "(has texture)" : "(NO TEXTURE)");
|
||
ImGui::Separator();
|
||
ImGui::Text("Layers (4-way)");
|
||
auto& layer_mgr = GetRoomLayerManager(room_id);
|
||
bool bg1l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout);
|
||
bool bg1o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects);
|
||
bool bg2l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout);
|
||
bool bg2o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects);
|
||
if (ImGui::Checkbox("BG1 Layout", &bg1l))
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout, bg1l);
|
||
if (ImGui::Checkbox("BG1 Objects", &bg1o))
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Objects, bg1o);
|
||
if (ImGui::Checkbox("BG2 Layout", &bg2l))
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout, bg2l);
|
||
if (ImGui::Checkbox("BG2 Objects", &bg2o))
|
||
layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Objects, bg2o);
|
||
int blend = static_cast<int>(
|
||
layer_mgr.GetLayerBlendMode(zelda3::LayerType::BG2_Layout));
|
||
if (ImGui::SliderInt("BG2 Blend", &blend, 0, 4)) {
|
||
layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Layout,
|
||
static_cast<zelda3::LayerBlendMode>(blend));
|
||
layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Objects,
|
||
static_cast<zelda3::LayerBlendMode>(blend));
|
||
}
|
||
|
||
ImGui::Separator();
|
||
ImGui::Text("Layout Override");
|
||
static bool enable_override = false;
|
||
ImGui::Checkbox("Enable Override", &enable_override);
|
||
if (enable_override) {
|
||
ImGui::SliderInt("Layout ID", &layout_override_, 0, 7);
|
||
} else {
|
||
layout_override_ = -1; // Disable override
|
||
}
|
||
|
||
if (show_object_bounds_) {
|
||
ImGui::Separator();
|
||
ImGui::Text("Object Outline Filters");
|
||
ImGui::Text("By Type:");
|
||
ImGui::Checkbox("Type 1", &object_outline_toggles_.show_type1_objects);
|
||
ImGui::Checkbox("Type 2", &object_outline_toggles_.show_type2_objects);
|
||
ImGui::Checkbox("Type 3", &object_outline_toggles_.show_type3_objects);
|
||
ImGui::Text("By Layer:");
|
||
ImGui::Checkbox("Layer 0",
|
||
&object_outline_toggles_.show_layer0_objects);
|
||
ImGui::Checkbox("Layer 1",
|
||
&object_outline_toggles_.show_layer1_objects);
|
||
ImGui::Checkbox("Layer 2",
|
||
&object_outline_toggles_.show_layer2_objects);
|
||
}
|
||
}
|
||
ImGui::End();
|
||
}
|
||
|
||
if (show_texture_debug_ && rooms_ && rom_->is_loaded()) {
|
||
ImGui::SetNextWindowPos(
|
||
ImVec2(canvas_.zero_point().x + 320, canvas_.zero_point().y + 10),
|
||
ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(250, 0), ImGuiCond_FirstUseEver);
|
||
if (ImGui::Begin("Texture Debug", &show_texture_debug_,
|
||
ImGuiWindowFlags_NoCollapse)) {
|
||
auto& room = (*rooms_)[room_id];
|
||
auto& bg1 = room.bg1_buffer().bitmap();
|
||
auto& bg2 = room.bg2_buffer().bitmap();
|
||
|
||
ImGui::Text("BG1 Bitmap");
|
||
ImGui::Text(" Size: %dx%d", bg1.width(), bg1.height());
|
||
ImGui::Text(" Active: %s", bg1.is_active() ? "YES" : "NO");
|
||
ImGui::Text(" Texture: 0x%p", bg1.texture());
|
||
ImGui::Text(" Modified: %s", bg1.modified() ? "YES" : "NO");
|
||
|
||
if (bg1.texture()) {
|
||
ImGui::Text(" Preview:");
|
||
ImGui::Image((ImTextureID)(intptr_t)bg1.texture(), ImVec2(128, 128));
|
||
}
|
||
|
||
ImGui::Separator();
|
||
ImGui::Text("BG2 Bitmap");
|
||
ImGui::Text(" Size: %dx%d", bg2.width(), bg2.height());
|
||
ImGui::Text(" Active: %s", bg2.is_active() ? "YES" : "NO");
|
||
ImGui::Text(" Texture: 0x%p", bg2.texture());
|
||
ImGui::Text(" Modified: %s", bg2.modified() ? "YES" : "NO");
|
||
|
||
if (bg2.texture()) {
|
||
ImGui::Text(" Preview:");
|
||
ImGui::Image((ImTextureID)(intptr_t)bg2.texture(), ImVec2(128, 128));
|
||
}
|
||
}
|
||
ImGui::End();
|
||
}
|
||
|
||
if (show_layer_info_) {
|
||
ImGui::SetNextWindowPos(
|
||
ImVec2(canvas_.zero_point().x + 580, canvas_.zero_point().y + 10),
|
||
ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(220, 0), ImGuiCond_FirstUseEver);
|
||
if (ImGui::Begin("Layer Info", &show_layer_info_,
|
||
ImGuiWindowFlags_NoCollapse)) {
|
||
ImGui::Text("Canvas Scale: %.2f", canvas_.global_scale());
|
||
ImGui::Text("Canvas Size: %.0fx%.0f", canvas_.width(), canvas_.height());
|
||
auto& layer_mgr = GetRoomLayerManager(room_id);
|
||
ImGui::Separator();
|
||
ImGui::Text("Layer Visibility (4-way):");
|
||
|
||
// Display each layer with visibility and blend mode
|
||
for (int i = 0; i < 4; ++i) {
|
||
auto layer = static_cast<zelda3::LayerType>(i);
|
||
bool visible = layer_mgr.IsLayerVisible(layer);
|
||
auto blend = layer_mgr.GetLayerBlendMode(layer);
|
||
ImGui::Text(" %s: %s (%s)",
|
||
zelda3::RoomLayerManager::GetLayerName(layer),
|
||
visible ? "VISIBLE" : "hidden",
|
||
zelda3::RoomLayerManager::GetBlendModeName(blend));
|
||
}
|
||
|
||
ImGui::Separator();
|
||
ImGui::Text("Draw Order:");
|
||
auto draw_order = layer_mgr.GetDrawOrder();
|
||
for (int i = 0; i < 4; ++i) {
|
||
ImGui::Text(" %d: %s", i + 1,
|
||
zelda3::RoomLayerManager::GetLayerName(draw_order[i]));
|
||
}
|
||
ImGui::Text("BG2 On Top: %s", layer_mgr.IsBG2OnTop() ? "YES" : "NO");
|
||
}
|
||
ImGui::End();
|
||
}
|
||
|
||
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;
|
||
|
||
// Render immediately if needed (but only once per room change)
|
||
static int last_rendered_room = -1;
|
||
static bool has_rendered = false;
|
||
if (needs_render && (last_rendered_room != room_id || !has_rendered)) {
|
||
printf(
|
||
"[DungeonCanvasViewer] Loading and rendering graphics for room %d\n",
|
||
room_id);
|
||
(void)LoadAndRenderRoomGraphics(room_id);
|
||
last_rendered_room = room_id;
|
||
has_rendered = true;
|
||
}
|
||
|
||
// Load room objects if not already loaded
|
||
if (room.GetTileObjects().empty()) {
|
||
room.LoadObjects();
|
||
}
|
||
|
||
// Load sprites if not already loaded
|
||
if (room.GetSprites().empty()) {
|
||
room.LoadSprites();
|
||
}
|
||
|
||
// Load pot items if not already loaded
|
||
if (room.GetPotItems().empty()) {
|
||
room.LoadPotItems();
|
||
}
|
||
|
||
// CRITICAL: Process texture queue BEFORE drawing to ensure textures are
|
||
// ready This must happen before DrawRoomBackgroundLayers() attempts to draw
|
||
// bitmaps
|
||
if (rom_ && rom_->is_loaded()) {
|
||
gfx::Arena::Get().ProcessTextureQueue(renderer_);
|
||
}
|
||
|
||
// Draw the room's background layers to canvas
|
||
// This already includes objects rendered by ObjectDrawer in
|
||
// Room::RenderObjectsToBackground()
|
||
DrawRoomBackgroundLayers(room_id);
|
||
|
||
// Draw mask highlights when mask selection mode is active
|
||
// This helps visualize which objects are BG2 overlays
|
||
if (object_interaction_.IsMaskModeActive()) {
|
||
DrawMaskHighlights(canvas_rt, room);
|
||
}
|
||
|
||
// Render entity overlays (sprites, pot items) as colored squares with labels
|
||
// (Entities are not part of the background buffers)
|
||
RenderEntityOverlay(canvas_rt, room);
|
||
|
||
// Handle object interaction if enabled
|
||
if (object_interaction_enabled_) {
|
||
object_interaction_.HandleCanvasMouseInput();
|
||
object_interaction_.CheckForObjectSelection();
|
||
object_interaction_.DrawSelectBox();
|
||
object_interaction_
|
||
.DrawSelectionHighlights(); // Draw object selection highlights
|
||
object_interaction_
|
||
.DrawEntitySelectionHighlights(); // Draw door/sprite/item selection
|
||
object_interaction_.DrawGhostPreview(); // Draw placement preview
|
||
// Context menu is handled by BeginCanvas via frame_opts.draw_context_menu
|
||
}
|
||
}
|
||
|
||
// Draw optional overlays on top of background bitmap
|
||
if (rooms_ && rom_->is_loaded()) {
|
||
auto& room = (*rooms_)[room_id];
|
||
|
||
// Draw the room layout first as the base layer
|
||
|
||
// VISUALIZATION: Draw object position rectangles (for debugging)
|
||
// This shows where objects are placed regardless of whether graphics render
|
||
if (show_object_bounds_) {
|
||
DrawObjectPositionOutlines(canvas_rt, room);
|
||
}
|
||
}
|
||
|
||
// Draw coordinate overlay when hovering over canvas
|
||
if (show_coordinate_overlay_ && canvas_.IsMouseHovering()) {
|
||
ImVec2 mouse_pos = ImGui::GetMousePos();
|
||
ImVec2 canvas_pos = canvas_.zero_point();
|
||
float scale = canvas_.global_scale();
|
||
if (scale <= 0.0f) scale = 1.0f;
|
||
|
||
// Calculate canvas-relative position
|
||
int canvas_x = static_cast<int>((mouse_pos.x - canvas_pos.x) / scale);
|
||
int canvas_y = static_cast<int>((mouse_pos.y - canvas_pos.y) / scale);
|
||
|
||
// Only show if within bounds
|
||
if (canvas_x >= 0 && canvas_x < kRoomPixelWidth &&
|
||
canvas_y >= 0 && canvas_y < kRoomPixelHeight) {
|
||
// Calculate tile coordinates
|
||
int tile_x = canvas_x / kDungeonTileSize;
|
||
int tile_y = canvas_y / kDungeonTileSize;
|
||
|
||
// Calculate camera/world coordinates (for minecart tracks, sprites, etc.)
|
||
auto [camera_x, camera_y] = dungeon_coords::TileToCameraCoords(room_id, tile_x, tile_y);
|
||
|
||
// Calculate sprite coordinates (16-pixel units)
|
||
int sprite_x = canvas_x / dungeon_coords::kSpriteTileSize;
|
||
int sprite_y = canvas_y / dungeon_coords::kSpriteTileSize;
|
||
|
||
// Draw coordinate info box at mouse position
|
||
ImVec2 overlay_pos = ImVec2(mouse_pos.x + 15, mouse_pos.y + 15);
|
||
|
||
// Build coordinate text
|
||
std::string coord_text = absl::StrFormat(
|
||
"Tile: (%d, %d)\n"
|
||
"Pixel: (%d, %d)\n"
|
||
"Camera: ($%04X, $%04X)\n"
|
||
"Sprite: (%d, %d)",
|
||
tile_x, tile_y,
|
||
canvas_x, canvas_y,
|
||
camera_x, camera_y,
|
||
sprite_x, sprite_y);
|
||
|
||
// Draw background box
|
||
ImVec2 text_size = ImGui::CalcTextSize(coord_text.c_str());
|
||
ImVec2 box_min = ImVec2(overlay_pos.x - 4, overlay_pos.y - 2);
|
||
ImVec2 box_max = ImVec2(overlay_pos.x + text_size.x + 8, overlay_pos.y + text_size.y + 4);
|
||
|
||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||
draw_list->AddRectFilled(box_min, box_max, IM_COL32(0, 0, 0, 200), 4.0f);
|
||
draw_list->AddRect(box_min, box_max, IM_COL32(100, 100, 100, 255), 4.0f);
|
||
draw_list->AddText(overlay_pos, IM_COL32(255, 255, 255, 255), coord_text.c_str());
|
||
}
|
||
}
|
||
|
||
// End canvas frame - this draws grid/overlay based on frame_opts
|
||
gui::EndCanvas(canvas_, canvas_rt, frame_opts);
|
||
}
|
||
|
||
void DungeonCanvasViewer::DisplayObjectInfo(const gui::CanvasRuntime& rt,
|
||
const zelda3::RoomObject& object,
|
||
int canvas_x, int canvas_y) {
|
||
// Display object information as text overlay with hex ID and name
|
||
std::string name = GetObjectName(object.id_);
|
||
std::string info_text;
|
||
if (object.id_ >= 0x100) {
|
||
info_text =
|
||
absl::StrFormat("0x%03X %s (X:%d Y:%d S:0x%02X)", object.id_,
|
||
name.c_str(), object.x_, object.y_, object.size_);
|
||
} else {
|
||
info_text =
|
||
absl::StrFormat("0x%02X %s (X:%d Y:%d S:0x%02X)", object.id_,
|
||
name.c_str(), object.x_, object.y_, object.size_);
|
||
}
|
||
|
||
// Draw text at the object position using runtime-based helper
|
||
gui::DrawText(rt, info_text, canvas_x, canvas_y - 12);
|
||
}
|
||
|
||
void DungeonCanvasViewer::RenderSprites(const gui::CanvasRuntime& rt,
|
||
const zelda3::Room& room) {
|
||
// Skip if sprites are not visible
|
||
if (!entity_visibility_.show_sprites) {
|
||
return;
|
||
}
|
||
|
||
const auto& theme = AgentUI::GetTheme();
|
||
|
||
// Render sprites as 16x16 colored squares with sprite name/ID
|
||
// NOTE: Sprite coordinates are in 16-pixel units (0-31 range = 512 pixels)
|
||
// unlike object coordinates which are in 8-pixel tile units
|
||
for (const auto& sprite : room.GetSprites()) {
|
||
// Sprites use 16-pixel coordinate system
|
||
int canvas_x = sprite.x() * 16;
|
||
int canvas_y = sprite.y() * 16;
|
||
|
||
if (IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
|
||
// Draw 16x16 square for sprite (like overworld entities)
|
||
ImVec4 sprite_color;
|
||
|
||
// Color-code sprites based on layer
|
||
if (sprite.layer() == 0) {
|
||
sprite_color = theme.dungeon_sprite_layer0; // Green for layer 0
|
||
} else {
|
||
sprite_color = theme.dungeon_sprite_layer1; // Blue for layer 1
|
||
}
|
||
|
||
// Draw filled square using runtime-based helper
|
||
gui::DrawRect(rt, canvas_x, canvas_y, 16, 16, sprite_color);
|
||
|
||
// Draw sprite ID and name using unified ResourceLabelProvider
|
||
std::string full_name = zelda3::GetSpriteLabel(sprite.id());
|
||
std::string sprite_text;
|
||
// Truncate long names for display
|
||
if (full_name.length() > 12) {
|
||
sprite_text = absl::StrFormat("%02X %s..", sprite.id(),
|
||
full_name.substr(0, 8).c_str());
|
||
} else {
|
||
sprite_text = absl::StrFormat("%02X %s", sprite.id(), full_name.c_str());
|
||
}
|
||
|
||
gui::DrawText(rt, sprite_text, canvas_x, canvas_y);
|
||
}
|
||
}
|
||
}
|
||
|
||
void DungeonCanvasViewer::RenderPotItems(const gui::CanvasRuntime& rt,
|
||
const zelda3::Room& room) {
|
||
// Skip if pot items are not visible
|
||
if (!entity_visibility_.show_pot_items) {
|
||
return;
|
||
}
|
||
|
||
const auto& pot_items = room.GetPotItems();
|
||
|
||
// If no pot items in this room, nothing to render
|
||
if (pot_items.empty()) {
|
||
return;
|
||
}
|
||
|
||
// Pot item names
|
||
static const char* kPotItemNames[] = {
|
||
"Nothing", // 0
|
||
"Green Rupee", // 1
|
||
"Rock", // 2
|
||
"Bee", // 3
|
||
"Health", // 4
|
||
"Bomb", // 5
|
||
"Heart", // 6
|
||
"Blue Rupee", // 7
|
||
"Key", // 8
|
||
"Arrow", // 9
|
||
"Bomb", // 10
|
||
"Heart", // 11
|
||
"Magic", // 12
|
||
"Full Magic", // 13
|
||
"Cucco", // 14
|
||
"Green Soldier", // 15
|
||
"Bush Stal", // 16
|
||
"Blue Soldier", // 17
|
||
"Landmine", // 18
|
||
"Heart", // 19
|
||
"Fairy", // 20
|
||
"Heart", // 21
|
||
"Nothing", // 22
|
||
"Hole", // 23
|
||
"Warp", // 24
|
||
"Staircase", // 25
|
||
"Bombable", // 26
|
||
"Switch" // 27
|
||
};
|
||
constexpr size_t kPotItemNameCount =
|
||
sizeof(kPotItemNames) / sizeof(kPotItemNames[0]);
|
||
|
||
// Pot items now have their own position data from ROM
|
||
// No need to match to objects - each item has exact pixel coordinates
|
||
for (const auto& pot_item : pot_items) {
|
||
// Get pixel coordinates from PotItem structure
|
||
int pixel_x = pot_item.GetPixelX();
|
||
int pixel_y = pot_item.GetPixelY();
|
||
|
||
// Convert to canvas coordinates (already in pixels, just need offset)
|
||
// Note: pot item coords are already in full room pixel space
|
||
auto [canvas_x, canvas_y] =
|
||
RoomToCanvasCoordinates(pixel_x / 8, pixel_y / 8);
|
||
|
||
if (IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
|
||
// Draw colored square
|
||
ImVec4 pot_item_color;
|
||
if (pot_item.item == 0) {
|
||
pot_item_color = ImVec4(0.5f, 0.5f, 0.5f, 0.5f); // Gray for Nothing
|
||
} else {
|
||
pot_item_color = ImVec4(1.0f, 0.85f, 0.2f, 0.75f); // Yellow for items
|
||
}
|
||
|
||
gui::DrawRect(rt, canvas_x, canvas_y, 16, 16, pot_item_color);
|
||
|
||
// Get item name
|
||
std::string item_name;
|
||
if (pot_item.item < kPotItemNameCount) {
|
||
item_name = kPotItemNames[pot_item.item];
|
||
} else {
|
||
item_name = absl::StrFormat("Unk%02X", pot_item.item);
|
||
}
|
||
|
||
// Draw label above the box
|
||
std::string item_text =
|
||
absl::StrFormat("%02X %s", pot_item.item, item_name.c_str());
|
||
gui::DrawText(rt, item_text, canvas_x, canvas_y - 10);
|
||
}
|
||
}
|
||
}
|
||
|
||
void DungeonCanvasViewer::RenderEntityOverlay(const gui::CanvasRuntime& rt,
|
||
const zelda3::Room& room) {
|
||
// Render all entity overlays using runtime-based helpers
|
||
RenderSprites(rt, room);
|
||
RenderPotItems(rt, room);
|
||
}
|
||
|
||
// Coordinate conversion helper functions
|
||
std::pair<int, int> DungeonCanvasViewer::RoomToCanvasCoordinates(
|
||
int room_x, int room_y) const {
|
||
// Convert room coordinates (tile units) to UNSCALED canvas pixel coordinates
|
||
// Dungeon tiles are 8x8 pixels (not 16x16!)
|
||
// IMPORTANT: Return UNSCALED coordinates - Canvas drawing functions apply
|
||
// scale internally Do NOT multiply by scale here or we get double-scaling!
|
||
|
||
// Simple conversion: tile units → pixel units (no scale, no offset)
|
||
return {room_x * 8, room_y * 8};
|
||
}
|
||
|
||
std::pair<int, int> DungeonCanvasViewer::CanvasToRoomCoordinates(
|
||
int canvas_x, int canvas_y) const {
|
||
// Convert canvas screen coordinates (pixels) to room coordinates (tile units)
|
||
// Input: Screen-space coordinates (affected by zoom/scale)
|
||
// Output: Logical tile coordinates (0-63 for each axis)
|
||
|
||
// IMPORTANT: Mouse coordinates are in screen space, must undo scale first
|
||
float scale = canvas_.global_scale();
|
||
if (scale <= 0.0f)
|
||
scale = 1.0f; // Prevent division by zero
|
||
|
||
// Step 1: Convert screen space → logical pixel space
|
||
int logical_x = static_cast<int>(canvas_x / scale);
|
||
int logical_y = static_cast<int>(canvas_y / scale);
|
||
|
||
// Step 2: Convert logical pixels → tile units (8 pixels per tile)
|
||
return {logical_x / 8, logical_y / 8};
|
||
}
|
||
|
||
bool DungeonCanvasViewer::IsWithinCanvasBounds(int canvas_x, int canvas_y,
|
||
int margin) const {
|
||
// Check if coordinates are within canvas bounds with optional margin
|
||
auto canvas_width = canvas_.width();
|
||
auto canvas_height = canvas_.height();
|
||
return (canvas_x >= -margin && canvas_y >= -margin &&
|
||
canvas_x <= canvas_width + margin &&
|
||
canvas_y <= canvas_height + margin);
|
||
}
|
||
// Room layout visualization
|
||
|
||
// Object visualization methods
|
||
void DungeonCanvasViewer::DrawObjectPositionOutlines(
|
||
const gui::CanvasRuntime& rt, const zelda3::Room& room) {
|
||
// Draw colored rectangles showing object positions
|
||
// This helps visualize object placement even if graphics don't render
|
||
// correctly
|
||
|
||
const auto& theme = AgentUI::GetTheme();
|
||
const auto& objects = room.GetTileObjects();
|
||
|
||
// Create ObjectDrawer for accurate dimension calculation
|
||
// ObjectDrawer uses game-accurate draw routine mapping to determine sizes
|
||
// Note: const_cast needed because rom() accessor is non-const, but we don't
|
||
// modify ROM
|
||
zelda3::ObjectDrawer drawer(const_cast<zelda3::Room&>(room).rom(), room.id(),
|
||
nullptr);
|
||
|
||
for (const auto& obj : objects) {
|
||
// Filter by object type (default to true if unknown type)
|
||
bool show_this_type = true; // Default to showing
|
||
if (obj.id_ < 0x100) {
|
||
show_this_type = object_outline_toggles_.show_type1_objects;
|
||
} else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
|
||
show_this_type = object_outline_toggles_.show_type2_objects;
|
||
} else if (obj.id_ >= 0xF00) {
|
||
show_this_type = object_outline_toggles_.show_type3_objects;
|
||
}
|
||
// else: unknown type, use default (true)
|
||
|
||
// Filter by layer (default to true if unknown layer)
|
||
bool show_this_layer = true; // Default to showing
|
||
if (obj.GetLayerValue() == 0) {
|
||
show_this_layer = object_outline_toggles_.show_layer0_objects;
|
||
} else if (obj.GetLayerValue() == 1) {
|
||
show_this_layer = object_outline_toggles_.show_layer1_objects;
|
||
} else if (obj.GetLayerValue() == 2) {
|
||
show_this_layer = object_outline_toggles_.show_layer2_objects;
|
||
}
|
||
// else: unknown layer, use default (true)
|
||
|
||
// Skip if filtered out
|
||
if (!show_this_type || !show_this_layer) {
|
||
continue;
|
||
}
|
||
|
||
// Convert object position (tile coordinates) to canvas pixel coordinates
|
||
// (UNSCALED)
|
||
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
|
||
|
||
// Calculate object dimensions using the shared dimension table when loaded
|
||
int width = 16;
|
||
int height = 16;
|
||
auto& dim_table = zelda3::ObjectDimensionTable::Get();
|
||
if (dim_table.IsLoaded()) {
|
||
auto [w_tiles, h_tiles] = dim_table.GetDimensions(obj.id_, obj.size_);
|
||
width = w_tiles * 8;
|
||
height = h_tiles * 8;
|
||
} else {
|
||
auto [w, h] = drawer.CalculateObjectDimensions(obj);
|
||
width = w;
|
||
height = h;
|
||
}
|
||
|
||
// IMPORTANT: Do NOT apply canvas scale here - DrawRect handles it
|
||
// Clamp to reasonable sizes (in logical space)
|
||
width = std::min(width, 512);
|
||
height = std::min(height, 512);
|
||
|
||
// Color-code by layer
|
||
ImVec4 outline_color;
|
||
if (obj.GetLayerValue() == 0) {
|
||
outline_color = theme.dungeon_outline_layer0; // Red for layer 0
|
||
} else if (obj.GetLayerValue() == 1) {
|
||
outline_color = theme.dungeon_outline_layer1; // Green for layer 1
|
||
} else {
|
||
outline_color = theme.dungeon_outline_layer2; // Blue for layer 2
|
||
}
|
||
|
||
// Draw outline rectangle using runtime-based helper
|
||
gui::DrawRect(rt, canvas_x, canvas_y, width, height, outline_color);
|
||
|
||
// Draw object ID label with hex ID and abbreviated name
|
||
// Format: "0xNN Name" where name is truncated if needed
|
||
std::string name = GetObjectName(obj.id_);
|
||
// Truncate name to fit (approx 12 chars for small objects)
|
||
if (name.length() > 12) {
|
||
name = name.substr(0, 10) + "..";
|
||
}
|
||
std::string label;
|
||
if (obj.id_ >= 0x100) {
|
||
label = absl::StrFormat("0x%03X\n%s\n[%dx%d]", obj.id_, name.c_str(),
|
||
width, height);
|
||
} else {
|
||
label = absl::StrFormat("0x%02X\n%s\n[%dx%d]", obj.id_, name.c_str(),
|
||
width, height);
|
||
}
|
||
gui::DrawText(rt, label, canvas_x + 1, canvas_y + 1);
|
||
}
|
||
}
|
||
|
||
// Room graphics management methods
|
||
absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(int room_id) {
|
||
LOG_DEBUG("[LoadAndRender]", "START room_id=%d", room_id);
|
||
|
||
if (room_id < 0 || room_id >= 128) {
|
||
LOG_DEBUG("[LoadAndRender]", "ERROR: Invalid room ID");
|
||
return absl::InvalidArgumentError("Invalid room ID");
|
||
}
|
||
|
||
if (!rom_ || !rom_->is_loaded()) {
|
||
LOG_DEBUG("[LoadAndRender]", "ERROR: ROM not loaded");
|
||
return absl::FailedPreconditionError("ROM not loaded");
|
||
}
|
||
|
||
if (!rooms_) {
|
||
LOG_DEBUG("[LoadAndRender]", "ERROR: Room data not available");
|
||
return absl::FailedPreconditionError("Room data not available");
|
||
}
|
||
|
||
auto& room = (*rooms_)[room_id];
|
||
LOG_DEBUG("[LoadAndRender]", "Got room reference");
|
||
|
||
// Load room graphics with proper blockset
|
||
LOG_DEBUG("[LoadAndRender]", "Loading graphics for blockset %d",
|
||
room.blockset);
|
||
room.LoadRoomGraphics(room.blockset);
|
||
LOG_DEBUG("[LoadAndRender]", "Graphics loaded");
|
||
|
||
// Load the room's palette with bounds checking
|
||
if (!game_data_) {
|
||
LOG_ERROR("[LoadAndRender]", "GameData not available");
|
||
return absl::FailedPreconditionError("GameData not available");
|
||
}
|
||
const auto& dungeon_main = game_data_->palette_groups.dungeon_main;
|
||
if (!dungeon_main.empty()) {
|
||
int palette_id = room.palette;
|
||
if (room.palette < game_data_->paletteset_ids.size()) {
|
||
palette_id = game_data_->paletteset_ids[room.palette][0];
|
||
}
|
||
current_palette_group_id_ =
|
||
std::min<uint64_t>(std::max(0, palette_id),
|
||
static_cast<int>(dungeon_main.size() - 1));
|
||
|
||
auto full_palette = dungeon_main[current_palette_group_id_];
|
||
ASSIGN_OR_RETURN(
|
||
current_palette_group_,
|
||
gfx::CreatePaletteGroupFromLargePalette(full_palette, 16));
|
||
LOG_DEBUG("[LoadAndRender]", "Palette loaded: group_id=%zu",
|
||
current_palette_group_id_);
|
||
}
|
||
|
||
// Render the room graphics (self-contained - handles all palette application)
|
||
LOG_DEBUG("[LoadAndRender]", "Calling room.RenderRoomGraphics()...");
|
||
room.RenderRoomGraphics();
|
||
LOG_DEBUG("[LoadAndRender]",
|
||
"RenderRoomGraphics() complete - room buffers self-contained");
|
||
|
||
LOG_DEBUG("[LoadAndRender]", "SUCCESS");
|
||
return absl::OkStatus();
|
||
}
|
||
|
||
void DungeonCanvasViewer::DrawRoomBackgroundLayers(int room_id) {
|
||
if (room_id < 0 || room_id >= zelda3::NumberOfRooms || !rooms_)
|
||
return;
|
||
|
||
auto& room = (*rooms_)[room_id];
|
||
auto& layer_mgr = GetRoomLayerManager(room_id);
|
||
|
||
// Apply room's layer merging settings to the manager
|
||
layer_mgr.ApplyLayerMerging(room.layer_merging());
|
||
|
||
float scale = canvas_.global_scale();
|
||
|
||
// Always use composite mode: single merged bitmap with back-to-front layer order
|
||
// This matches SNES hardware behavior where BG2 is drawn first, then BG1 on top
|
||
auto& composite = room.GetCompositeBitmap(layer_mgr);
|
||
if (composite.is_active() && composite.width() > 0) {
|
||
// Ensure texture exists or is updated when bitmap data changes
|
||
if (!composite.texture()) {
|
||
gfx::Arena::Get().QueueTextureCommand(
|
||
gfx::Arena::TextureCommandType::CREATE, &composite);
|
||
composite.set_modified(false);
|
||
} else if (composite.modified()) {
|
||
// Update texture when bitmap was regenerated
|
||
gfx::Arena::Get().QueueTextureCommand(
|
||
gfx::Arena::TextureCommandType::UPDATE, &composite);
|
||
composite.set_modified(false);
|
||
}
|
||
if (composite.texture()) {
|
||
canvas_.DrawBitmap(composite, 0, 0, scale, 255);
|
||
}
|
||
}
|
||
}
|
||
|
||
void DungeonCanvasViewer::DrawMaskHighlights(const gui::CanvasRuntime& rt,
|
||
const zelda3::Room& room) {
|
||
// Draw semi-transparent blue overlay on BG2/Layer 1 objects when mask mode
|
||
// is active. This helps identify which objects are the "overlay" content
|
||
// (platforms, statues, stairs) that create transparency holes in BG1.
|
||
const auto& objects = room.GetTileObjects();
|
||
|
||
// Create ObjectDrawer for dimension calculation
|
||
zelda3::ObjectDrawer drawer(const_cast<zelda3::Room&>(room).rom(), room.id(),
|
||
nullptr);
|
||
|
||
// Mask highlight color: semi-transparent cyan/blue
|
||
// DrawRect draws a filled rectangle with a black outline
|
||
ImVec4 mask_color(0.2f, 0.6f, 1.0f, 0.4f); // Light blue, 40% opacity
|
||
|
||
for (const auto& obj : objects) {
|
||
// Only highlight Layer 1 (BG2) objects - these are the mask/overlay objects
|
||
if (obj.GetLayerValue() != 1) {
|
||
continue;
|
||
}
|
||
|
||
// Convert object position to canvas coordinates
|
||
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
|
||
|
||
// Calculate object dimensions
|
||
int width = 16;
|
||
int height = 16;
|
||
auto& dim_table = zelda3::ObjectDimensionTable::Get();
|
||
if (dim_table.IsLoaded()) {
|
||
auto [w_tiles, h_tiles] = dim_table.GetDimensions(obj.id_, obj.size_);
|
||
width = w_tiles * 8;
|
||
height = h_tiles * 8;
|
||
} else {
|
||
auto [w, h] = drawer.CalculateObjectDimensions(obj);
|
||
width = w;
|
||
height = h;
|
||
}
|
||
|
||
// Clamp to reasonable sizes
|
||
width = std::min(width, 512);
|
||
height = std::min(height, 512);
|
||
|
||
// Draw filled rectangle with semi-transparent overlay (includes black outline)
|
||
gui::DrawRect(rt, canvas_x, canvas_y, width, height, mask_color);
|
||
}
|
||
}
|
||
|
||
} // namespace yaze::editor
|