yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_viewer.cc
Go to the documentation of this file.
1#include <algorithm>
2#include <cfloat>
3#include <cstddef>
4#include <cstdint>
5#include <cstdio>
6#include <optional>
7#include <string>
8#include <utility>
9
10#include "absl/strings/str_format.h"
14#include "app/gui/core/input.h"
16#include "dungeon_coordinates.h"
17#include "canvas/canvas_menu.h"
18#include "core/icons.h"
19#include "absl/status/status.h"
21#include "imgui/imgui.h"
22#include "rom/rom.h"
23#include "util/log.h"
24#include "util/macro.h"
27#include "zelda3/dungeon/room.h"
32
33namespace yaze::editor {
34
35namespace {
36
37constexpr int kRoomMatrixCols = 16;
38constexpr int kRoomMatrixRows = 19;
39constexpr int kRoomPropertyColumns = 2;
40
41} // namespace
42
43// Use shared GetObjectName() from zelda3/dungeon/room_object.h
46
47void DungeonCanvasViewer::Draw(int room_id) {
48 DrawDungeonCanvas(room_id);
49}
50
52 // Validate room_id and ROM
53 if (room_id < 0 || room_id >= 0x128) {
54 ImGui::Text("Invalid room ID: %d", room_id);
55 return;
56 }
57
58 if (!rom_ || !rom_->is_loaded()) {
59 ImGui::Text("ROM not loaded");
60 return;
61 }
62
63 // Handle pending scroll request
64 if (pending_scroll_target_.has_value()) {
65 auto [target_x, target_y] = pending_scroll_target_.value();
66
67 // Convert tile coordinates to pixels
68 float scale = canvas_.global_scale();
69 if (scale <= 0.0f)
70 scale = 1.0f;
71
72 float pixel_x = target_x * 8 * scale;
73 float pixel_y = target_y * 8 * scale;
74
75 // Center in view
76 ImVec2 view_size = ImGui::GetWindowSize();
77 float scroll_x = pixel_x - (view_size.x * 0.5f);
78 float scroll_y = pixel_y - (view_size.y * 0.5f);
79
80 // Account for canvas position offset if possible, but roughly centering is
81 // usually enough Ideally we'd add the cursor position y-offset to scroll_y
82 // to account for the UI above canvas but GetCursorPosY() might not be
83 // accurate before content is laid out. For X, canvas usually starts at
84 // left, so it's fine.
85
86 ImGui::SetScrollX(scroll_x);
87 ImGui::SetScrollY(scroll_y);
88
90 }
91
92 ImGui::BeginGroup();
93
94 // CRITICAL: Canvas coordinate system for dungeons
95 // The canvas system uses a two-stage scaling model:
96 // 1. Canvas size: UNSCALED content dimensions (512x512 for dungeon rooms)
97 // 2. Viewport size: canvas_size * global_scale (handles zoom)
98 // 3. Grid lines: grid_step * global_scale (auto-scales with zoom)
99 // 4. Bitmaps: drawn with scale = global_scale (matches viewport)
100 constexpr int kRoomPixelWidth = 512; // 64 tiles * 8 pixels (UNSCALED)
101 constexpr int kRoomPixelHeight = 512;
102 constexpr int kDungeonTileSize = 8; // Dungeon tiles are 8x8 pixels
103
104 // Configure canvas frame options for the new BeginCanvas/EndCanvas pattern
105 gui::CanvasFrameOptions frame_opts;
106 frame_opts.canvas_size = ImVec2(kRoomPixelWidth, kRoomPixelHeight);
107 frame_opts.draw_grid = show_grid_;
108 frame_opts.grid_step = static_cast<float>(custom_grid_size_);
109 frame_opts.draw_context_menu = true;
110 frame_opts.draw_overlay = true;
111 frame_opts.render_popups = true;
112
113 // Legacy configuration for context menu and interaction systems
114 canvas_.SetShowBuiltinContextMenu(false); // Hide default canvas debug items
115
116 // DEBUG: Log canvas configuration
117 static int debug_frame_count = 0;
118 if (debug_frame_count++ % 60 == 0) { // Log once per second (assuming 60fps)
119 LOG_DEBUG("[DungeonCanvas]",
120 "Canvas config: size=(%.0f,%.0f) scale=%.2f grid=%.0f",
123 LOG_DEBUG(
124 "[DungeonCanvas]", "Canvas viewport: p0=(%.0f,%.0f) p1=(%.0f,%.0f)",
128 }
129
130 if (rooms_) {
131 auto& room = (*rooms_)[room_id];
132
133 // Check if critical properties changed and trigger reload
134 if (prev_blockset_ != room.blockset || prev_palette_ != room.palette ||
135 prev_layout_ != room.layout || prev_spriteset_ != room.spriteset) {
136 // Only reload if ROM is properly loaded
137 if (room.rom() && room.rom()->is_loaded()) {
138 // Force reload of room graphics
139 // Room buffers are now self-contained - no need for separate palette
140 // operations
141 room.LoadRoomGraphics(room.blockset);
142 room.RenderRoomGraphics(); // Applies palettes internally
143 }
144
145 prev_blockset_ = room.blockset;
146 prev_palette_ = room.palette;
147 prev_layout_ = room.layout;
148 prev_spriteset_ = room.spriteset;
149 }
150 ImGui::Separator();
151
152 auto draw_navigation = [&]() {
153 // Use swap callback (swaps room in current panel) if available,
154 // otherwise fall back to navigation callback (opens new panel)
156 return;
157
158 const int col = room_id % kRoomMatrixCols;
159 const int row = room_id / kRoomMatrixCols;
160
161 auto room_if_valid = [](int candidate) -> std::optional<int> {
162 if (candidate < 0 || candidate >= zelda3::NumberOfRooms)
163 return std::nullopt;
164 return candidate;
165 };
166
167 const auto north =
168 room_if_valid(row > 0 ? room_id - kRoomMatrixCols : -1);
169 const auto south = room_if_valid(
170 row < kRoomMatrixRows - 1 ? room_id + kRoomMatrixCols : -1);
171 const auto west = room_if_valid(col > 0 ? room_id - 1 : -1);
172 const auto east =
173 room_if_valid(col < kRoomMatrixCols - 1 ? room_id + 1 : -1);
174
175 auto nav_button = [&](const char* id, ImGuiDir dir,
176 const std::optional<int>& target,
177 const char* tooltip) {
178 const bool enabled = target.has_value();
179 if (!enabled)
180 ImGui::BeginDisabled();
181 if (ImGui::ArrowButton(id, dir) && enabled) {
182 // Prefer swap callback (swaps room in current panel)
184 room_swap_callback_(room_id, *target);
185 } else if (room_navigation_callback_) {
187 }
188 }
189 if (!enabled)
190 ImGui::EndDisabled();
191 if (enabled && ImGui::IsItemHovered() && tooltip && tooltip[0] != '\0')
192 ImGui::SetTooltip("%s", tooltip);
193 };
194
195 ImGui::BeginGroup();
196 nav_button("RoomNavNorth", ImGuiDir_Up, north, "Go to room above");
197 ImGui::SameLine();
198 nav_button("RoomNavSouth", ImGuiDir_Down, south, "Go to room below");
199 ImGui::SameLine();
200 nav_button("RoomNavWest", ImGuiDir_Left, west, "Previous room");
201 ImGui::SameLine();
202 nav_button("RoomNavEast", ImGuiDir_Right, east, "Next room");
203 ImGui::EndGroup();
204 ImGui::SameLine();
205 };
206
207 auto& layer_mgr = GetRoomLayerManager(room_id);
208 // TODO(zelda3-hacking-expert): The SNES path allows BG merge flags and
209 // layer types to coexist (four object streams with BothBG routines); make
210 // sure UI toggles here don’t enforce mutual exclusivity. See
211 // docs/internal/agents/dungeon-object-rendering-spec.md for the expected
212 // layering/merge semantics from bank_01.asm.
213 layer_mgr.ApplyLayerMerging(room.layer_merging());
214
215 uint8_t blockset_val = room.blockset;
216 uint8_t spriteset_val = room.spriteset;
217 uint8_t palette_val = room.palette;
218 uint8_t floor1_val = room.floor1();
219 uint8_t floor2_val = room.floor2();
220 int effect_val = static_cast<int>(room.effect());
221 int tag1_val = static_cast<int>(room.tag1());
222 int tag2_val = static_cast<int>(room.tag2());
223 uint8_t layout_val = room.layout;
224
225 // Effect names matching RoomEffect array in room.cc (8 entries, 0-7)
226 const char* effect_names[] = {
227 "Nothing", // 0
228 "Nothing (1)", // 1 - unused but exists in ROM
229 "Moving Floor", // 2
230 "Moving Water", // 3
231 "Trinexx Shell", // 4
232 "Red Flashes", // 5
233 "Light Torch to See", // 6
234 "Ganon's Darkness" // 7
235 };
236
237 // Tag names matching RoomTag array in room.cc
238 const char* tag_names[] = {
239 "Nothing", // 0
240 "NW Kill Enemy to Open", // 1
241 "NE Kill Enemy to Open", // 2
242 "SW Kill Enemy to Open", // 3
243 "SE Kill Enemy to Open", // 4
244 "W Kill Enemy to Open", // 5
245 "E Kill Enemy to Open", // 6
246 "N Kill Enemy to Open", // 7
247 "S Kill Enemy to Open", // 8
248 "Clear Quadrant to Open", // 9
249 "Clear Full Tile to Open", // 10
250 "NW Push Block to Open", // 11
251 "NE Push Block to Open", // 12
252 "SW Push Block to Open", // 13
253 "SE Push Block to Open", // 14
254 "W Push Block to Open", // 15
255 "E Push Block to Open", // 16
256 "N Push Block to Open", // 17
257 "S Push Block to Open", // 18
258 "Push Block to Open", // 19
259 "Pull Lever to Open", // 20
260 "Collect Prize to Open", // 21
261 "Hold Switch Open Door", // 22
262 "Toggle Switch to Open", // 23
263 "Turn off Water", // 24
264 "Turn on Water", // 25
265 "Water Gate", // 26
266 "Water Twin", // 27
267 "Moving Wall Right", // 28
268 "Moving Wall Left", // 29
269 "Crash (30)", // 30
270 "Crash (31)", // 31
271 "Push Switch Exploding Wall", // 32
272 "Holes 0", // 33
273 "Open Chest (Holes 0)", // 34
274 "Holes 1", // 35
275 "Holes 2", // 36
276 "Defeat Boss for Prize", // 37
277 "SE Kill Enemy Push Block", // 38
278 "Trigger Switch Chest", // 39
279 "Pull Lever Exploding Wall", // 40
280 "NW Kill Enemy for Chest", // 41
281 "NE Kill Enemy for Chest", // 42
282 "SW Kill Enemy for Chest", // 43
283 "SE Kill Enemy for Chest", // 44
284 "W Kill Enemy for Chest", // 45
285 "E Kill Enemy for Chest", // 46
286 "N Kill Enemy for Chest", // 47
287 "S Kill Enemy for Chest", // 48
288 "Clear Quadrant for Chest", // 49
289 "Clear Full Tile for Chest", // 50
290 "Light Torches to Open", // 51
291 "Holes 3", // 52
292 "Holes 4", // 53
293 "Holes 5", // 54
294 "Holes 6", // 55
295 "Agahnim Room", // 56
296 "Holes 7", // 57
297 "Holes 8", // 58
298 "Open Chest for Holes 8", // 59
299 "Push Block for Chest", // 60
300 "Clear Room for Triforce", // 61
301 "Light Torches for Chest", // 62
302 "Kill Boss Again", // 63
303 "64 (Unused)" // 64
304 };
305 constexpr int kNumTags = IM_ARRAYSIZE(tag_names);
306
307 const char* merge_types[] = {"Off", "Parallax", "Dark",
308 "On top", "Translucent", "Addition",
309 "Normal", "Transparent", "Dark room"};
310 const char* blend_modes[] = {"Normal", "Trans", "Add", "Dark", "Off"};
311
312 // ========================================================================
313 // ROOM PROPERTIES TABLE - Compact layout for docking
314 // ========================================================================
315 if (ImGui::BeginTable("##RoomPropsTable", 2, ImGuiTableFlags_SizingStretchProp)) {
316 ImGui::TableSetupColumn("NavCol", ImGuiTableColumnFlags_WidthFixed, 120);
317 ImGui::TableSetupColumn("PropsCol", ImGuiTableColumnFlags_WidthStretch);
318
319 // Row 1: Navigation + Room ID + Hex inputs
320 ImGui::TableNextRow();
321 ImGui::TableNextColumn();
322 draw_navigation();
323 ImGui::TableNextColumn();
324 // Room ID and hex property inputs with icons
325 ImGui::Text(ICON_MD_TUNE " %03X", room_id);
326 ImGui::SameLine();
327 ImGui::TextDisabled(ICON_MD_VIEW_MODULE);
328 ImGui::SameLine(0, 2);
329 // Blockset: max 81 (kNumRoomBlocksets = 82)
330 if (auto res = gui::InputHexByteEx("##Blockset", &blockset_val, 81, 32.f, true);
331 res.ShouldApply()) {
332 room.SetBlockset(blockset_val);
333 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
334 }
335 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Blockset (0-51)");
336 ImGui::SameLine();
337 ImGui::TextDisabled(ICON_MD_PALETTE);
338 ImGui::SameLine(0, 2);
339 // Palette: max 71 (kNumPalettesets = 72)
340 if (auto res = gui::InputHexByteEx("##Palette", &palette_val, 71, 32.f, true);
341 res.ShouldApply()) {
342 room.SetPalette(palette_val);
343 SetCurrentPaletteId(palette_val);
344 if (game_data_ && rom_) {
345 if (palette_val < game_data_->paletteset_ids.size() &&
346 !game_data_->paletteset_ids[palette_val].empty()) {
347 auto palette_ptr = game_data_->paletteset_ids[palette_val][0];
348 if (auto palette_id_res = rom_->ReadWord(0xDEC4B + palette_ptr);
349 palette_id_res.ok()) {
350 current_palette_group_id_ = palette_id_res.value() / 180;
353 auto full_palette =
356 if (auto res =
358 res.ok()) {
359 current_palette_group_ = res.value();
360 }
361 }
362 }
363 }
364 }
365 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
366 }
367 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Palette (0-47)");
368 ImGui::SameLine();
369 ImGui::TextDisabled(ICON_MD_GRID_VIEW);
370 ImGui::SameLine(0, 2);
371 // Layout: 8 valid layouts (0-7)
372 if (auto res = gui::InputHexByteEx("##Layout", &layout_val, 7, 32.f, true);
373 res.ShouldApply()) {
374 room.layout = layout_val;
375 room.MarkLayoutDirty();
376 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
377 }
378 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Layout (0-7)");
379 ImGui::SameLine();
380 ImGui::TextDisabled(ICON_MD_PEST_CONTROL);
381 ImGui::SameLine(0, 2);
382 // Spriteset: max 143 (kNumSpritesets = 144)
383 if (auto res = gui::InputHexByteEx("##Spriteset", &spriteset_val, 143, 32.f, true);
384 res.ShouldApply()) {
385 room.SetSpriteset(spriteset_val);
386 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
387 }
388 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Spriteset (0-8F)");
389 ImGui::SameLine();
390 ImGui::TextDisabled(ICON_MD_SQUARE);
391 ImGui::SameLine(0, 2);
392 // Floor graphics: max 15 (4-bit value, 0-F)
393 if (auto res = gui::InputHexByteEx("##Floor1", &floor1_val, 15, 32.f, true);
394 res.ShouldApply()) {
395 room.set_floor1(floor1_val);
396 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
397 }
398 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Floor 1 (0-F)");
399 ImGui::SameLine();
400 ImGui::TextDisabled(ICON_MD_SQUARE_FOOT);
401 ImGui::SameLine(0, 2);
402 // Floor graphics: max 15 (4-bit value, 0-F)
403 if (auto res = gui::InputHexByteEx("##Floor2", &floor2_val, 15, 32.f, true);
404 res.ShouldApply()) {
405 room.set_floor2(floor2_val);
406 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
407 }
408 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Floor 2 (0-F)");
409
410 // Row 2: Effect + Tags (all in one flow, not separated by table columns)
411 ImGui::TableNextRow();
412 ImGui::TableNextColumn();
413 ImGui::TextDisabled(ICON_MD_AUTO_AWESOME " FX/Tags");
414 ImGui::TableNextColumn();
415 constexpr int kNumEffects = IM_ARRAYSIZE(effect_names);
416 if (effect_val < 0) effect_val = 0;
417 if (effect_val >= kNumEffects) effect_val = kNumEffects - 1;
418 ImGui::SetNextItemWidth(150);
419 if (ImGui::BeginCombo("##Effect", effect_names[effect_val])) {
420 for (int i = 0; i < kNumEffects; i++) {
421 if (ImGui::Selectable(effect_names[i], effect_val == i)) {
422 room.SetEffect(static_cast<zelda3::EffectKey>(i));
423 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
424 }
425 }
426 ImGui::EndCombo();
427 }
428 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Effect");
429 ImGui::SameLine();
430 ImGui::TextDisabled(ICON_MD_LABEL);
431 ImGui::SameLine(0, 2);
432 int tag1_idx = std::clamp(tag1_val, 0, kNumTags - 1);
433 ImGui::SetNextItemWidth(180);
434 if (ImGui::BeginCombo("##Tag1", tag_names[tag1_idx])) {
435 for (int i = 0; i < kNumTags; i++) {
436 if (ImGui::Selectable(tag_names[i], tag1_idx == i)) {
437 room.SetTag1(static_cast<zelda3::TagKey>(i));
438 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
439 }
440 }
441 ImGui::EndCombo();
442 }
443 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Tag 1");
444 ImGui::SameLine();
445 ImGui::TextDisabled(ICON_MD_LABEL_OUTLINE);
446 ImGui::SameLine(0, 2);
447 int tag2_idx = std::clamp(tag2_val, 0, kNumTags - 1);
448 ImGui::SetNextItemWidth(180);
449 if (ImGui::BeginCombo("##Tag2", tag_names[tag2_idx])) {
450 for (int i = 0; i < kNumTags; i++) {
451 if (ImGui::Selectable(tag_names[i], tag2_idx == i)) {
452 room.SetTag2(static_cast<zelda3::TagKey>(i));
453 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
454 }
455 }
456 ImGui::EndCombo();
457 }
458 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Tag 2");
459
460 // Row 3: Layer visibility + Blend/Merge
461 ImGui::TableNextRow();
462 ImGui::TableNextColumn();
463 ImGui::TextDisabled(ICON_MD_LAYERS " Layers");
464 ImGui::TableNextColumn();
465 bool bg1_layout = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout);
466 bool bg1_objects = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects);
467 bool bg2_layout = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout);
468 bool bg2_objects = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects);
469
470 // Helper to mark layer bitmaps as needing texture update
471 auto mark_layers_dirty = [&]() {
472 if (rooms_) {
473 auto& r = (*rooms_)[room_id];
474 r.bg1_buffer().bitmap().set_modified(true);
475 r.bg2_buffer().bitmap().set_modified(true);
476 r.object_bg1_buffer().bitmap().set_modified(true);
477 r.object_bg2_buffer().bitmap().set_modified(true);
478 r.MarkCompositeDirty();
479 }
480 };
481
482 if (ImGui::Checkbox("BG1##L", &bg1_layout)) {
483 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout, bg1_layout);
484 mark_layers_dirty();
485 }
486 if (ImGui::IsItemHovered()) {
487 ImGui::SetTooltip("BG1 Layout: Main floor tiles (rendered on top of BG2)");
488 }
489 ImGui::SameLine();
490 if (ImGui::Checkbox("O1##O", &bg1_objects)) {
491 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Objects, bg1_objects);
492 mark_layers_dirty();
493 }
494 if (ImGui::IsItemHovered()) {
495 ImGui::SetTooltip("BG1 Objects: Walls, pots, interactive objects (topmost layer)");
496 }
497 ImGui::SameLine();
498 if (ImGui::Checkbox("BG2##L2", &bg2_layout)) {
499 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout, bg2_layout);
500 mark_layers_dirty();
501 }
502 if (ImGui::IsItemHovered()) {
503 ImGui::SetTooltip("BG2 Layout: Background floor patterns (behind BG1)");
504 }
505 ImGui::SameLine();
506 if (ImGui::Checkbox("O2##O2", &bg2_objects)) {
507 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Objects, bg2_objects);
508 mark_layers_dirty();
509 }
510 if (ImGui::IsItemHovered()) {
511 ImGui::SetTooltip("BG2 Objects: Background details (behind BG1)");
512 }
513 ImGui::SameLine();
514 ImGui::SetNextItemWidth(60);
515 int bg2_blend = static_cast<int>(
516 layer_mgr.GetLayerBlendMode(zelda3::LayerType::BG2_Layout));
517 if (ImGui::Combo("##Bld", &bg2_blend, blend_modes, IM_ARRAYSIZE(blend_modes))) {
518 auto mode = static_cast<zelda3::LayerBlendMode>(bg2_blend);
519 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Layout, mode);
520 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Objects, mode);
521 }
522 if (ImGui::IsItemHovered()) {
523 ImGui::SetTooltip(
524 "BG2 Blend Mode (color math effect):\n"
525 "- Normal: Opaque pixels\n"
526 "- Translucent: 50% alpha\n"
527 "- Addition: Additive blending\n"
528 "Does not change layer order (BG1 always on top)");
529 }
530 ImGui::SameLine();
531 ImGui::SetNextItemWidth(70);
532 int merge_val = room.layer_merging().ID;
533 if (ImGui::Combo("##Mrg", &merge_val, merge_types, IM_ARRAYSIZE(merge_types))) {
534 room.SetLayerMerging(zelda3::kLayerMergeTypeList[merge_val]);
535 layer_mgr.ApplyLayerMergingPreserveVisibility(room.layer_merging());
536 if (room.rom() && room.rom()->is_loaded()) room.RenderRoomGraphics();
537 }
538 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Merge type");
539
540 // Row 4: Selection filter
541 ImGui::TableNextRow();
542 ImGui::TableNextColumn();
543 ImGui::TextDisabled(ICON_MD_SELECT_ALL " Select");
544 ImGui::TableNextColumn();
545 object_interaction_.SetLayersMerged(layer_mgr.AreLayersMerged());
546 int current_filter = object_interaction_.GetLayerFilter();
547 if (ImGui::RadioButton("All", current_filter == ObjectSelection::kLayerAll))
549 ImGui::SameLine();
550 if (ImGui::RadioButton("L1", current_filter == ObjectSelection::kLayer1))
552 ImGui::SameLine();
553 if (ImGui::RadioButton("L2", current_filter == ObjectSelection::kLayer2))
555 ImGui::SameLine();
556 if (ImGui::RadioButton("L3", current_filter == ObjectSelection::kLayer3))
558 ImGui::SameLine();
559 // Mask mode: filter to BG2/Layer 1 overlay objects only (platforms, statues, etc.)
560 bool is_mask_mode = current_filter == ObjectSelection::kMaskLayer;
561 if (is_mask_mode) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 1.0f, 1.0f));
562 if (ImGui::RadioButton("Mask", is_mask_mode))
564 if (is_mask_mode) ImGui::PopStyleColor();
565 if (ImGui::IsItemHovered()) {
566 ImGui::SetTooltip(
567 "Mask Selection Mode\n"
568 "Only select BG2/Layer 1 overlay objects (platforms, statues, stairs)\n"
569 "These are the objects that create transparency holes in BG1");
570 }
571 if (object_interaction_.IsLayerFilterActive() && !is_mask_mode) {
572 ImGui::SameLine();
573 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), ICON_MD_FILTER_ALT);
574 }
575 if (layer_mgr.AreLayersMerged()) {
576 ImGui::SameLine();
577 ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), ICON_MD_MERGE_TYPE);
578 }
579
580 ImGui::EndTable();
581 }
582
583 // === Quick Access Toolbar for Entity Pickers ===
584 ImGui::Spacing();
585 ImGui::BeginGroup();
586 ImGui::TextDisabled(ICON_MD_ADD_CIRCLE " Place:");
587 ImGui::SameLine();
588
589 // Object picker button
590 if (ImGui::Button(ICON_MD_WIDGETS " Object")) {
593 }
594 }
595 if (ImGui::IsItemHovered()) {
596 ImGui::SetTooltip("Open Object Editor panel to select objects for placement");
597 }
598 ImGui::SameLine();
599
600 // Sprite picker button
601 if (ImGui::Button(ICON_MD_PERSON " Sprite")) {
604 }
605 }
606 if (ImGui::IsItemHovered()) {
607 ImGui::SetTooltip("Open Sprite Editor panel to select sprites for placement");
608 }
609 ImGui::SameLine();
610
611 // Item picker button
612 if (ImGui::Button(ICON_MD_INVENTORY " Item")) {
615 }
616 }
617 if (ImGui::IsItemHovered()) {
618 ImGui::SetTooltip("Open Item Editor panel to select items for placement");
619 }
620 ImGui::SameLine();
621
622 // Door placement toggle (inline)
624 if (door_mode) {
625 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.6f, 0.9f, 1.0f));
626 }
627 if (ImGui::Button(ICON_MD_DOOR_FRONT " Door")) {
629 }
630 if (door_mode) {
631 ImGui::PopStyleColor();
632 }
633 if (ImGui::IsItemHovered()) {
634 ImGui::SetTooltip(door_mode ? "Click to cancel door placement" : "Click to place doors");
635 }
636 ImGui::EndGroup();
637 ImGui::Separator();
638 }
639
640 ImGui::EndGroup();
641
642 // Set up context menu items BEFORE DrawBackground so DrawContextMenu can be
643 // called immediately after (OpenPopupOnItemClick requires this ordering)
645
646 if (rooms_ && rom_->is_loaded()) {
647 auto& room = (*rooms_)[room_id];
648
649 // === Entity Placement Menu ===
650 gui::CanvasMenuItem place_menu;
651 place_menu.label = "Place Entity";
652 place_menu.icon = ICON_MD_ADD;
653
654 // Place Object option
655 place_menu.subitems.push_back(gui::CanvasMenuItem(
656 "Object", ICON_MD_WIDGETS,
657 [this]() {
660 }
661 }));
662
663 // Place Sprite option
664 place_menu.subitems.push_back(gui::CanvasMenuItem(
665 "Sprite", ICON_MD_PERSON,
666 [this]() {
669 }));
670
671 // Place Item option
672 place_menu.subitems.push_back(gui::CanvasMenuItem(
673 "Item", ICON_MD_INVENTORY,
674 [this]() {
677 }));
678
679 // Place Door option
680 place_menu.subitems.push_back(gui::CanvasMenuItem(
681 "Door", ICON_MD_DOOR_FRONT,
682 [this]() {
686 }));
687
688 canvas_.AddContextMenuItem(place_menu);
689
690 // Add room property quick toggles (4-way layer visibility)
691 gui::CanvasMenuItem layer_menu;
692 layer_menu.label = "Layer Visibility";
693 layer_menu.icon = ICON_MD_LAYERS;
694
695 layer_menu.subitems.push_back(
696 gui::CanvasMenuItem("BG1 Layout", [this, room_id]() {
697 auto& mgr = GetRoomLayerManager(room_id);
698 mgr.SetLayerVisible(
700 !mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout));
701 }));
702 layer_menu.subitems.push_back(
703 gui::CanvasMenuItem("BG1 Objects", [this, room_id]() {
704 auto& mgr = GetRoomLayerManager(room_id);
705 mgr.SetLayerVisible(
707 !mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects));
708 }));
709 layer_menu.subitems.push_back(
710 gui::CanvasMenuItem("BG2 Layout", [this, room_id]() {
711 auto& mgr = GetRoomLayerManager(room_id);
712 mgr.SetLayerVisible(
714 !mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout));
715 }));
716 layer_menu.subitems.push_back(
717 gui::CanvasMenuItem("BG2 Objects", [this, room_id]() {
718 auto& mgr = GetRoomLayerManager(room_id);
719 mgr.SetLayerVisible(
721 !mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects));
722 }));
723
724 canvas_.AddContextMenuItem(layer_menu);
725
726 // Entity Visibility menu
727 gui::CanvasMenuItem entity_menu;
728 entity_menu.label = "Entity Visibility";
729 entity_menu.icon = ICON_MD_PERSON;
730
731 entity_menu.subitems.push_back(
732 gui::CanvasMenuItem("Show Sprites", [this]() {
734 }));
735 entity_menu.subitems.push_back(
736 gui::CanvasMenuItem("Show Pot Items", [this]() {
738 }));
739
740 canvas_.AddContextMenuItem(entity_menu);
741
742 // Add re-render option
744 "Re-render Room", ICON_MD_REFRESH,
745 [&room]() { room.RenderRoomGraphics(); }, "Ctrl+R"));
746
747 // Grid Options
748 gui::CanvasMenuItem grid_menu;
749 grid_menu.label = "Grid Options";
750 grid_menu.icon = ICON_MD_GRID_ON;
751
752 // Toggle grid visibility
753 gui::CanvasMenuItem toggle_grid_item(
754 show_grid_ ? "Hide Grid" : "Show Grid",
756 [this]() { show_grid_ = !show_grid_; }, "G");
757 grid_menu.subitems.push_back(toggle_grid_item);
758
759 // Grid size options (only show if grid is visible)
760 grid_menu.subitems.push_back(
761 gui::CanvasMenuItem("8x8", [this]() { custom_grid_size_ = 8; show_grid_ = true; }));
762 grid_menu.subitems.push_back(
763 gui::CanvasMenuItem("16x16", [this]() { custom_grid_size_ = 16; show_grid_ = true; }));
764 grid_menu.subitems.push_back(
765 gui::CanvasMenuItem("32x32", [this]() { custom_grid_size_ = 32; show_grid_ = true; }));
766
767 canvas_.AddContextMenuItem(grid_menu);
768
769 // === DEBUG MENU ===
770 gui::CanvasMenuItem debug_menu;
771 debug_menu.label = "Debug";
772 debug_menu.icon = ICON_MD_BUG_REPORT;
773
774 // Show room info
775 gui::CanvasMenuItem room_info_item(
776 "Show Room Info", ICON_MD_INFO,
778 debug_menu.subitems.push_back(room_info_item);
779
780 // Show texture info
781 gui::CanvasMenuItem texture_info_item(
782 "Show Texture Debug", ICON_MD_IMAGE,
784 debug_menu.subitems.push_back(texture_info_item);
785
786 // Toggle coordinate overlay
787 gui::CanvasMenuItem coord_overlay_item(
788 show_coordinate_overlay_ ? "Hide Coordinates" : "Show Coordinates",
791 debug_menu.subitems.push_back(coord_overlay_item);
792
793 // Show object bounds with sub-menu for categories
794 gui::CanvasMenuItem object_bounds_menu;
795 object_bounds_menu.label = "Show Object Bounds";
796 object_bounds_menu.icon = ICON_MD_CROP_SQUARE;
797 object_bounds_menu.callback = [this]() {
799 };
800
801 // Sub-menu for filtering by type
802 object_bounds_menu.subitems.push_back(
803 gui::CanvasMenuItem("Type 1 (0x00-0xFF)", [this]() {
806 }));
807 object_bounds_menu.subitems.push_back(
808 gui::CanvasMenuItem("Type 2 (0x100-0x1FF)", [this]() {
811 }));
812 object_bounds_menu.subitems.push_back(
813 gui::CanvasMenuItem("Type 3 (0xF00-0xFFF)", [this]() {
816 }));
817
818 // Separator
820 sep.label = "---";
821 sep.enabled_condition = []() {
822 return false;
823 };
824 object_bounds_menu.subitems.push_back(sep);
825
826 // Sub-menu for filtering by layer
827 object_bounds_menu.subitems.push_back(
828 gui::CanvasMenuItem("Layer 0 (BG1)", [this]() {
831 }));
832 object_bounds_menu.subitems.push_back(
833 gui::CanvasMenuItem("Layer 1 (BG2)", [this]() {
836 }));
837 object_bounds_menu.subitems.push_back(
838 gui::CanvasMenuItem("Layer 2 (BG3)", [this]() {
841 }));
842
843 debug_menu.subitems.push_back(object_bounds_menu);
844
845 // Show layer info
846 gui::CanvasMenuItem layer_info_item(
847 "Show Layer Info", ICON_MD_LAYERS,
848 [this]() { show_layer_info_ = !show_layer_info_; });
849 debug_menu.subitems.push_back(layer_info_item);
850
851 // Force reload room
852 gui::CanvasMenuItem force_reload_item(
853 "Force Reload", ICON_MD_REFRESH, [&room]() {
854 room.LoadObjects();
855 room.LoadRoomGraphics(room.blockset);
856 room.RenderRoomGraphics();
857 });
858 debug_menu.subitems.push_back(force_reload_item);
859
860 // Log room state
861 gui::CanvasMenuItem log_item(
862 "Log Room State", ICON_MD_PRINT, [&room, room_id]() {
863 LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id);
864 LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d",
865 room.blockset, room.palette, room.layout);
866 LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu",
867 room.GetTileObjects().size(), room.GetSprites().size());
868 LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d",
869 room.bg1_buffer().bitmap().width(),
870 room.bg1_buffer().bitmap().height(),
871 room.bg2_buffer().bitmap().width(),
872 room.bg2_buffer().bitmap().height());
873 });
874 debug_menu.subitems.push_back(log_item);
875
876 canvas_.AddContextMenuItem(debug_menu);
877 }
878
879 // Add object interaction menu items to canvas context menu
881 auto& interaction = object_interaction_;
882 auto selected = interaction.GetSelectedObjectIndices();
883 const bool has_selection = !selected.empty();
884 const bool single_selection = selected.size() == 1;
885 const bool has_clipboard = interaction.HasClipboardData();
886 const bool placing_object = interaction.IsObjectLoaded();
887
888 if (single_selection && rooms_) {
889 auto& room = (*rooms_)[room_id];
890 const auto& objects = room.GetTileObjects();
891 if (selected[0] < objects.size()) {
892 const auto& obj = objects[selected[0]];
893 std::string name = GetObjectName(obj.id_);
895 absl::StrFormat("Object 0x%02X: %s", obj.id_, name.c_str())));
896 }
897 }
898
899 auto enabled_if = [](bool enabled) {
900 return [enabled]() {
901 return enabled;
902 };
903 };
904
905 gui::CanvasMenuItem cut_item(
906 "Cut", ICON_MD_CONTENT_CUT,
907 [&interaction]() {
908 interaction.HandleCopySelected();
909 interaction.HandleDeleteSelected();
910 },
911 "Ctrl+X");
912 cut_item.enabled_condition = enabled_if(has_selection);
913 canvas_.AddContextMenuItem(cut_item);
914
915 gui::CanvasMenuItem copy_item(
916 "Copy", ICON_MD_CONTENT_COPY,
917 [&interaction]() { interaction.HandleCopySelected(); }, "Ctrl+C");
918 copy_item.enabled_condition = enabled_if(has_selection);
919 canvas_.AddContextMenuItem(copy_item);
920
921 gui::CanvasMenuItem duplicate_item(
922 "Duplicate", ICON_MD_CONTENT_PASTE,
923 [&interaction]() {
924 interaction.HandleCopySelected();
925 interaction.HandlePasteObjects();
926 },
927 "Ctrl+D");
928 duplicate_item.enabled_condition = enabled_if(has_selection);
929 canvas_.AddContextMenuItem(duplicate_item);
930
931 gui::CanvasMenuItem delete_item(
932 "Delete", ICON_MD_DELETE,
933 [&interaction]() { interaction.HandleDeleteSelected(); }, "Del");
934 delete_item.enabled_condition = enabled_if(has_selection);
935 canvas_.AddContextMenuItem(delete_item);
936
937 gui::CanvasMenuItem paste_item(
938 "Paste", ICON_MD_CONTENT_PASTE,
939 [&interaction]() { interaction.HandlePasteObjects(); }, "Ctrl+V");
940 paste_item.enabled_condition = enabled_if(has_clipboard);
941 canvas_.AddContextMenuItem(paste_item);
942
943 gui::CanvasMenuItem cancel_item(
944 "Cancel Placement", ICON_MD_CANCEL,
945 [&interaction]() { interaction.CancelPlacement(); }, "Esc");
946 cancel_item.enabled_condition = enabled_if(placing_object);
947 canvas_.AddContextMenuItem(cancel_item);
948
949 // Send to Layer submenu
950 gui::CanvasMenuItem layer_menu;
951 layer_menu.label = "Send to Layer";
952 layer_menu.icon = ICON_MD_LAYERS;
953 layer_menu.enabled_condition = enabled_if(has_selection);
954
955 gui::CanvasMenuItem layer1_item(
956 "Layer 1 (BG1)", ICON_MD_LOOKS_ONE,
957 [&interaction]() { interaction.SendSelectedToLayer(0); }, "1");
958 layer1_item.enabled_condition = enabled_if(has_selection);
959 layer_menu.subitems.push_back(layer1_item);
960
961 gui::CanvasMenuItem layer2_item(
962 "Layer 2 (BG2)", ICON_MD_LOOKS_TWO,
963 [&interaction]() { interaction.SendSelectedToLayer(1); }, "2");
964 layer2_item.enabled_condition = enabled_if(has_selection);
965 layer_menu.subitems.push_back(layer2_item);
966
967 gui::CanvasMenuItem layer3_item(
968 "Layer 3 (BG3)", ICON_MD_LOOKS_3,
969 [&interaction]() { interaction.SendSelectedToLayer(2); }, "3");
970 layer3_item.enabled_condition = enabled_if(has_selection);
971 layer_menu.subitems.push_back(layer3_item);
972
973 canvas_.AddContextMenuItem(layer_menu);
974
975 // Arrange submenu (object draw order)
976 gui::CanvasMenuItem arrange_menu;
977 arrange_menu.label = "Arrange";
978 arrange_menu.icon = ICON_MD_SWAP_VERT;
979 arrange_menu.enabled_condition = enabled_if(has_selection);
980
981 gui::CanvasMenuItem bring_front_item(
982 "Bring to Front", ICON_MD_FLIP_TO_FRONT,
983 [&interaction]() { interaction.SendSelectedToFront(); }, "Ctrl+Shift+]");
984 bring_front_item.enabled_condition = enabled_if(has_selection);
985 arrange_menu.subitems.push_back(bring_front_item);
986
987 gui::CanvasMenuItem send_back_item(
988 "Send to Back", ICON_MD_FLIP_TO_BACK,
989 [&interaction]() { interaction.SendSelectedToBack(); }, "Ctrl+Shift+[");
990 send_back_item.enabled_condition = enabled_if(has_selection);
991 arrange_menu.subitems.push_back(send_back_item);
992
993 gui::CanvasMenuItem bring_forward_item(
994 "Bring Forward", ICON_MD_ARROW_UPWARD,
995 [&interaction]() { interaction.BringSelectedForward(); }, "Ctrl+]");
996 bring_forward_item.enabled_condition = enabled_if(has_selection);
997 arrange_menu.subitems.push_back(bring_forward_item);
998
999 gui::CanvasMenuItem send_backward_item(
1000 "Send Backward", ICON_MD_ARROW_DOWNWARD,
1001 [&interaction]() { interaction.SendSelectedBackward(); }, "Ctrl+[");
1002 send_backward_item.enabled_condition = enabled_if(has_selection);
1003 arrange_menu.subitems.push_back(send_backward_item);
1004
1005 canvas_.AddContextMenuItem(arrange_menu);
1006
1007 // === Entity Selection Actions (Doors, Sprites, Items) ===
1008 const auto& selected_entity = interaction.GetSelectedEntity();
1009 const bool has_entity_selection = interaction.HasEntitySelection();
1010
1011 if (has_entity_selection && rooms_) {
1012 auto& room = (*rooms_)[room_id];
1013
1014 // Show selected entity info header
1015 std::string entity_info;
1016 switch (selected_entity.type) {
1017 case EntityType::Door: {
1018 const auto& doors = room.GetDoors();
1019 if (selected_entity.index < doors.size()) {
1020 const auto& door = doors[selected_entity.index];
1021 entity_info = absl::StrFormat(ICON_MD_DOOR_FRONT " Door: %s",
1022 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
1023 }
1024 break;
1025 }
1026 case EntityType::Sprite: {
1027 const auto& sprites = room.GetSprites();
1028 if (selected_entity.index < sprites.size()) {
1029 const auto& sprite = sprites[selected_entity.index];
1030 entity_info = absl::StrFormat(ICON_MD_PERSON " Sprite: %s (0x%02X)",
1031 zelda3::ResolveSpriteName(sprite.id()), sprite.id());
1032 }
1033 break;
1034 }
1035 case EntityType::Item: {
1036 const auto& items = room.GetPotItems();
1037 if (selected_entity.index < items.size()) {
1038 const auto& item = items[selected_entity.index];
1039 entity_info = absl::StrFormat(ICON_MD_INVENTORY " Item: 0x%02X", item.item);
1040 }
1041 break;
1042 }
1043 default:
1044 break;
1045 }
1046
1047 if (!entity_info.empty()) {
1049
1050 // Delete entity action
1051 gui::CanvasMenuItem delete_entity_item(
1052 "Delete Entity", ICON_MD_DELETE,
1053 [this, &room, selected_entity]() {
1054 switch (selected_entity.type) {
1055 case EntityType::Door: {
1056 auto& doors = room.GetDoors();
1057 if (selected_entity.index < doors.size()) {
1058 doors.erase(doors.begin() +
1059 static_cast<long>(selected_entity.index));
1060 }
1061 break;
1062 }
1063 case EntityType::Sprite: {
1064 auto& sprites = room.GetSprites();
1065 if (selected_entity.index < sprites.size()) {
1066 sprites.erase(sprites.begin() +
1067 static_cast<long>(selected_entity.index));
1068 }
1069 break;
1070 }
1071 case EntityType::Item: {
1072 auto& items = room.GetPotItems();
1073 if (selected_entity.index < items.size()) {
1074 items.erase(items.begin() +
1075 static_cast<long>(selected_entity.index));
1076 }
1077 break;
1078 }
1079 default:
1080 break;
1081 }
1083 },
1084 "Del");
1085 canvas_.AddContextMenuItem(delete_entity_item);
1086 }
1087 }
1088 }
1089
1090 // CRITICAL: Begin canvas frame using modern BeginCanvas/EndCanvas pattern
1091 // This replaces DrawBackground + DrawContextMenu with a unified frame
1092 auto canvas_rt = gui::BeginCanvas(canvas_, frame_opts);
1093
1094 // Draw persistent debug overlays
1095 if (show_room_debug_info_ && rooms_ && rom_->is_loaded()) {
1096 auto& room = (*rooms_)[room_id];
1097 ImGui::SetNextWindowPos(
1098 ImVec2(canvas_.zero_point().x + 10, canvas_.zero_point().y + 10),
1099 ImGuiCond_FirstUseEver);
1100 ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_FirstUseEver);
1101 if (ImGui::Begin("Room Debug Info", &show_room_debug_info_,
1102 ImGuiWindowFlags_NoCollapse)) {
1103 ImGui::Text("Room: 0x%03X (%d)", room_id, room_id);
1104 ImGui::Separator();
1105 ImGui::Text("Graphics");
1106 ImGui::Text(" Blockset: 0x%02X", room.blockset);
1107 ImGui::Text(" Palette: 0x%02X", room.palette);
1108 ImGui::Text(" Layout: 0x%02X", room.layout);
1109 ImGui::Text(" Spriteset: 0x%02X", room.spriteset);
1110 ImGui::Separator();
1111 ImGui::Text("Content");
1112 ImGui::Text(" Objects: %zu", room.GetTileObjects().size());
1113 ImGui::Text(" Sprites: %zu", room.GetSprites().size());
1114 ImGui::Separator();
1115 ImGui::Text("Buffers");
1116 auto& bg1 = room.bg1_buffer().bitmap();
1117 auto& bg2 = room.bg2_buffer().bitmap();
1118 ImGui::Text(" BG1: %dx%d %s", bg1.width(), bg1.height(),
1119 bg1.texture() ? "(has texture)" : "(NO TEXTURE)");
1120 ImGui::Text(" BG2: %dx%d %s", bg2.width(), bg2.height(),
1121 bg2.texture() ? "(has texture)" : "(NO TEXTURE)");
1122 ImGui::Separator();
1123 ImGui::Text("Layers (4-way)");
1124 auto& layer_mgr = GetRoomLayerManager(room_id);
1125 bool bg1l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout);
1126 bool bg1o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects);
1127 bool bg2l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout);
1128 bool bg2o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects);
1129 if (ImGui::Checkbox("BG1 Layout", &bg1l))
1130 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout, bg1l);
1131 if (ImGui::Checkbox("BG1 Objects", &bg1o))
1132 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Objects, bg1o);
1133 if (ImGui::Checkbox("BG2 Layout", &bg2l))
1134 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout, bg2l);
1135 if (ImGui::Checkbox("BG2 Objects", &bg2o))
1136 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Objects, bg2o);
1137 int blend = static_cast<int>(
1138 layer_mgr.GetLayerBlendMode(zelda3::LayerType::BG2_Layout));
1139 if (ImGui::SliderInt("BG2 Blend", &blend, 0, 4)) {
1140 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Layout,
1141 static_cast<zelda3::LayerBlendMode>(blend));
1142 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Objects,
1143 static_cast<zelda3::LayerBlendMode>(blend));
1144 }
1145
1146 ImGui::Separator();
1147 ImGui::Text("Layout Override");
1148 static bool enable_override = false;
1149 ImGui::Checkbox("Enable Override", &enable_override);
1150 if (enable_override) {
1151 ImGui::SliderInt("Layout ID", &layout_override_, 0, 7);
1152 } else {
1153 layout_override_ = -1; // Disable override
1154 }
1155
1156 if (show_object_bounds_) {
1157 ImGui::Separator();
1158 ImGui::Text("Object Outline Filters");
1159 ImGui::Text("By Type:");
1160 ImGui::Checkbox("Type 1", &object_outline_toggles_.show_type1_objects);
1161 ImGui::Checkbox("Type 2", &object_outline_toggles_.show_type2_objects);
1162 ImGui::Checkbox("Type 3", &object_outline_toggles_.show_type3_objects);
1163 ImGui::Text("By Layer:");
1164 ImGui::Checkbox("Layer 0",
1165 &object_outline_toggles_.show_layer0_objects);
1166 ImGui::Checkbox("Layer 1",
1167 &object_outline_toggles_.show_layer1_objects);
1168 ImGui::Checkbox("Layer 2",
1169 &object_outline_toggles_.show_layer2_objects);
1170 }
1171 }
1172 ImGui::End();
1173 }
1174
1175 if (show_texture_debug_ && rooms_ && rom_->is_loaded()) {
1176 ImGui::SetNextWindowPos(
1177 ImVec2(canvas_.zero_point().x + 320, canvas_.zero_point().y + 10),
1178 ImGuiCond_FirstUseEver);
1179 ImGui::SetNextWindowSize(ImVec2(250, 0), ImGuiCond_FirstUseEver);
1180 if (ImGui::Begin("Texture Debug", &show_texture_debug_,
1181 ImGuiWindowFlags_NoCollapse)) {
1182 auto& room = (*rooms_)[room_id];
1183 auto& bg1 = room.bg1_buffer().bitmap();
1184 auto& bg2 = room.bg2_buffer().bitmap();
1185
1186 ImGui::Text("BG1 Bitmap");
1187 ImGui::Text(" Size: %dx%d", bg1.width(), bg1.height());
1188 ImGui::Text(" Active: %s", bg1.is_active() ? "YES" : "NO");
1189 ImGui::Text(" Texture: 0x%p", bg1.texture());
1190 ImGui::Text(" Modified: %s", bg1.modified() ? "YES" : "NO");
1191
1192 if (bg1.texture()) {
1193 ImGui::Text(" Preview:");
1194 ImGui::Image((ImTextureID)(intptr_t)bg1.texture(), ImVec2(128, 128));
1195 }
1196
1197 ImGui::Separator();
1198 ImGui::Text("BG2 Bitmap");
1199 ImGui::Text(" Size: %dx%d", bg2.width(), bg2.height());
1200 ImGui::Text(" Active: %s", bg2.is_active() ? "YES" : "NO");
1201 ImGui::Text(" Texture: 0x%p", bg2.texture());
1202 ImGui::Text(" Modified: %s", bg2.modified() ? "YES" : "NO");
1203
1204 if (bg2.texture()) {
1205 ImGui::Text(" Preview:");
1206 ImGui::Image((ImTextureID)(intptr_t)bg2.texture(), ImVec2(128, 128));
1207 }
1208 }
1209 ImGui::End();
1210 }
1211
1212 if (show_layer_info_) {
1213 ImGui::SetNextWindowPos(
1214 ImVec2(canvas_.zero_point().x + 580, canvas_.zero_point().y + 10),
1215 ImGuiCond_FirstUseEver);
1216 ImGui::SetNextWindowSize(ImVec2(220, 0), ImGuiCond_FirstUseEver);
1217 if (ImGui::Begin("Layer Info", &show_layer_info_,
1218 ImGuiWindowFlags_NoCollapse)) {
1219 ImGui::Text("Canvas Scale: %.2f", canvas_.global_scale());
1220 ImGui::Text("Canvas Size: %.0fx%.0f", canvas_.width(), canvas_.height());
1221 auto& layer_mgr = GetRoomLayerManager(room_id);
1222 ImGui::Separator();
1223 ImGui::Text("Layer Visibility (4-way):");
1224
1225 // Display each layer with visibility and blend mode
1226 for (int i = 0; i < 4; ++i) {
1227 auto layer = static_cast<zelda3::LayerType>(i);
1228 bool visible = layer_mgr.IsLayerVisible(layer);
1229 auto blend = layer_mgr.GetLayerBlendMode(layer);
1230 ImGui::Text(" %s: %s (%s)",
1232 visible ? "VISIBLE" : "hidden",
1233 zelda3::RoomLayerManager::GetBlendModeName(blend));
1234 }
1235
1236 ImGui::Separator();
1237 ImGui::Text("Draw Order:");
1238 auto draw_order = layer_mgr.GetDrawOrder();
1239 for (int i = 0; i < 4; ++i) {
1240 ImGui::Text(" %d: %s", i + 1,
1242 }
1243 ImGui::Text("BG2 On Top: %s", layer_mgr.IsBG2OnTop() ? "YES" : "NO");
1244 }
1245 ImGui::End();
1246 }
1247
1248 if (rooms_ && rom_->is_loaded()) {
1249 auto& room = (*rooms_)[room_id];
1250
1251 // Update object interaction context
1252 object_interaction_.SetCurrentRoom(rooms_, room_id);
1253
1254 // Check if THIS ROOM's buffers need rendering (not global arena!)
1255 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1256 bool needs_render = !bg1_bitmap.is_active() || bg1_bitmap.width() == 0;
1257
1258 // Render immediately if needed (but only once per room change)
1259 static int last_rendered_room = -1;
1260 static bool has_rendered = false;
1261 if (needs_render && (last_rendered_room != room_id || !has_rendered)) {
1262 printf(
1263 "[DungeonCanvasViewer] Loading and rendering graphics for room %d\n",
1264 room_id);
1265 (void)LoadAndRenderRoomGraphics(room_id);
1266 last_rendered_room = room_id;
1267 has_rendered = true;
1268 }
1269
1270 // Load room objects if not already loaded
1271 if (room.GetTileObjects().empty()) {
1272 room.LoadObjects();
1273 }
1274
1275 // Load sprites if not already loaded
1276 if (room.GetSprites().empty()) {
1277 room.LoadSprites();
1278 }
1279
1280 // Load pot items if not already loaded
1281 if (room.GetPotItems().empty()) {
1282 room.LoadPotItems();
1283 }
1284
1285 // CRITICAL: Process texture queue BEFORE drawing to ensure textures are
1286 // ready This must happen before DrawRoomBackgroundLayers() attempts to draw
1287 // bitmaps
1288 if (rom_ && rom_->is_loaded()) {
1290 }
1291
1292 // Draw the room's background layers to canvas
1293 // This already includes objects rendered by ObjectDrawer in
1294 // Room::RenderObjectsToBackground()
1295 DrawRoomBackgroundLayers(room_id);
1296
1297 // Draw mask highlights when mask selection mode is active
1298 // This helps visualize which objects are BG2 overlays
1299 if (object_interaction_.IsMaskModeActive()) {
1300 DrawMaskHighlights(canvas_rt, room);
1301 }
1302
1303 // Render entity overlays (sprites, pot items) as colored squares with labels
1304 // (Entities are not part of the background buffers)
1305 RenderEntityOverlay(canvas_rt, room);
1306
1307 // Handle object interaction if enabled
1308 if (object_interaction_enabled_) {
1309 object_interaction_.HandleCanvasMouseInput();
1310 object_interaction_.CheckForObjectSelection();
1311 object_interaction_.DrawSelectBox();
1312 object_interaction_
1313 .DrawSelectionHighlights(); // Draw object selection highlights
1314 object_interaction_
1315 .DrawEntitySelectionHighlights(); // Draw door/sprite/item selection
1316 object_interaction_.DrawGhostPreview(); // Draw placement preview
1317 // Context menu is handled by BeginCanvas via frame_opts.draw_context_menu
1318 }
1319 }
1320
1321 // Draw optional overlays on top of background bitmap
1322 if (rooms_ && rom_->is_loaded()) {
1323 auto& room = (*rooms_)[room_id];
1324
1325 // Draw the room layout first as the base layer
1326
1327 // VISUALIZATION: Draw object position rectangles (for debugging)
1328 // This shows where objects are placed regardless of whether graphics render
1329 if (show_object_bounds_) {
1330 DrawObjectPositionOutlines(canvas_rt, room);
1331 }
1332 }
1333
1334 // Draw coordinate overlay when hovering over canvas
1335 if (show_coordinate_overlay_ && canvas_.IsMouseHovering()) {
1336 ImVec2 mouse_pos = ImGui::GetMousePos();
1337 ImVec2 canvas_pos = canvas_.zero_point();
1338 float scale = canvas_.global_scale();
1339 if (scale <= 0.0f) scale = 1.0f;
1340
1341 // Calculate canvas-relative position
1342 int canvas_x = static_cast<int>((mouse_pos.x - canvas_pos.x) / scale);
1343 int canvas_y = static_cast<int>((mouse_pos.y - canvas_pos.y) / scale);
1344
1345 // Only show if within bounds
1346 if (canvas_x >= 0 && canvas_x < kRoomPixelWidth &&
1347 canvas_y >= 0 && canvas_y < kRoomPixelHeight) {
1348 // Calculate tile coordinates
1349 int tile_x = canvas_x / kDungeonTileSize;
1350 int tile_y = canvas_y / kDungeonTileSize;
1351
1352 // Calculate camera/world coordinates (for minecart tracks, sprites, etc.)
1353 auto [camera_x, camera_y] = dungeon_coords::TileToCameraCoords(room_id, tile_x, tile_y);
1354
1355 // Calculate sprite coordinates (16-pixel units)
1356 int sprite_x = canvas_x / dungeon_coords::kSpriteTileSize;
1357 int sprite_y = canvas_y / dungeon_coords::kSpriteTileSize;
1358
1359 // Draw coordinate info box at mouse position
1360 ImVec2 overlay_pos = ImVec2(mouse_pos.x + 15, mouse_pos.y + 15);
1361
1362 // Build coordinate text
1363 std::string coord_text = absl::StrFormat(
1364 "Tile: (%d, %d)\n"
1365 "Pixel: (%d, %d)\n"
1366 "Camera: ($%04X, $%04X)\n"
1367 "Sprite: (%d, %d)",
1368 tile_x, tile_y,
1369 canvas_x, canvas_y,
1370 camera_x, camera_y,
1371 sprite_x, sprite_y);
1372
1373 // Draw background box
1374 ImVec2 text_size = ImGui::CalcTextSize(coord_text.c_str());
1375 ImVec2 box_min = ImVec2(overlay_pos.x - 4, overlay_pos.y - 2);
1376 ImVec2 box_max = ImVec2(overlay_pos.x + text_size.x + 8, overlay_pos.y + text_size.y + 4);
1377
1378 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1379 draw_list->AddRectFilled(box_min, box_max, IM_COL32(0, 0, 0, 200), 4.0f);
1380 draw_list->AddRect(box_min, box_max, IM_COL32(100, 100, 100, 255), 4.0f);
1381 draw_list->AddText(overlay_pos, IM_COL32(255, 255, 255, 255), coord_text.c_str());
1382 }
1383 }
1384
1385 // End canvas frame - this draws grid/overlay based on frame_opts
1386 gui::EndCanvas(canvas_, canvas_rt, frame_opts);
1387}
1388
1389void DungeonCanvasViewer::DisplayObjectInfo(const gui::CanvasRuntime& rt,
1390 const zelda3::RoomObject& object,
1391 int canvas_x, int canvas_y) {
1392 // Display object information as text overlay with hex ID and name
1393 std::string name = GetObjectName(object.id_);
1394 std::string info_text;
1395 if (object.id_ >= 0x100) {
1396 info_text =
1397 absl::StrFormat("0x%03X %s (X:%d Y:%d S:0x%02X)", object.id_,
1398 name.c_str(), object.x_, object.y_, object.size_);
1399 } else {
1400 info_text =
1401 absl::StrFormat("0x%02X %s (X:%d Y:%d S:0x%02X)", object.id_,
1402 name.c_str(), object.x_, object.y_, object.size_);
1403 }
1404
1405 // Draw text at the object position using runtime-based helper
1406 gui::DrawText(rt, info_text, canvas_x, canvas_y - 12);
1407}
1408
1409void DungeonCanvasViewer::RenderSprites(const gui::CanvasRuntime& rt,
1410 const zelda3::Room& room) {
1411 // Skip if sprites are not visible
1412 if (!entity_visibility_.show_sprites) {
1413 return;
1414 }
1415
1416 const auto& theme = AgentUI::GetTheme();
1417
1418 // Render sprites as 16x16 colored squares with sprite name/ID
1419 // NOTE: Sprite coordinates are in 16-pixel units (0-31 range = 512 pixels)
1420 // unlike object coordinates which are in 8-pixel tile units
1421 for (const auto& sprite : room.GetSprites()) {
1422 // Sprites use 16-pixel coordinate system
1423 int canvas_x = sprite.x() * 16;
1424 int canvas_y = sprite.y() * 16;
1425
1426 if (IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
1427 // Draw 16x16 square for sprite (like overworld entities)
1428 ImVec4 sprite_color;
1429
1430 // Color-code sprites based on layer
1431 if (sprite.layer() == 0) {
1432 sprite_color = theme.dungeon_sprite_layer0; // Green for layer 0
1433 } else {
1434 sprite_color = theme.dungeon_sprite_layer1; // Blue for layer 1
1435 }
1436
1437 // Draw filled square using runtime-based helper
1438 gui::DrawRect(rt, canvas_x, canvas_y, 16, 16, sprite_color);
1439
1440 // Draw sprite ID and name using unified ResourceLabelProvider
1441 std::string full_name = zelda3::GetSpriteLabel(sprite.id());
1442 std::string sprite_text;
1443 // Truncate long names for display
1444 if (full_name.length() > 12) {
1445 sprite_text = absl::StrFormat("%02X %s..", sprite.id(),
1446 full_name.substr(0, 8).c_str());
1447 } else {
1448 sprite_text = absl::StrFormat("%02X %s", sprite.id(), full_name.c_str());
1449 }
1450
1451 gui::DrawText(rt, sprite_text, canvas_x, canvas_y);
1452 }
1453 }
1454}
1455
1456void DungeonCanvasViewer::RenderPotItems(const gui::CanvasRuntime& rt,
1457 const zelda3::Room& room) {
1458 // Skip if pot items are not visible
1459 if (!entity_visibility_.show_pot_items) {
1460 return;
1461 }
1462
1463 const auto& pot_items = room.GetPotItems();
1464
1465 // If no pot items in this room, nothing to render
1466 if (pot_items.empty()) {
1467 return;
1468 }
1469
1470 // Pot item names
1471 static const char* kPotItemNames[] = {
1472 "Nothing", // 0
1473 "Green Rupee", // 1
1474 "Rock", // 2
1475 "Bee", // 3
1476 "Health", // 4
1477 "Bomb", // 5
1478 "Heart", // 6
1479 "Blue Rupee", // 7
1480 "Key", // 8
1481 "Arrow", // 9
1482 "Bomb", // 10
1483 "Heart", // 11
1484 "Magic", // 12
1485 "Full Magic", // 13
1486 "Cucco", // 14
1487 "Green Soldier", // 15
1488 "Bush Stal", // 16
1489 "Blue Soldier", // 17
1490 "Landmine", // 18
1491 "Heart", // 19
1492 "Fairy", // 20
1493 "Heart", // 21
1494 "Nothing", // 22
1495 "Hole", // 23
1496 "Warp", // 24
1497 "Staircase", // 25
1498 "Bombable", // 26
1499 "Switch" // 27
1500 };
1501 constexpr size_t kPotItemNameCount =
1502 sizeof(kPotItemNames) / sizeof(kPotItemNames[0]);
1503
1504 // Pot items now have their own position data from ROM
1505 // No need to match to objects - each item has exact pixel coordinates
1506 for (const auto& pot_item : pot_items) {
1507 // Get pixel coordinates from PotItem structure
1508 int pixel_x = pot_item.GetPixelX();
1509 int pixel_y = pot_item.GetPixelY();
1510
1511 // Convert to canvas coordinates (already in pixels, just need offset)
1512 // Note: pot item coords are already in full room pixel space
1513 auto [canvas_x, canvas_y] =
1514 RoomToCanvasCoordinates(pixel_x / 8, pixel_y / 8);
1515
1516 if (IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
1517 // Draw colored square
1518 ImVec4 pot_item_color;
1519 if (pot_item.item == 0) {
1520 pot_item_color = ImVec4(0.5f, 0.5f, 0.5f, 0.5f); // Gray for Nothing
1521 } else {
1522 pot_item_color = ImVec4(1.0f, 0.85f, 0.2f, 0.75f); // Yellow for items
1523 }
1524
1525 gui::DrawRect(rt, canvas_x, canvas_y, 16, 16, pot_item_color);
1526
1527 // Get item name
1528 std::string item_name;
1529 if (pot_item.item < kPotItemNameCount) {
1530 item_name = kPotItemNames[pot_item.item];
1531 } else {
1532 item_name = absl::StrFormat("Unk%02X", pot_item.item);
1533 }
1534
1535 // Draw label above the box
1536 std::string item_text =
1537 absl::StrFormat("%02X %s", pot_item.item, item_name.c_str());
1538 gui::DrawText(rt, item_text, canvas_x, canvas_y - 10);
1539 }
1540 }
1541}
1542
1543void DungeonCanvasViewer::RenderEntityOverlay(const gui::CanvasRuntime& rt,
1544 const zelda3::Room& room) {
1545 // Render all entity overlays using runtime-based helpers
1546 RenderSprites(rt, room);
1547 RenderPotItems(rt, room);
1548}
1549
1550// Coordinate conversion helper functions
1551std::pair<int, int> DungeonCanvasViewer::RoomToCanvasCoordinates(
1552 int room_x, int room_y) const {
1553 // Convert room coordinates (tile units) to UNSCALED canvas pixel coordinates
1554 // Dungeon tiles are 8x8 pixels (not 16x16!)
1555 // IMPORTANT: Return UNSCALED coordinates - Canvas drawing functions apply
1556 // scale internally Do NOT multiply by scale here or we get double-scaling!
1557
1558 // Simple conversion: tile units → pixel units (no scale, no offset)
1559 return {room_x * 8, room_y * 8};
1560}
1561
1562std::pair<int, int> DungeonCanvasViewer::CanvasToRoomCoordinates(
1563 int canvas_x, int canvas_y) const {
1564 // Convert canvas screen coordinates (pixels) to room coordinates (tile units)
1565 // Input: Screen-space coordinates (affected by zoom/scale)
1566 // Output: Logical tile coordinates (0-63 for each axis)
1567
1568 // IMPORTANT: Mouse coordinates are in screen space, must undo scale first
1569 float scale = canvas_.global_scale();
1570 if (scale <= 0.0f)
1571 scale = 1.0f; // Prevent division by zero
1572
1573 // Step 1: Convert screen space → logical pixel space
1574 int logical_x = static_cast<int>(canvas_x / scale);
1575 int logical_y = static_cast<int>(canvas_y / scale);
1576
1577 // Step 2: Convert logical pixels → tile units (8 pixels per tile)
1578 return {logical_x / 8, logical_y / 8};
1579}
1580
1581bool DungeonCanvasViewer::IsWithinCanvasBounds(int canvas_x, int canvas_y,
1582 int margin) const {
1583 // Check if coordinates are within canvas bounds with optional margin
1584 auto canvas_width = canvas_.width();
1585 auto canvas_height = canvas_.height();
1586 return (canvas_x >= -margin && canvas_y >= -margin &&
1587 canvas_x <= canvas_width + margin &&
1588 canvas_y <= canvas_height + margin);
1589}
1590// Room layout visualization
1591
1592// Object visualization methods
1593void DungeonCanvasViewer::DrawObjectPositionOutlines(
1594 const gui::CanvasRuntime& rt, const zelda3::Room& room) {
1595 // Draw colored rectangles showing object positions
1596 // This helps visualize object placement even if graphics don't render
1597 // correctly
1598
1599 const auto& theme = AgentUI::GetTheme();
1600 const auto& objects = room.GetTileObjects();
1601
1602 // Create ObjectDrawer for accurate dimension calculation
1603 // ObjectDrawer uses game-accurate draw routine mapping to determine sizes
1604 // Note: const_cast needed because rom() accessor is non-const, but we don't
1605 // modify ROM
1606 zelda3::ObjectDrawer drawer(const_cast<zelda3::Room&>(room).rom(), room.id(),
1607 nullptr);
1608
1609 for (const auto& obj : objects) {
1610 // Filter by object type (default to true if unknown type)
1611 bool show_this_type = true; // Default to showing
1612 if (obj.id_ < 0x100) {
1613 show_this_type = object_outline_toggles_.show_type1_objects;
1614 } else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
1615 show_this_type = object_outline_toggles_.show_type2_objects;
1616 } else if (obj.id_ >= 0xF00) {
1617 show_this_type = object_outline_toggles_.show_type3_objects;
1618 }
1619 // else: unknown type, use default (true)
1620
1621 // Filter by layer (default to true if unknown layer)
1622 bool show_this_layer = true; // Default to showing
1623 if (obj.GetLayerValue() == 0) {
1624 show_this_layer = object_outline_toggles_.show_layer0_objects;
1625 } else if (obj.GetLayerValue() == 1) {
1626 show_this_layer = object_outline_toggles_.show_layer1_objects;
1627 } else if (obj.GetLayerValue() == 2) {
1628 show_this_layer = object_outline_toggles_.show_layer2_objects;
1629 }
1630 // else: unknown layer, use default (true)
1631
1632 // Skip if filtered out
1633 if (!show_this_type || !show_this_layer) {
1634 continue;
1635 }
1636
1637 // Convert object position (tile coordinates) to canvas pixel coordinates
1638 // (UNSCALED)
1639 auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
1640
1641 // Calculate object dimensions using the shared dimension table when loaded
1642 int width = 16;
1643 int height = 16;
1644 auto& dim_table = zelda3::ObjectDimensionTable::Get();
1645 if (dim_table.IsLoaded()) {
1646 auto [w_tiles, h_tiles] = dim_table.GetDimensions(obj.id_, obj.size_);
1647 width = w_tiles * 8;
1648 height = h_tiles * 8;
1649 } else {
1650 auto [w, h] = drawer.CalculateObjectDimensions(obj);
1651 width = w;
1652 height = h;
1653 }
1654
1655 // IMPORTANT: Do NOT apply canvas scale here - DrawRect handles it
1656 // Clamp to reasonable sizes (in logical space)
1657 width = std::min(width, 512);
1658 height = std::min(height, 512);
1659
1660 // Color-code by layer
1661 ImVec4 outline_color;
1662 if (obj.GetLayerValue() == 0) {
1663 outline_color = theme.dungeon_outline_layer0; // Red for layer 0
1664 } else if (obj.GetLayerValue() == 1) {
1665 outline_color = theme.dungeon_outline_layer1; // Green for layer 1
1666 } else {
1667 outline_color = theme.dungeon_outline_layer2; // Blue for layer 2
1668 }
1669
1670 // Draw outline rectangle using runtime-based helper
1671 gui::DrawRect(rt, canvas_x, canvas_y, width, height, outline_color);
1672
1673 // Draw object ID label with hex ID and abbreviated name
1674 // Format: "0xNN Name" where name is truncated if needed
1675 std::string name = GetObjectName(obj.id_);
1676 // Truncate name to fit (approx 12 chars for small objects)
1677 if (name.length() > 12) {
1678 name = name.substr(0, 10) + "..";
1679 }
1680 std::string label;
1681 if (obj.id_ >= 0x100) {
1682 label = absl::StrFormat("0x%03X\n%s\n[%dx%d]", obj.id_, name.c_str(),
1683 width, height);
1684 } else {
1685 label = absl::StrFormat("0x%02X\n%s\n[%dx%d]", obj.id_, name.c_str(),
1686 width, height);
1687 }
1688 gui::DrawText(rt, label, canvas_x + 1, canvas_y + 1);
1689 }
1690}
1691
1692// Room graphics management methods
1693absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(int room_id) {
1694 LOG_DEBUG("[LoadAndRender]", "START room_id=%d", room_id);
1695
1696 if (room_id < 0 || room_id >= 128) {
1697 LOG_DEBUG("[LoadAndRender]", "ERROR: Invalid room ID");
1698 return absl::InvalidArgumentError("Invalid room ID");
1699 }
1700
1701 if (!rom_ || !rom_->is_loaded()) {
1702 LOG_DEBUG("[LoadAndRender]", "ERROR: ROM not loaded");
1703 return absl::FailedPreconditionError("ROM not loaded");
1704 }
1705
1706 if (!rooms_) {
1707 LOG_DEBUG("[LoadAndRender]", "ERROR: Room data not available");
1708 return absl::FailedPreconditionError("Room data not available");
1709 }
1710
1711 auto& room = (*rooms_)[room_id];
1712 LOG_DEBUG("[LoadAndRender]", "Got room reference");
1713
1714 // Load room graphics with proper blockset
1715 LOG_DEBUG("[LoadAndRender]", "Loading graphics for blockset %d",
1716 room.blockset);
1717 room.LoadRoomGraphics(room.blockset);
1718 LOG_DEBUG("[LoadAndRender]", "Graphics loaded");
1719
1720 // Load the room's palette with bounds checking
1721 if (!game_data_) {
1722 LOG_ERROR("[LoadAndRender]", "GameData not available");
1723 return absl::FailedPreconditionError("GameData not available");
1724 }
1725 const auto& dungeon_main = game_data_->palette_groups.dungeon_main;
1726 if (!dungeon_main.empty()) {
1727 int palette_id = room.palette;
1728 if (room.palette < game_data_->paletteset_ids.size()) {
1729 palette_id = game_data_->paletteset_ids[room.palette][0];
1730 }
1731 current_palette_group_id_ =
1732 std::min<uint64_t>(std::max(0, palette_id),
1733 static_cast<int>(dungeon_main.size() - 1));
1734
1735 auto full_palette = dungeon_main[current_palette_group_id_];
1737 current_palette_group_,
1738 gfx::CreatePaletteGroupFromLargePalette(full_palette, 16));
1739 LOG_DEBUG("[LoadAndRender]", "Palette loaded: group_id=%zu",
1740 current_palette_group_id_);
1741 }
1742
1743 // Render the room graphics (self-contained - handles all palette application)
1744 LOG_DEBUG("[LoadAndRender]", "Calling room.RenderRoomGraphics()...");
1745 room.RenderRoomGraphics();
1746 LOG_DEBUG("[LoadAndRender]",
1747 "RenderRoomGraphics() complete - room buffers self-contained");
1748
1749 LOG_DEBUG("[LoadAndRender]", "SUCCESS");
1750 return absl::OkStatus();
1751}
1752
1753void DungeonCanvasViewer::DrawRoomBackgroundLayers(int room_id) {
1754 if (room_id < 0 || room_id >= zelda3::NumberOfRooms || !rooms_)
1755 return;
1756
1757 auto& room = (*rooms_)[room_id];
1758 auto& layer_mgr = GetRoomLayerManager(room_id);
1759
1760 // Apply room's layer merging settings to the manager
1761 layer_mgr.ApplyLayerMerging(room.layer_merging());
1762
1763 float scale = canvas_.global_scale();
1764
1765 // Always use composite mode: single merged bitmap with back-to-front layer order
1766 // This matches SNES hardware behavior where BG2 is drawn first, then BG1 on top
1767 auto& composite = room.GetCompositeBitmap(layer_mgr);
1768 if (composite.is_active() && composite.width() > 0) {
1769 // Ensure texture exists or is updated when bitmap data changes
1770 if (!composite.texture()) {
1773 composite.set_modified(false);
1774 } else if (composite.modified()) {
1775 // Update texture when bitmap was regenerated
1778 composite.set_modified(false);
1779 }
1780 if (composite.texture()) {
1781 canvas_.DrawBitmap(composite, 0, 0, scale, 255);
1782 }
1783 }
1784}
1785
1786void DungeonCanvasViewer::DrawMaskHighlights(const gui::CanvasRuntime& rt,
1787 const zelda3::Room& room) {
1788 // Draw semi-transparent blue overlay on BG2/Layer 1 objects when mask mode
1789 // is active. This helps identify which objects are the "overlay" content
1790 // (platforms, statues, stairs) that create transparency holes in BG1.
1791 const auto& objects = room.GetTileObjects();
1792
1793 // Create ObjectDrawer for dimension calculation
1794 zelda3::ObjectDrawer drawer(const_cast<zelda3::Room&>(room).rom(), room.id(),
1795 nullptr);
1796
1797 // Mask highlight color: semi-transparent cyan/blue
1798 // DrawRect draws a filled rectangle with a black outline
1799 ImVec4 mask_color(0.2f, 0.6f, 1.0f, 0.4f); // Light blue, 40% opacity
1800
1801 for (const auto& obj : objects) {
1802 // Only highlight Layer 1 (BG2) objects - these are the mask/overlay objects
1803 if (obj.GetLayerValue() != 1) {
1804 continue;
1805 }
1806
1807 // Convert object position to canvas coordinates
1808 auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
1809
1810 // Calculate object dimensions
1811 int width = 16;
1812 int height = 16;
1813 auto& dim_table = zelda3::ObjectDimensionTable::Get();
1814 if (dim_table.IsLoaded()) {
1815 auto [w_tiles, h_tiles] = dim_table.GetDimensions(obj.id_, obj.size_);
1816 width = w_tiles * 8;
1817 height = h_tiles * 8;
1818 } else {
1819 auto [w, h] = drawer.CalculateObjectDimensions(obj);
1820 width = w;
1821 height = h;
1822 }
1823
1824 // Clamp to reasonable sizes
1825 width = std::min(width, 512);
1826 height = std::min(height, 512);
1827
1828 // Draw filled rectangle with semi-transparent overlay (includes black outline)
1829 gui::DrawRect(rt, canvas_x, canvas_y, width, height, mask_color);
1830 }
1831}
1832
1833} // namespace yaze::editor
absl::StatusOr< uint16_t > ReadWord(int offset)
Definition rom.cc:228
bool is_loaded() const
Definition rom.h:128
std::optional< std::pair< int, int > > pending_scroll_target_
std::array< zelda3::Room, 0x128 > * rooms_
DungeonObjectInteraction object_interaction_
std::function< void()> show_object_panel_callback_
zelda3::RoomLayerManager & GetRoomLayerManager(int room_id)
std::function< void(int)> room_navigation_callback_
std::function< void()> show_item_panel_callback_
std::function< void()> show_sprite_panel_callback_
std::function< void(int, int)> room_swap_callback_
void SetItemPlacementMode(bool enabled, uint8_t item_id=0)
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
std::vector< size_t > GetSelectedObjectIndices() const
void SetDoorPlacementMode(bool enabled, zelda3::DoorType type=zelda3::DoorType::NormalDoor)
static constexpr int kMaskLayer
static constexpr int kLayerAll
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:34
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:110
static Arena & Get()
Definition arena.cc:19
auto height() const
Definition canvas.h:498
auto custom_step() const
Definition canvas.h:496
auto global_scale() const
Definition canvas.h:494
auto width() const
Definition canvas.h:497
void ClearContextMenuItems()
Definition canvas.cc:776
void AddContextMenuItem(const gui::CanvasMenuItem &item)
Definition canvas.cc:753
void SetShowBuiltinContextMenu(bool show)
Definition canvas.h:301
auto zero_point() const
Definition canvas.h:443
static ObjectDimensionTable & Get()
Draws dungeon objects to background buffers using game patterns.
std::pair< int, int > CalculateObjectDimensions(const RoomObject &object)
Calculate the dimensions (width, height) of an object in pixels.
static const char * GetLayerName(LayerType layer)
Get human-readable name for layer type.
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:246
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:346
const std::vector< PotItem > & GetPotItems() const
Definition room.h:340
int id() const
Definition room.h:488
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_MY_LOCATION
Definition icons.h:1270
#define ICON_MD_CONTENT_CUT
Definition icons.h:466
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_VIEW_MODULE
Definition icons.h:2093
#define ICON_MD_LOOKS_ONE
Definition icons.h:1154
#define ICON_MD_SQUARE
Definition icons.h:1841
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_FLIP_TO_FRONT
Definition icons.h:802
#define ICON_MD_AUTO_AWESOME
Definition icons.h:214
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_LOOKS_TWO
Definition icons.h:1155
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_LABEL_OUTLINE
Definition icons.h:1057
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_LOOKS_3
Definition icons.h:1150
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_INVENTORY
Definition icons.h:1011
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CROP_SQUARE
Definition icons.h:500
#define ICON_MD_SWAP_VERT
Definition icons.h:1898
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_MERGE_TYPE
Definition icons.h:1200
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_GRID_OFF
Definition icons.h:895
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_SQUARE_FOOT
Definition icons.h:1842
#define ICON_MD_FILTER_ALT
Definition icons.h:761
#define ICON_MD_PRINT
Definition icons.h:1515
#define ICON_MD_FLIP_TO_BACK
Definition icons.h:801
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::pair< uint16_t, uint16_t > TileToCameraCoords(int room_id, int tile_x, int tile_y)
Calculate camera coordinates from room and tile position.
Editors are the view controllers for the application.
Definition agent_chat.cc:23
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromLargePalette(SnesPalette &palette, int num_colors)
Create a PaletteGroup by dividing a large palette into sub-palettes.
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1509
void DrawRect(const CanvasRuntime &rt, int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:2182
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1486
void DrawText(const CanvasRuntime &rt, const std::string &text, int x, int y)
Definition canvas.cc:2189
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:397
@ NormalDoor
Normal door (upper layer)
LayerBlendMode
Layer blend modes for compositing.
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
constexpr int NumberOfRooms
Definition room.h:70
int GetObjectSubtype(int object_id)
LayerType
Layer types for the 4-way visibility system.
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
std::optional< float > grid_step
Definition canvas.h:70
Declarative menu item definition.
Definition canvas_menu.h:64
std::vector< CanvasMenuItem > subitems
Definition canvas_menu.h:91
std::function< bool()> enabled_condition
Definition canvas_menu.h:81
std::function< void()> callback
Definition canvas_menu.h:75
static CanvasMenuItem Disabled(const std::string &lbl)
bool ShouldApply() const
Definition input.h:48
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition game_data.h:99
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89