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