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"
20#include "app/gui/core/icons.h"
21#include "app/gui/core/input.h"
28#include "dungeon_coordinates.h"
30#include "imgui/imgui.h"
31#include "rom/rom.h"
32#include "util/log.h"
33#include "util/macro.h"
38#include "zelda3/dungeon/room.h"
44
45namespace yaze::editor {
46
47namespace {
48
49constexpr int kRoomMatrixCols = 16;
50constexpr int kRoomMatrixRows = 19;
51constexpr int kRoomPropertyColumns = 2;
52
53enum class TrackDir : uint8_t { North, East, South, West };
54
56
57const char* GetObjectStreamLabel(int layer_value) {
58 switch (layer_value) {
59 case 0:
60 return "Primary";
61 case 1:
62 return "BG2 overlay";
63 case 2:
64 return "BG1 overlay";
65 default:
66 return "Unknown";
67 }
68}
69
70} // namespace
71
72// Use shared GetObjectName() from zelda3/dungeon/room_object.h
75
80
82 auto apply_list = [](std::array<bool, 256>& dest,
83 const std::vector<uint16_t>& values) {
84 dest.fill(false);
85 for (uint16_t value : values) {
86 if (value < 256) {
87 dest[value] = true;
88 }
89 }
90 };
91 auto ids_valid = [](const std::vector<uint16_t>& values) {
92 std::array<bool, 256> seen{};
93 for (uint16_t value : values) {
94 if (value >= seen.size() || seen[value]) {
95 return false;
96 }
97 seen[value] = true;
98 }
99 return true;
100 };
101
102 std::vector<uint16_t> default_track_tile_list;
103 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
104 default_track_tile_list.push_back(tile);
105 }
106 const std::vector<uint16_t> default_stop_tile_list = {0xB7, 0xB8, 0xB9, 0xBA};
107 const std::vector<uint16_t> default_switch_tile_list = {0xD0, 0xD1, 0xD2,
108 0xD3};
109
110 const auto& track_tiles =
113 : default_track_tile_list;
114 apply_list(track_collision_config_.track_tiles, track_tiles);
115 track_tile_order_ = track_tiles;
116
117 const auto& stop_tiles =
120 : default_stop_tile_list;
121 apply_list(track_collision_config_.stop_tiles, stop_tiles);
122
123 const auto& switch_tiles =
126 : default_switch_tile_list;
127 apply_list(track_collision_config_.switch_tiles, switch_tiles);
128 switch_tile_order_ = switch_tiles;
129
131 (track_tile_order_.size() == default_track_tile_list.size()) &&
132 (switch_tile_order_.size() == default_switch_tile_list.size()) &&
133 ids_valid(track_tile_order_) && ids_valid(switch_tile_order_);
134
135 minecart_sprite_ids_.reset();
136 std::vector<uint16_t> minecart_ids = {0xA3};
139 }
140 for (uint16_t id : minecart_ids) {
141 if (id < minecart_sprite_ids_.size()) {
142 minecart_sprite_ids_[id] = true;
143 }
144 }
145
147}
148
149void DungeonCanvasViewer::Draw(int room_id) {
150 DrawDungeonCanvas(room_id);
151}
152
154 current_room_id_ = room_id;
155 // Validate room_id and ROM
156 if (room_id < 0 || room_id >= 0x128) {
157 ImGui::Text("Invalid room ID: %d", room_id);
158 return;
159 }
160
161 if (!rom_ || !rom_->is_loaded()) {
162 ImGui::Text("ROM not loaded");
163 return;
164 }
165
166 ImGui::BeginGroup();
167
168 // CRITICAL: Canvas coordinate system for dungeons
169 // The canvas system uses a two-stage scaling model:
170 // 1. Canvas size: UNSCALED content dimensions (512x512 for dungeon rooms)
171 // 2. Viewport size: canvas_size * global_scale (handles zoom)
172 // 3. Grid lines: grid_step * global_scale (auto-scales with zoom)
173 // 4. Bitmaps: drawn with scale = global_scale (matches viewport)
174 constexpr int kRoomPixelWidth = 512; // 64 tiles * 8 pixels (UNSCALED)
175 constexpr int kRoomPixelHeight = 512;
176 constexpr int kDungeonTileSize = 8; // Dungeon tiles are 8x8 pixels
177
178 // Configure canvas frame options for the new BeginCanvas/EndCanvas pattern
179 gui::CanvasFrameOptions frame_opts;
180 frame_opts.canvas_size = ImVec2(kRoomPixelWidth, kRoomPixelHeight);
181 frame_opts.draw_grid = show_grid_;
182 frame_opts.grid_step = static_cast<float>(custom_grid_size_);
183 frame_opts.draw_context_menu = true;
184 frame_opts.draw_overlay = true;
185 frame_opts.render_popups = true;
186
187 // Legacy configuration for context menu and interaction systems
188 canvas_.SetShowBuiltinContextMenu(false); // Hide default canvas debug items
189
190 if (rooms_) {
191 auto& room = (*rooms_)[room_id];
192
193 // Check if critical properties changed and trigger reload
194 if (prev_blockset_ != room.blockset() || prev_palette_ != room.palette() ||
195 prev_layout_ != room.layout_id() ||
196 prev_spriteset_ != room.spriteset()) {
197 if (room.rom() && room.rom()->is_loaded()) {
198 room.ReloadGraphics(room.blockset());
199 }
200
201 prev_blockset_ = room.blockset();
202 prev_palette_ = room.palette();
203 prev_layout_ = room.layout_id();
204 prev_spriteset_ = room.spriteset();
205 }
206 if (header_visible_) {
207 DrawRoomHeader(room, room_id);
208 }
209 }
210
211 // Compact layer/overlay toggle bar (always visible above canvas)
213
214 ImGui::EndGroup();
215
216 // Set up context menu items BEFORE DrawBackground so DrawContextMenu can be
217 // called immediately after (OpenPopupOnItemClick requires this ordering)
219
220 // Add object interaction menu items to canvas context menu
222 auto& interaction = object_interaction_;
223 auto selected = interaction.GetSelectedObjectIndices();
224 const bool has_selection = !selected.empty();
225 const bool single_selection = selected.size() == 1;
226 const bool group_selection = selected.size() > 1;
227 const bool has_clipboard = interaction.HasClipboardData();
228 const bool placing_object = interaction.IsObjectLoaded();
229 const bool door_mode = interaction.IsDoorPlacementActive();
230 bool has_objects = false;
231 if (rooms_ && room_id >= 0 && room_id < 296) {
232 has_objects = !(*rooms_)[room_id].GetTileObjects().empty();
233 }
234
235 if (single_selection && rooms_) {
236 auto& room = (*rooms_)[room_id];
237 const auto& objects = room.GetTileObjects();
238 if (selected[0] < objects.size()) {
239 const auto& obj = objects[selected[0]];
240 std::string name = GetObjectName(obj.id_);
242 absl::StrFormat("Object 0x%02X: %s", obj.id_, name.c_str())));
243 }
244 }
245
246 auto enabled_if = [](bool enabled) {
247 return [enabled]() {
248 return enabled;
249 };
250 };
251
252 // Insert menu (parity with ZScream "Insert new <mode>")
253 gui::CanvasMenuItem insert_menu;
254 insert_menu.label = "Insert";
255 insert_menu.icon = ICON_MD_ADD_CIRCLE;
256
257 gui::CanvasMenuItem insert_object_item("Object...", ICON_MD_WIDGETS,
258 [this]() {
261 }
262 });
263 insert_object_item.enabled_condition =
264 enabled_if(show_object_panel_callback_ != nullptr);
265 insert_menu.subitems.push_back(insert_object_item);
266
267 gui::CanvasMenuItem insert_sprite_item("Sprite...", ICON_MD_PERSON,
268 [this]() {
271 }
272 });
273 insert_sprite_item.enabled_condition =
274 enabled_if(show_sprite_panel_callback_ != nullptr);
275 insert_menu.subitems.push_back(insert_sprite_item);
276
277 gui::CanvasMenuItem insert_item_item("Item...", ICON_MD_INVENTORY,
278 [this]() {
281 }
282 });
283 insert_item_item.enabled_condition =
284 enabled_if(show_item_panel_callback_ != nullptr);
285 insert_menu.subitems.push_back(insert_item_item);
286
287 gui::CanvasMenuItem insert_door_item(
288 door_mode ? "Cancel Door Placement" : "Door (Normal)",
289 ICON_MD_DOOR_FRONT, [&interaction, door_mode]() {
290 interaction.SetDoorPlacementMode(!door_mode,
292 });
293 insert_menu.subitems.push_back(insert_door_item);
294
295 canvas_.AddContextMenuItem(insert_menu);
296
297 gui::CanvasMenuItem cut_item(
298 "Cut", ICON_MD_CONTENT_CUT,
299 [&interaction]() {
300 interaction.HandleCopySelected();
301 interaction.HandleDeleteSelected();
302 },
303 "Ctrl+X");
304 cut_item.enabled_condition = enabled_if(has_selection);
305 canvas_.AddContextMenuItem(cut_item);
306
307 gui::CanvasMenuItem copy_item(
308 "Copy", ICON_MD_CONTENT_COPY,
309 [&interaction]() { interaction.HandleCopySelected(); }, "Ctrl+C");
310 copy_item.enabled_condition = enabled_if(has_selection);
311 canvas_.AddContextMenuItem(copy_item);
312
313 gui::CanvasMenuItem paste_item(
314 "Paste", ICON_MD_CONTENT_PASTE,
315 [&interaction]() { interaction.HandlePasteObjects(); }, "Ctrl+V");
316 paste_item.enabled_condition = enabled_if(has_clipboard);
317 canvas_.AddContextMenuItem(paste_item);
318
319 gui::CanvasMenuItem duplicate_item(
320 "Duplicate", ICON_MD_CONTENT_PASTE,
321 [&interaction]() {
322 interaction.HandleCopySelected();
323 interaction.HandlePasteObjects();
324 },
325 "Ctrl+D");
326 duplicate_item.enabled_condition = enabled_if(has_selection);
327 canvas_.AddContextMenuItem(duplicate_item);
328
329 gui::CanvasMenuItem delete_item(
330 "Delete", ICON_MD_DELETE,
331 [&interaction]() { interaction.HandleDeleteSelected(); }, "Del");
332 delete_item.enabled_condition = enabled_if(has_selection);
333 canvas_.AddContextMenuItem(delete_item);
334
335 gui::CanvasMenuItem delete_all_item(
336 "Delete All Objects", ICON_MD_DELETE_FOREVER,
337 [&interaction]() { interaction.HandleDeleteAllObjects(); });
338 delete_all_item.enabled_condition = enabled_if(has_objects);
339 canvas_.AddContextMenuItem(delete_all_item);
340
341 gui::CanvasMenuItem cancel_item(
342 "Cancel Placement", ICON_MD_CANCEL,
343 [&interaction]() { interaction.CancelPlacement(); }, "Esc");
344 cancel_item.enabled_condition = enabled_if(placing_object);
345 canvas_.AddContextMenuItem(cancel_item);
346
347 // Arrange submenu (object draw order)
348 gui::CanvasMenuItem arrange_menu;
349 arrange_menu.label = "Arrange";
350 arrange_menu.icon = ICON_MD_SWAP_VERT;
351 arrange_menu.enabled_condition = enabled_if(has_selection);
352
353 gui::CanvasMenuItem bring_front_item(
354 "Bring to Front", ICON_MD_FLIP_TO_FRONT,
355 [&interaction]() { interaction.SendSelectedToFront(); },
356 "Ctrl+Shift+]");
357 bring_front_item.enabled_condition = enabled_if(has_selection);
358 arrange_menu.subitems.push_back(bring_front_item);
359
360 gui::CanvasMenuItem send_back_item(
361 "Send to Back", ICON_MD_FLIP_TO_BACK,
362 [&interaction]() { interaction.SendSelectedToBack(); }, "Ctrl+Shift+[");
363 send_back_item.enabled_condition = enabled_if(has_selection);
364 arrange_menu.subitems.push_back(send_back_item);
365
366 gui::CanvasMenuItem bring_forward_item(
367 "Bring Forward", ICON_MD_ARROW_UPWARD,
368 [&interaction]() { interaction.BringSelectedForward(); }, "Ctrl+]");
369 bring_forward_item.enabled_condition = enabled_if(has_selection);
370 arrange_menu.subitems.push_back(bring_forward_item);
371
372 gui::CanvasMenuItem send_backward_item(
373 "Send Backward", ICON_MD_ARROW_DOWNWARD,
374 [&interaction]() { interaction.SendSelectedBackward(); }, "Ctrl+[");
375 send_backward_item.enabled_condition = enabled_if(has_selection);
376 arrange_menu.subitems.push_back(send_backward_item);
377
378 canvas_.AddContextMenuItem(arrange_menu);
379
380 // Send to Layer submenu
381 gui::CanvasMenuItem layer_menu;
382 layer_menu.label = "Send to Layer";
383 layer_menu.icon = ICON_MD_LAYERS;
384 layer_menu.enabled_condition = enabled_if(has_selection);
385
386 gui::CanvasMenuItem layer1_item(
387 "Primary (main pass)", ICON_MD_LOOKS_ONE,
388 [&interaction]() { interaction.SendSelectedToLayer(0); }, "1");
389 layer1_item.enabled_condition = enabled_if(has_selection);
390 layer_menu.subitems.push_back(layer1_item);
391
392 gui::CanvasMenuItem layer2_item(
393 "BG2 overlay", ICON_MD_LOOKS_TWO,
394 [&interaction]() { interaction.SendSelectedToLayer(1); }, "2");
395 layer2_item.enabled_condition = enabled_if(has_selection);
396 layer_menu.subitems.push_back(layer2_item);
397
398 gui::CanvasMenuItem layer3_item(
399 "BG1 overlay", ICON_MD_LOOKS_3,
400 [&interaction]() { interaction.SendSelectedToLayer(2); }, "3");
401 layer3_item.enabled_condition = enabled_if(has_selection);
402 layer_menu.subitems.push_back(layer3_item);
403
404 canvas_.AddContextMenuItem(layer_menu);
405
406 // Room layout template export (available when room is loaded)
407 if (rooms_) {
408 gui::CanvasMenuItem export_layout_item(
409 "Export Layout Template...", ICON_MD_FILE_DOWNLOAD,
410 [this, room_id]() {
411 auto& room = (*rooms_)[room_id];
412 auto result = zelda3::ExportRoomLayoutTemplate(room);
413 if (result.ok()) {
414 ImGui::SetClipboardText(result.value().c_str());
415 }
416 });
417 canvas_.AddContextMenuItem(export_layout_item);
418 }
419
420 if (single_selection && rooms_) {
421 auto& room = (*rooms_)[room_id];
422 const auto& objects = room.GetTileObjects();
423 if (selected[0] < objects.size()) {
424 const auto object = objects[selected[0]];
425 gui::CanvasMenuItem edit_graphics_item(
426 "Edit Graphics...", ICON_MD_IMAGE, [this, room_id, object]() {
428 edit_graphics_callback_(room_id, object);
429 } else if (show_room_graphics_callback_) {
431 }
432 });
433 edit_graphics_item.enabled_condition =
434 enabled_if(edit_graphics_callback_ != nullptr ||
436 canvas_.AddContextMenuItem(edit_graphics_item);
437 }
438 }
439
440 // === Entity Selection Actions (Doors, Sprites, Items) ===
441 const auto& selected_entity = interaction.GetSelectedEntity();
442 const bool has_entity_selection = interaction.HasEntitySelection();
443
444 if (has_entity_selection && rooms_) {
445 auto& room = (*rooms_)[room_id];
446
447 // Show selected entity info header
448 std::string entity_info;
449 switch (selected_entity.type) {
450 case EntityType::Door: {
451 const auto& doors = room.GetDoors();
452 if (selected_entity.index < doors.size()) {
453 const auto& door = doors[selected_entity.index];
454 entity_info = absl::StrFormat(
455 ICON_MD_DOOR_FRONT " Door: %s",
456 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
457 }
458 break;
459 }
460 case EntityType::Sprite: {
461 const auto& sprites = room.GetSprites();
462 if (selected_entity.index < sprites.size()) {
463 const auto& sprite = sprites[selected_entity.index];
464 entity_info = absl::StrFormat(
465 ICON_MD_PERSON " Sprite: %s (0x%02X)",
466 zelda3::ResolveSpriteName(sprite.id()), sprite.id());
467 }
468 break;
469 }
470 case EntityType::Item: {
471 const auto& items = room.GetPotItems();
472 if (selected_entity.index < items.size()) {
473 const auto& item = items[selected_entity.index];
474 entity_info =
475 absl::StrFormat(ICON_MD_INVENTORY " Item: 0x%02X", item.item);
476 }
477 break;
478 }
479 default:
480 break;
481 }
482
483 if (!entity_info.empty()) {
485
486 // Delete entity action
487 gui::CanvasMenuItem delete_entity_item(
488 "Delete Entity", ICON_MD_DELETE,
489 [this, &room, selected_entity]() {
490 switch (selected_entity.type) {
491 case EntityType::Door: {
492 auto& doors = room.GetDoors();
493 if (selected_entity.index < doors.size()) {
494 doors.erase(doors.begin() +
495 static_cast<long>(selected_entity.index));
496 }
497 break;
498 }
499 case EntityType::Sprite: {
500 auto& sprites = room.GetSprites();
501 if (selected_entity.index < sprites.size()) {
502 sprites.erase(sprites.begin() +
503 static_cast<long>(selected_entity.index));
504 }
505 break;
506 }
507 case EntityType::Item: {
508 auto& items = room.GetPotItems();
509 if (selected_entity.index < items.size()) {
510 items.erase(items.begin() +
511 static_cast<long>(selected_entity.index));
512 }
513 break;
514 }
515 default:
516 break;
517 }
519 },
520 "Del");
521 canvas_.AddContextMenuItem(delete_entity_item);
522 }
523 }
524 }
525 if (rooms_ && rom_->is_loaded()) {
526 auto& room = (*rooms_)[room_id];
527
528 // === Room Menu (Save, Copy, Navigate, Settings, Re-render) ===
529 gui::CanvasMenuItem room_menu;
530 room_menu.label = "Room";
531 room_menu.icon = ICON_MD_HOME;
532
533 const std::string room_label = zelda3::GetRoomLabel(room_id);
534 room_menu.subitems.push_back(gui::CanvasMenuItem::Disabled(
535 absl::StrFormat("Room 0x%03X: %s", room_id, room_label.c_str())));
536
537 if (save_room_callback_) {
538 room_menu.subitems.push_back(gui::CanvasMenuItem(
539 "Save Room", ICON_MD_SAVE,
540 [this, room_id]() { save_room_callback_(room_id); }, "Ctrl+Shift+S"));
541 }
542 room_menu.subitems.push_back(
543 gui::CanvasMenuItem("Copy Room ID", ICON_MD_CONTENT_COPY, [room_id]() {
544 ImGui::SetClipboardText(absl::StrFormat("0x%03X", room_id).c_str());
545 }));
546 room_menu.subitems.push_back(gui::CanvasMenuItem(
547 "Copy Room Name", ICON_MD_CONTENT_COPY,
548 [room_label]() { ImGui::SetClipboardText(room_label.c_str()); }));
549 room_menu.subitems.push_back(gui::CanvasMenuItem(
550 "Re-render Room", ICON_MD_REFRESH,
551 [&room]() { room.RenderRoomGraphics(); }, "Ctrl+R"));
552
553 room_menu.subitems.push_back(
554 gui::CanvasMenuItem("Open Room List", ICON_MD_LIST, [this]() {
555 if (show_room_list_callback_)
556 show_room_list_callback_();
557 }));
558 room_menu.subitems.push_back(
559 gui::CanvasMenuItem("Open Room Matrix", ICON_MD_GRID_VIEW, [this]() {
560 if (show_room_matrix_callback_)
561 show_room_matrix_callback_();
562 }));
563 room_menu.subitems.push_back(
564 gui::CanvasMenuItem("Open Entrance List", ICON_MD_DOOR_FRONT, [this]() {
565 if (show_entrance_list_callback_)
566 show_entrance_list_callback_();
567 }));
568 room_menu.subitems.push_back(
569 gui::CanvasMenuItem("Open Room Graphics", ICON_MD_IMAGE, [this]() {
570 if (show_room_graphics_callback_)
571 show_room_graphics_callback_();
572 }));
573 room_menu.subitems.push_back(
574 gui::CanvasMenuItem("Dungeon Settings", ICON_MD_SETTINGS, [this]() {
575 if (show_dungeon_settings_callback_)
576 show_dungeon_settings_callback_();
577 }));
578
579 // Room layout template export (available when room is loaded)
580 room_menu.subitems.push_back(gui::CanvasMenuItem(
581 "Export Layout Template...", ICON_MD_FILE_DOWNLOAD, [this, room_id]() {
582 auto& r = (*rooms_)[room_id];
583 auto result = zelda3::ExportRoomLayoutTemplate(r);
584 if (result.ok()) {
585 ImGui::SetClipboardText(result.value().c_str());
586 }
587 }));
588
589 canvas_.AddContextMenuItem(room_menu);
590
591 // === View Menu (Layer Visibility + Entity Visibility + Grid) ===
592 gui::CanvasMenuItem view_menu;
593 view_menu.label = "View";
594 view_menu.icon = ICON_MD_VISIBILITY;
595
596 // Layer visibility toggles
597 view_menu.subitems.push_back(gui::CanvasMenuItem("BG1 Layout", [this,
598 room_id]() {
599 auto& mgr = GetRoomLayerManager(room_id);
600 mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout,
601 !mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout));
602 }));
603 view_menu.subitems.push_back(
604 gui::CanvasMenuItem("BG1 Objects", [this, room_id]() {
605 auto& mgr = GetRoomLayerManager(room_id);
606 mgr.SetLayerVisible(
608 !mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects));
609 }));
610 view_menu.subitems.push_back(gui::CanvasMenuItem("BG2 Layout", [this,
611 room_id]() {
612 auto& mgr = GetRoomLayerManager(room_id);
613 mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout,
614 !mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout));
615 }));
616 view_menu.subitems.push_back(
617 gui::CanvasMenuItem("BG2 Objects", [this, room_id]() {
618 auto& mgr = GetRoomLayerManager(room_id);
619 mgr.SetLayerVisible(
621 !mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects));
622 }));
623
624 // Entity visibility
625 view_menu.subitems.push_back(
626 gui::CanvasMenuItem("Sprites", ICON_MD_PERSON, [this]() {
627 entity_visibility_.show_sprites = !entity_visibility_.show_sprites;
628 }));
629 view_menu.subitems.push_back(
630 gui::CanvasMenuItem("Pot Items", ICON_MD_INVENTORY, [this]() {
631 entity_visibility_.show_pot_items =
632 !entity_visibility_.show_pot_items;
633 }));
634
635 // Grid options
636 view_menu.subitems.push_back(gui::CanvasMenuItem(
637 show_grid_ ? "Hide Grid" : "Show Grid",
638 show_grid_ ? ICON_MD_GRID_OFF : ICON_MD_GRID_ON,
639 [this]() { show_grid_ = !show_grid_; }, "G"));
640
641 gui::CanvasMenuItem grid_size_menu;
642 grid_size_menu.label = "Grid Size";
643 grid_size_menu.icon = ICON_MD_GRID_ON;
644 grid_size_menu.subitems.push_back(gui::CanvasMenuItem("8x8", [this]() {
645 custom_grid_size_ = 8;
646 show_grid_ = true;
647 }));
648 grid_size_menu.subitems.push_back(gui::CanvasMenuItem("16x16", [this]() {
649 custom_grid_size_ = 16;
650 show_grid_ = true;
651 }));
652 grid_size_menu.subitems.push_back(gui::CanvasMenuItem("32x32", [this]() {
653 custom_grid_size_ = 32;
654 show_grid_ = true;
655 }));
656 view_menu.subitems.push_back(grid_size_menu);
657
658 // Coordinate overlay
659 view_menu.subitems.push_back(gui::CanvasMenuItem(
660 show_coordinate_overlay_ ? "Hide Coordinates" : "Show Coordinates",
662 [this]() { show_coordinate_overlay_ = !show_coordinate_overlay_; }));
663
664 canvas_.AddContextMenuItem(view_menu);
665
666 // === Overlays Menu (custom overlays consolidated) ===
667 gui::CanvasMenuItem overlays_menu;
668 overlays_menu.label = "Overlays";
669 overlays_menu.icon = ICON_MD_LAYERS;
670
671 gui::CanvasMenuItem minecart_toggle(
672 show_minecart_tracks_ ? "Hide Minecart Tracks" : "Show Minecart Tracks",
674 [this]() { show_minecart_tracks_ = !show_minecart_tracks_; });
675 minecart_toggle.enabled_condition = [this]() {
676 return minecart_track_panel_ != nullptr;
677 };
678 overlays_menu.subitems.push_back(minecart_toggle);
679
680 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
681 show_custom_collision_overlay_ ? "Hide Custom Collision"
682 : "Show Custom Collision",
683 ICON_MD_GRID_ON, [this]() {
684 show_custom_collision_overlay_ = !show_custom_collision_overlay_;
685 }));
686
687 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
688 show_track_collision_overlay_ ? "Hide Track Collision"
689 : "Show Track Collision",
690 ICON_MD_LAYERS, [this]() {
691 show_track_collision_overlay_ = !show_track_collision_overlay_;
692 }));
693
694 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
695 show_camera_quadrant_overlay_ ? "Hide Camera Quadrants"
696 : "Show Camera Quadrants",
697 ICON_MD_GRID_VIEW, [this]() {
698 show_camera_quadrant_overlay_ = !show_camera_quadrant_overlay_;
699 }));
700
701 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
702 show_minecart_sprite_overlay_ ? "Hide Minecart Sprites"
703 : "Show Minecart Sprites",
704 ICON_MD_TRAIN, [this]() {
705 show_minecart_sprite_overlay_ = !show_minecart_sprite_overlay_;
706 }));
707
708 if (show_track_collision_overlay_) {
709 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
710 show_track_collision_legend_ ? "Hide Collision Legend"
711 : "Show Collision Legend",
712 ICON_MD_INFO, [this]() {
713 show_track_collision_legend_ = !show_track_collision_legend_;
714 }));
715 }
716
717 overlays_menu.subitems.push_back(gui::CanvasMenuItem(
718 show_object_bounds_ ? "Hide Object Bounds" : "Show Object Bounds",
720 [this]() { show_object_bounds_ = !show_object_bounds_; }));
721
722 canvas_.AddContextMenuItem(overlays_menu);
723
724 // === DEBUG MENU ===
725 gui::CanvasMenuItem debug_menu;
726 debug_menu.label = "Debug";
727 debug_menu.icon = ICON_MD_BUG_REPORT;
728
729 debug_menu.subitems.push_back(gui::CanvasMenuItem(
730 "Show Room Info", ICON_MD_INFO,
731 [this]() { show_room_debug_info_ = !show_room_debug_info_; }));
732
733 debug_menu.subitems.push_back(gui::CanvasMenuItem(
734 "Show Texture Debug", ICON_MD_IMAGE,
735 [this]() { show_texture_debug_ = !show_texture_debug_; }));
736
737 // Object bounds type/layer filter (sub-menu)
738 gui::CanvasMenuItem object_bounds_menu;
739 object_bounds_menu.label = "Object Bounds Filter";
740 object_bounds_menu.icon = ICON_MD_CROP_SQUARE;
741
742 object_bounds_menu.subitems.push_back(
743 gui::CanvasMenuItem("Type 1 (0x00-0xFF)", [this]() {
744 object_outline_toggles_.show_type1_objects =
745 !object_outline_toggles_.show_type1_objects;
746 }));
747 object_bounds_menu.subitems.push_back(
748 gui::CanvasMenuItem("Type 2 (0x100-0x1FF)", [this]() {
749 object_outline_toggles_.show_type2_objects =
750 !object_outline_toggles_.show_type2_objects;
751 }));
752 object_bounds_menu.subitems.push_back(
753 gui::CanvasMenuItem("Type 3 (0xF00-0xFFF)", [this]() {
754 object_outline_toggles_.show_type3_objects =
755 !object_outline_toggles_.show_type3_objects;
756 }));
757
758 gui::CanvasMenuItem sep;
759 sep.label = "---";
760 sep.enabled_condition = []() {
761 return false;
762 };
763 object_bounds_menu.subitems.push_back(sep);
764
765 object_bounds_menu.subitems.push_back(
766 gui::CanvasMenuItem("Primary (main pass)", [this]() {
767 object_outline_toggles_.show_layer0_objects =
768 !object_outline_toggles_.show_layer0_objects;
769 }));
770 object_bounds_menu.subitems.push_back(
771 gui::CanvasMenuItem("BG2 overlay", [this]() {
772 object_outline_toggles_.show_layer1_objects =
773 !object_outline_toggles_.show_layer1_objects;
774 }));
775 object_bounds_menu.subitems.push_back(
776 gui::CanvasMenuItem("BG1 overlay", [this]() {
777 object_outline_toggles_.show_layer2_objects =
778 !object_outline_toggles_.show_layer2_objects;
779 }));
780
781 debug_menu.subitems.push_back(object_bounds_menu);
782
783 debug_menu.subitems.push_back(gui::CanvasMenuItem(
784 "Show Layer Info", ICON_MD_LAYERS,
785 [this]() { show_layer_info_ = !show_layer_info_; }));
786
787 debug_menu.subitems.push_back(gui::CanvasMenuItem(
788 "Force Reload", ICON_MD_REFRESH,
789 [&room]() { room.ReloadGraphics(room.blockset()); }));
790
791 debug_menu.subitems.push_back(gui::CanvasMenuItem(
792 "Log Room State", ICON_MD_PRINT, [&room, room_id]() {
793 LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id);
794 LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d",
795 room.blockset(), room.palette(), room.layout_id());
796 LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu",
797 room.GetTileObjects().size(), room.GetSprites().size());
798 LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d",
799 room.bg1_buffer().bitmap().width(),
800 room.bg1_buffer().bitmap().height(),
801 room.bg2_buffer().bitmap().width(),
802 room.bg2_buffer().bitmap().height());
803 }));
804
805 canvas_.AddContextMenuItem(debug_menu);
806 }
807
808 // CRITICAL: Begin canvas frame using modern BeginCanvas/EndCanvas pattern
809 // This replaces DrawBackground + DrawContextMenu with a unified frame
810 auto canvas_rt = gui::BeginCanvas(canvas_, frame_opts);
811
812 // Handle pending scroll request using the canvas's internal scrolling model.
813 if (pending_scroll_target_.has_value()) {
814 const auto [target_x, target_y] = pending_scroll_target_.value();
815 float scale = canvas_.global_scale();
816 if (scale <= 0.0f) {
817 scale = 1.0f;
818 }
819
820 const float pixel_x =
821 static_cast<float>(target_x * kDungeonTileSize) * scale;
822 const float pixel_y =
823 static_cast<float>(target_y * kDungeonTileSize) * scale;
824 const ImVec2 view_size = canvas_rt.canvas_sz;
825 const ImVec2 content_size(static_cast<float>(kRoomPixelWidth) * scale,
826 static_cast<float>(kRoomPixelHeight) * scale);
827
828 const ImVec2 desired_scroll((view_size.x * 0.5f) - pixel_x,
829 (view_size.y * 0.5f) - pixel_y);
830 canvas_.set_scrolling(
831 gui::ClampScroll(desired_scroll, content_size, view_size));
832 canvas_rt.scrolling = canvas_.scrolling();
833
834 pending_scroll_target_.reset();
835 }
836
837 // Update touch handler for long-press gesture detection
838 touch_handler_.ProcessForCanvas(canvas_rt.canvas_p0, canvas_rt.canvas_sz,
839 canvas_rt.hovered);
840 touch_handler_.Update();
841
842 // When the header is hidden (e.g. split/compare stitched views), draw a small
843 // in-canvas label so the user always knows what they're looking at.
844 if (!header_visible_ && show_header_hidden_metadata_hud_) {
845 const auto& label = zelda3::GetRoomLabel(room_id);
846 char text1[160];
847 snprintf(text1, sizeof(text1), "[%03X] %s", room_id, label.c_str());
848
849 char text2[96] = {};
850 bool show_meta = false;
851 if (rooms_ && room_id >= 0 && room_id < static_cast<int>(rooms_->size())) {
852 const auto& room = (*rooms_)[room_id];
853 if (!object_interaction_enabled_) {
854 snprintf(text2, sizeof(text2), "B:%02X P:%02X L:%02X S:%02X RO",
855 room.blockset(), room.palette(), room.layout_id(),
856 room.spriteset());
857 } else {
858 snprintf(text2, sizeof(text2), "B:%02X P:%02X L:%02X S:%02X",
859 room.blockset(), room.palette(), room.layout_id(),
860 room.spriteset());
861 }
862 show_meta = true;
863 } else if (!object_interaction_enabled_) {
864 snprintf(text2, sizeof(text2), "Read-only");
865 show_meta = true;
866 }
867
868 const float pad = 10.0f;
869 const ImVec2 hud_pos(canvas_.zero_point().x + pad,
870 canvas_.zero_point().y + pad);
871 const ImVec2 hud_size(0, 0); // Auto-resize
872
873 gui::DrawCanvasHUD("##MetadataHUD", hud_pos, hud_size, [&]() {
874 ImGui::TextUnformatted(text1);
875 if (show_meta) {
876 ImGui::TextDisabled("%s", text2);
877 }
878 });
879 }
880
881 // Draw persistent debug overlays
882 if (show_room_debug_info_ && rooms_ && rom_->is_loaded()) {
883 auto& room = (*rooms_)[room_id];
884 ImGui::SetNextWindowPos(
885 ImVec2(canvas_.zero_point().x + 10, canvas_.zero_point().y + 10),
886 ImGuiCond_FirstUseEver);
887 ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_FirstUseEver);
888 if (ImGui::Begin("Room Debug Info", &show_room_debug_info_,
889 ImGuiWindowFlags_NoCollapse)) {
890 ImGui::Text("Room: 0x%03X (%d)", room_id, room_id);
891 ImGui::Separator();
892 ImGui::Text("Graphics");
893 ImGui::Text(" Blockset: 0x%02X", room.blockset());
894 ImGui::Text(" Palette: 0x%02X", room.palette());
895 ImGui::Text(" Layout: 0x%02X", room.layout_id());
896 ImGui::Text(" Spriteset: 0x%02X", room.spriteset());
897 ImGui::Separator();
898 ImGui::Text("Content");
899 ImGui::Text(" Objects: %zu", room.GetTileObjects().size());
900 ImGui::Text(" Sprites: %zu", room.GetSprites().size());
901 ImGui::Separator();
902 ImGui::Text("Buffers");
903 auto& bg1 = room.bg1_buffer().bitmap();
904 auto& bg2 = room.bg2_buffer().bitmap();
905 ImGui::Text(" BG1: %dx%d %s", bg1.width(), bg1.height(),
906 bg1.texture() ? "(has texture)" : "(NO TEXTURE)");
907 ImGui::Text(" BG2: %dx%d %s", bg2.width(), bg2.height(),
908 bg2.texture() ? "(has texture)" : "(NO TEXTURE)");
909 ImGui::Separator();
910 ImGui::Text("Layers (4-way)");
911 auto& layer_mgr = GetRoomLayerManager(room_id);
912 bool bg1l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Layout);
913 bool bg1o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG1_Objects);
914 bool bg2l = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Layout);
915 bool bg2o = layer_mgr.IsLayerVisible(zelda3::LayerType::BG2_Objects);
916 if (ImGui::Checkbox("BG1 Layout", &bg1l))
917 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Layout, bg1l);
918 if (ImGui::Checkbox("BG1 Objects", &bg1o))
919 layer_mgr.SetLayerVisible(zelda3::LayerType::BG1_Objects, bg1o);
920 if (ImGui::Checkbox("BG2 Layout", &bg2l))
921 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Layout, bg2l);
922 if (ImGui::Checkbox("BG2 Objects", &bg2o))
923 layer_mgr.SetLayerVisible(zelda3::LayerType::BG2_Objects, bg2o);
924 int blend = static_cast<int>(
925 layer_mgr.GetLayerBlendMode(zelda3::LayerType::BG2_Layout));
926 if (ImGui::SliderInt("BG2 Blend", &blend, 0, 4)) {
927 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Layout,
928 static_cast<zelda3::LayerBlendMode>(blend));
929 layer_mgr.SetLayerBlendMode(zelda3::LayerType::BG2_Objects,
930 static_cast<zelda3::LayerBlendMode>(blend));
931 }
932
933 ImGui::Separator();
934 ImGui::Text("Layout Override");
935 static bool enable_override = false;
936 ImGui::Checkbox("Enable Override", &enable_override);
937 if (enable_override) {
938 ImGui::SliderInt("Layout ID", &layout_override_, 0, 7);
939 } else {
940 layout_override_ = -1; // Disable override
941 }
942
943 if (show_object_bounds_) {
944 ImGui::Separator();
945 ImGui::Text("Object Outline Filters");
946 ImGui::Text("By Type:");
947 ImGui::Checkbox("Type 1", &object_outline_toggles_.show_type1_objects);
948 ImGui::Checkbox("Type 2", &object_outline_toggles_.show_type2_objects);
949 ImGui::Checkbox("Type 3", &object_outline_toggles_.show_type3_objects);
950 ImGui::Text("By Layer:");
951 ImGui::Checkbox("Primary (main pass)",
952 &object_outline_toggles_.show_layer0_objects);
953 ImGui::Checkbox("BG2 overlay",
954 &object_outline_toggles_.show_layer1_objects);
955 ImGui::Checkbox("BG1 overlay",
956 &object_outline_toggles_.show_layer2_objects);
957 }
958 }
959 ImGui::End();
960 }
961
962 if (show_texture_debug_ && rooms_ && rom_->is_loaded()) {
963 ImGui::SetNextWindowPos(
964 ImVec2(canvas_.zero_point().x + 320, canvas_.zero_point().y + 10),
965 ImGuiCond_FirstUseEver);
966 ImGui::SetNextWindowSize(ImVec2(250, 0), ImGuiCond_FirstUseEver);
967 if (ImGui::Begin("Texture Debug", &show_texture_debug_,
968 ImGuiWindowFlags_NoCollapse)) {
969 auto& room = (*rooms_)[room_id];
970 auto& bg1 = room.bg1_buffer().bitmap();
971 auto& bg2 = room.bg2_buffer().bitmap();
972
973 auto ensure_bitmap_texture = [this](gfx::Bitmap& bitmap) {
974 if (!renderer_ || !bitmap.is_active() || bitmap.width() <= 0) {
975 return;
976 }
977 if (!bitmap.texture()) {
980 } else if (bitmap.modified()) {
983 }
984 };
985
986 ensure_bitmap_texture(bg1);
987 ensure_bitmap_texture(bg2);
988 if (renderer_) {
990 }
991
992 ImGui::Text("BG1 Bitmap");
993 ImGui::Text(" Size: %dx%d", bg1.width(), bg1.height());
994 ImGui::Text(" Active: %s", bg1.is_active() ? "YES" : "NO");
995 ImGui::Text(" Texture: 0x%p", bg1.texture());
996 ImGui::Text(" Modified: %s", bg1.modified() ? "YES" : "NO");
997
998 if (bg1.texture()) {
999 ImGui::Text(" Preview:");
1000 ImGui::Image((ImTextureID)(intptr_t)bg1.texture(), ImVec2(128, 128));
1001 }
1002
1003 ImGui::Separator();
1004 ImGui::Text("BG2 Bitmap");
1005 ImGui::Text(" Size: %dx%d", bg2.width(), bg2.height());
1006 ImGui::Text(" Active: %s", bg2.is_active() ? "YES" : "NO");
1007 ImGui::Text(" Texture: 0x%p", bg2.texture());
1008 ImGui::Text(" Modified: %s", bg2.modified() ? "YES" : "NO");
1009
1010 if (bg2.texture()) {
1011 ImGui::Text(" Preview:");
1012 ImGui::Image((ImTextureID)(intptr_t)bg2.texture(), ImVec2(128, 128));
1013 }
1014 }
1015 ImGui::End();
1016 }
1017
1018 if (show_layer_info_) {
1019 ImGui::SetNextWindowPos(
1020 ImVec2(canvas_.zero_point().x + 580, canvas_.zero_point().y + 10),
1021 ImGuiCond_FirstUseEver);
1022 ImGui::SetNextWindowSize(ImVec2(220, 0), ImGuiCond_FirstUseEver);
1023 if (ImGui::Begin("Layer Info", &show_layer_info_,
1024 ImGuiWindowFlags_NoCollapse)) {
1025 ImGui::Text("Canvas Scale: %.2f", canvas_.global_scale());
1026 ImGui::Text("Canvas Size: %.0fx%.0f", canvas_.width(), canvas_.height());
1027 auto& layer_mgr = GetRoomLayerManager(room_id);
1028 ImGui::Separator();
1029 ImGui::Text("Layer Visibility (4-way):");
1030
1031 // Display each layer with visibility and blend mode
1032 for (int i = 0; i < 4; ++i) {
1033 auto layer = static_cast<zelda3::LayerType>(i);
1034 bool visible = layer_mgr.IsLayerVisible(layer);
1035 auto blend = layer_mgr.GetLayerBlendMode(layer);
1036 ImGui::Text(" %s: %s (%s)",
1038 visible ? "VISIBLE" : "hidden",
1039 zelda3::RoomLayerManager::GetBlendModeName(blend));
1040 }
1041
1042 ImGui::Separator();
1043 ImGui::Text("Draw Order:");
1044 auto draw_order = layer_mgr.GetDrawOrder();
1045 for (int i = 0; i < 4; ++i) {
1046 ImGui::Text(" %d: %s", i + 1,
1048 }
1049 ImGui::Text("BG2 On Top: %s", layer_mgr.IsBG2OnTop() ? "YES" : "NO");
1050 }
1051 ImGui::End();
1052 }
1053
1054 if (rooms_ && rom_->is_loaded()) {
1055 auto& room = (*rooms_)[room_id];
1056
1057 // Update object interaction context
1058 object_interaction_.SetCurrentRoom(rooms_, room_id);
1059
1060 if (!room.AreObjectsLoaded()) {
1061 room.LoadObjects();
1062 }
1063
1064 if (!room.AreSpritesLoaded()) {
1065 room.LoadSprites();
1066 }
1067
1068 if (!room.ArePotItemsLoaded()) {
1069 room.LoadPotItems();
1070 }
1071
1072 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1073 bool needs_render = !bg1_bitmap.is_active() || bg1_bitmap.width() == 0;
1074
1075 static int last_rendered_room = -1;
1076 static bool has_rendered = false;
1077 if (needs_render && (last_rendered_room != room_id || !has_rendered)) {
1078 (void)LoadAndRenderRoomGraphics(room_id);
1079 last_rendered_room = room_id;
1080 has_rendered = true;
1081 }
1082
1083 // CRITICAL: Process texture queue BEFORE drawing to ensure textures are
1084 // ready This must happen before DrawRoomBackgroundLayers() attempts to draw
1085 // bitmaps
1086 if (rom_ && rom_->is_loaded()) {
1088 }
1089
1090 // Draw the room's background layers to canvas
1091 // This already includes objects rendered by ObjectDrawer in
1092 // Room::RenderObjectsToBackground()
1093 DrawRoomBackgroundLayers(room_id);
1094
1095 // Draw mask highlights when mask selection mode is active
1096 // This helps visualize which objects are BG2 overlays
1097 if (object_interaction_.IsMaskModeActive()) {
1098 DrawMaskHighlights(canvas_rt, room);
1099 }
1100
1101 // Render entity overlays (sprites, pot items) as colored squares with labels
1102 // (Entities are not part of the background buffers)
1103 RenderEntityOverlay(canvas_rt, room);
1104
1105 // Handle object interaction if enabled
1106 if (object_interaction_enabled_) {
1107 object_interaction_.HandleCanvasMouseInput();
1108 object_interaction_.CheckForObjectSelection();
1109 object_interaction_
1110 .DrawSelectionHighlights(); // Draw object selection highlights
1111 object_interaction_
1112 .DrawEntitySelectionHighlights(); // Draw door/sprite/item selection
1113 object_interaction_.DrawGhostPreview(); // Draw placement preview
1114 // Context menu is handled by BeginCanvas via frame_opts.draw_context_menu
1115
1116 // --- DRAG SOURCES for selected objects/entities ---
1117 // Emit drag source for the primary selected tile object
1118 const auto selected = object_interaction_.GetSelectedObjectIndices();
1119 if (selected.size() == 1) {
1120 const auto& objects = room.GetTileObjects();
1121 size_t idx = selected.front();
1122 if (idx < objects.size()) {
1123 const auto& obj = objects[idx];
1124 gui::BeginRoomObjectDragSource(static_cast<uint16_t>(obj.id_),
1125 room_id, obj.x_, obj.y_);
1126 }
1127 }
1128
1129 // Emit drag source for selected sprite entity
1130 if (object_interaction_.HasEntitySelection()) {
1131 const auto sel = object_interaction_.GetSelectedEntity();
1132 if (sel.type == EntityType::Sprite) {
1133 const auto& sprites = room.GetSprites();
1134 if (sel.index < sprites.size()) {
1135 const auto& sprite = sprites[sel.index];
1136 gui::BeginSpriteDragSource(sprite.id(), room_id);
1137 }
1138 }
1139 }
1140
1141 // Touch long-press context menu for entity interaction
1142 HandleTouchLongPressContextMenu(canvas_rt, room);
1143 }
1144
1145 // --- DROP TARGETS on canvas ---
1146 // Accept room object drops (reposition from another room or palette)
1147 gui::RoomObjectDragPayload obj_drop;
1148 if (gui::AcceptRoomObjectDrop(&obj_drop)) {
1149 // Convert canvas mouse position to room tile coordinates
1151 ImGui::GetMousePos(), canvas_.zero_point(), canvas_.global_scale());
1152 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
1153 zelda3::RoomObject new_obj(static_cast<int16_t>(obj_drop.object_id),
1154 static_cast<uint8_t>(tile_x),
1155 static_cast<uint8_t>(tile_y), 0, 0);
1156 const size_t before = room.GetTileObjects().size();
1157 object_interaction_.entity_coordinator().tile_handler().PlaceObjectAt(
1158 room_id, new_obj, tile_x, tile_y);
1159 if (room.GetTileObjects().size() > before) {
1160 object_interaction_.SetSelectedObjects({before});
1161 }
1162 }
1163 }
1164
1165 // Accept sprite drops (reposition from another room or sprite list)
1166 gui::SpriteDragPayload sprite_drop;
1167 if (gui::AcceptSpriteDrop(&sprite_drop)) {
1169 ImGui::GetMousePos(), canvas_.zero_point(), canvas_.global_scale());
1170 // Sprites use 16-pixel units, tiles are 8-pixel
1171 int sprite_x = (tile_x * 8) / 16;
1172 int sprite_y = (tile_y * 8) / 16;
1173 if (sprite_x >= 0 && sprite_x < 32 && sprite_y >= 0 && sprite_y < 32) {
1174 // Use 5-arg constructor: (id, x, y, subtype, layer)
1175 zelda3::Sprite new_sprite(static_cast<uint8_t>(sprite_drop.sprite_id),
1176 static_cast<uint8_t>(sprite_x),
1177 static_cast<uint8_t>(sprite_y), 0, 0);
1178 if (auto* ctx = object_interaction_.entity_coordinator()
1179 .sprite_handler()
1180 .context()) {
1181 ctx->NotifyMutation(MutationDomain::kSprites);
1182 }
1183 room.GetSprites().push_back(new_sprite);
1184 if (auto* ctx = object_interaction_.entity_coordinator()
1185 .sprite_handler()
1186 .context()) {
1187 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
1188 }
1189 }
1190 }
1191 }
1192
1193 // Draw optional overlays on top of background bitmap
1194 if (rooms_ && rom_->is_loaded()) {
1195 auto& room = (*rooms_)[room_id];
1196
1197 // Draw the room layout first as the base layer
1198
1199 // VISUALIZATION: Draw object position rectangles (for debugging)
1200 // This shows where objects are placed regardless of whether graphics render
1201 if (show_object_bounds_) {
1202 DrawObjectPositionOutlines(canvas_rt, room);
1203 }
1204
1205 // Track collision overlay (custom collision tiles)
1206 if (show_track_collision_overlay_) {
1208 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1209 canvas_.global_scale(), GetCollisionOverlayCache(room.id()),
1210 track_collision_config_, track_direction_map_enabled_,
1211 track_tile_order_, switch_tile_order_, show_track_collision_legend_);
1212 }
1213
1214 if (show_custom_collision_overlay_) {
1216 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1217 canvas_.global_scale(), room);
1218 }
1219
1220 if (show_water_fill_overlay_) {
1222 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1223 canvas_.global_scale(), room);
1224 }
1225
1226 if (show_camera_quadrant_overlay_) {
1228 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1229 canvas_.global_scale(), room);
1230 }
1231
1232 if (show_minecart_sprite_overlay_) {
1234 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1235 canvas_.global_scale(), room, minecart_sprite_ids_,
1236 track_collision_config_);
1237 }
1238
1239 if (show_track_gap_overlay_) {
1241 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1242 canvas_.global_scale(), room, GetCollisionOverlayCache(room.id()));
1243 }
1244
1245 if (show_track_route_overlay_) {
1247 ImGui::GetWindowDrawList(), canvas_.zero_point(),
1248 canvas_.global_scale(), GetCollisionOverlayCache(room.id()));
1249 }
1250
1251 // Custom Objects overlay: draw a translucent cyan rectangle + label for
1252 // each tile object that uses a custom draw routine (IDs 0x31/0x32).
1253 if (show_custom_objects_overlay_) {
1254 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1255 const ImVec2 canvas_pos = canvas_.zero_point();
1256 const float scale = canvas_.global_scale();
1257 // Base the highlight on theme.info (semantically: "look here"). The
1258 // overlay reuses the accent hue at translucent/opaque alpha for fill
1259 // vs border so the pair stays visually linked under any theme.
1260 const ImVec4 info = gui::GetInfoColor();
1261 const ImU32 fill_color =
1262 ImGui::GetColorU32(ImVec4(info.x, info.y, info.z, 0.25f));
1263 const ImU32 border_color =
1264 ImGui::GetColorU32(ImVec4(info.x, info.y, info.z, 0.8f));
1265 // Text-background dark overlay stays hardcoded: it's a legibility plate
1266 // behind the label, not a themed element.
1267 const ImU32 text_bg_color = ImGui::GetColorU32(ImVec4(0, 0, 0, 0.6f));
1268
1269 // Custom draw routines are registered for object IDs 0x31 and 0x32
1270 // (Oracle of Secrets minecart track objects). Flag any object whose ID
1271 // falls in that range so the overlay is general but practically correct.
1272 auto is_custom = [](int id) {
1273 return id == 0x31 || id == 0x32;
1274 };
1275
1276 for (const auto& obj : room.GetTileObjects()) {
1277 if (!is_custom(static_cast<int>(obj.id_))) {
1278 continue;
1279 }
1280
1281 // Object positions are in tile units; canvas pixels = tile * 8 * scale.
1282 const float px = static_cast<float>(obj.x()) * 8.0f * scale;
1283 const float py = static_cast<float>(obj.y()) * 8.0f * scale;
1284
1285 // Draw a 16x16-pixel (2-tile) highlight box — small but visible.
1286 const float box_w = 16.0f * scale;
1287 const float box_h = 16.0f * scale;
1288 const ImVec2 p0(canvas_pos.x + px, canvas_pos.y + py);
1289 const ImVec2 p1(p0.x + box_w, p0.y + box_h);
1290
1291 draw_list->AddRectFilled(p0, p1, fill_color, 2.0f);
1292 draw_list->AddRect(p0, p1, border_color, 2.0f, 0, 1.5f);
1293
1294 // Label: object ID and subtype
1295 char label[32];
1296 std::snprintf(label, sizeof(label), "0x%02X s%d",
1297 static_cast<int>(obj.id_),
1298 static_cast<int>(obj.size_ & 0x1F));
1299 const ImVec2 text_sz = ImGui::CalcTextSize(label);
1300 const ImVec2 tp(p0.x + 1.0f, p0.y - text_sz.y - 1.0f);
1301 draw_list->AddRectFilled(
1302 tp, ImVec2(tp.x + text_sz.x + 2.0f, tp.y + text_sz.y),
1303 text_bg_color, 2.0f);
1304 draw_list->AddText(tp, border_color, label);
1305 }
1306 }
1307
1308 if (minecart_track_panel_) {
1309 const bool show_tracks = show_minecart_tracks_ ||
1310 minecart_track_panel_->IsPickingCoordinates();
1311 const auto& tracks = minecart_track_panel_->GetTracks();
1312 if (show_tracks && !tracks.empty()) {
1313 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1314 ImVec2 canvas_pos = canvas_.zero_point();
1315 float scale = canvas_.global_scale();
1316 const auto& theme = AgentUI::GetTheme();
1317 const int active_track =
1318 minecart_track_panel_->IsPickingCoordinates()
1319 ? minecart_track_panel_->GetPickingTrackIndex()
1320 : -1;
1321
1322 for (const auto& track : tracks) {
1324 static_cast<uint16_t>(track.start_x),
1325 static_cast<uint16_t>(track.start_y));
1326 if (local.room_id != room_id) {
1327 continue;
1328 }
1329
1330 ImVec4 marker_color = theme.selection_primary;
1331 if (track.id == active_track) {
1332 marker_color = theme.status_warning;
1333 }
1334
1335 const float px = static_cast<float>(local.local_pixel_x) * scale;
1336 const float py = static_cast<float>(local.local_pixel_y) * scale;
1337 ImVec2 center(canvas_pos.x + px, canvas_pos.y + py);
1338 const float radius = 6.0f * scale;
1339
1340 draw_list->AddCircleFilled(center, radius,
1341 ImGui::GetColorU32(marker_color));
1342 draw_list->AddCircle(center, radius + 2.0f,
1343 ImGui::GetColorU32(ImVec4(0, 0, 0, 0.6f)), 0,
1344 2.0f);
1345
1346 std::string label = absl::StrFormat("T%d", track.id);
1347 draw_list->AddText(
1348 ImVec2(center.x + 8.0f * scale, center.y - 6.0f * scale),
1349 ImGui::GetColorU32(theme.text_primary), label.c_str());
1350 }
1351 }
1352 }
1353 }
1354
1355 // Draw coordinate overlay when hovering over canvas
1356 if (show_coordinate_overlay_ && canvas_.IsMouseHovering()) {
1358 ImGui::GetMousePos(), canvas_.zero_point(), canvas_.global_scale());
1359
1360 // Only show if within bounds
1361 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
1362
1363 // Calculate logical pixel coordinates
1364 int canvas_x = tile_x * 8;
1365 int canvas_y = tile_y * 8;
1366
1367 // Calculate camera/world coordinates (for minecart tracks, sprites, etc.)
1368 auto [camera_x, camera_y] =
1369 dungeon_coords::TileToCameraCoords(room_id, tile_x, tile_y);
1370
1371 // Calculate sprite coordinates (16-pixel units)
1372 int sprite_x = canvas_x / dungeon_coords::kSpriteTileSize;
1373 int sprite_y = canvas_y / dungeon_coords::kSpriteTileSize;
1374
1375 // Draw coordinate HUD at mouse position
1376 ImVec2 mouse_pos = ImGui::GetMousePos();
1377 ImVec2 overlay_pos = ImVec2(mouse_pos.x + 15, mouse_pos.y + 15);
1378
1379 gui::DrawCanvasHUD("##CoordHUD", overlay_pos, ImVec2(0, 0), [&]() {
1380 ImGui::Text("Tile: (%d, %d)", tile_x, tile_y);
1381 ImGui::Text("Pixel: (%d, %d)", canvas_x, canvas_y);
1382 ImGui::Text("Camera: ($%04X, $%04X)", camera_x, camera_y);
1383 ImGui::Text("Sprite: (%d, %d)", sprite_x, sprite_y);
1384 });
1385 }
1386 }
1387
1388 // End canvas frame - this draws grid/overlay based on frame_opts
1389 gui::EndCanvas(canvas_, canvas_rt, frame_opts);
1390}
1391
1392void DungeonCanvasViewer::DisplayObjectInfo(const gui::CanvasRuntime& rt,
1393 const zelda3::RoomObject& object,
1394 int canvas_x, int canvas_y) {
1395 // Display object information as text overlay with hex ID and name
1396 std::string name = GetObjectName(object.id_);
1397 std::string info_text;
1398 if (object.id_ >= 0x100) {
1399 info_text =
1400 absl::StrFormat("0x%03X %s (X:%d Y:%d S:0x%02X)", object.id_,
1401 name.c_str(), object.x_, object.y_, object.size_);
1402 } else {
1403 info_text =
1404 absl::StrFormat("0x%02X %s (X:%d Y:%d S:0x%02X)", object.id_,
1405 name.c_str(), object.x_, object.y_, object.size_);
1406 }
1407
1408 // Draw text at the object position using runtime-based helper
1409 gui::DrawText(rt, info_text, canvas_x, canvas_y - 12);
1410}
1411
1412void DungeonCanvasViewer::RenderSprites(const gui::CanvasRuntime& rt,
1413 const zelda3::Room& room) {
1414 // Skip if sprites are not visible
1415 if (!entity_visibility_.show_sprites) {
1416 return;
1417 }
1418
1419 const auto& theme = AgentUI::GetTheme();
1420
1421 // Adaptive entity size: expand on touch devices for easier tapping
1422 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
1423 const int entity_size = is_touch ? 24 : 16;
1424
1425 // Render sprites as colored squares with sprite name/ID
1426 // NOTE: Sprite coordinates are in 16-pixel units (0-31 range = 512 pixels)
1427 // unlike object coordinates which are in 8-pixel tile units
1428 for (const auto& sprite : room.GetSprites()) {
1429 // Sprites use 16-pixel coordinate system
1430 int canvas_x = sprite.x() * 16;
1431 int canvas_y = sprite.y() * 16;
1432
1433 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
1434 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
1435 ImVec4 sprite_color;
1436
1437 // Color-code sprites based on layer
1438 if (sprite.layer() == 0) {
1439 sprite_color = theme.dungeon_sprite_layer0; // Green for layer 0
1440 } else {
1441 sprite_color = theme.dungeon_sprite_layer1; // Blue for layer 1
1442 }
1443
1444 // Draw square with adaptive size for touch targets
1445 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
1446 sprite_color);
1447
1448 // Draw sprite ID and name using unified ResourceLabelProvider
1449 std::string full_name = zelda3::GetSpriteLabel(sprite.id());
1450 std::string sprite_text;
1451 // Truncate long names for display
1452 if (full_name.length() > 12) {
1453 sprite_text = absl::StrFormat("%02X %s..", sprite.id(),
1454 full_name.substr(0, 8).c_str());
1455 } else {
1456 sprite_text =
1457 absl::StrFormat("%02X %s", sprite.id(), full_name.c_str());
1458 }
1459
1460 gui::DrawText(rt, sprite_text, canvas_x, canvas_y);
1461 }
1462 }
1463}
1464
1465void DungeonCanvasViewer::RenderPotItems(const gui::CanvasRuntime& rt,
1466 const zelda3::Room& room) {
1467 // Skip if pot items are not visible
1468 if (!entity_visibility_.show_pot_items) {
1469 return;
1470 }
1471
1472 const auto& pot_items = room.GetPotItems();
1473
1474 // If no pot items in this room, nothing to render
1475 if (pot_items.empty()) {
1476 return;
1477 }
1478
1479 // Pot item names
1480 static const char* kPotItemNames[] = {
1481 "Nothing", // 0
1482 "Green Rupee", // 1
1483 "Rock", // 2
1484 "Bee", // 3
1485 "Health", // 4
1486 "Bomb", // 5
1487 "Heart", // 6
1488 "Blue Rupee", // 7
1489 "Key", // 8
1490 "Arrow", // 9
1491 "Bomb", // 10
1492 "Heart", // 11
1493 "Magic", // 12
1494 "Full Magic", // 13
1495 "Cucco", // 14
1496 "Green Soldier", // 15
1497 "Bush Stal", // 16
1498 "Blue Soldier", // 17
1499 "Landmine", // 18
1500 "Heart", // 19
1501 "Fairy", // 20
1502 "Heart", // 21
1503 "Nothing", // 22
1504 "Hole", // 23
1505 "Warp", // 24
1506 "Staircase", // 25
1507 "Bombable", // 26
1508 "Switch" // 27
1509 };
1510 constexpr size_t kPotItemNameCount =
1511 sizeof(kPotItemNames) / sizeof(kPotItemNames[0]);
1512
1513 // Pot items now have their own position data from ROM
1514 // No need to match to objects - each item has exact pixel coordinates
1515 for (const auto& pot_item : pot_items) {
1516 // Get pixel coordinates from PotItem structure
1517 int pixel_x = pot_item.GetPixelX();
1518 int pixel_y = pot_item.GetPixelY();
1519
1520 // Convert to canvas coordinates (already in pixels, just need offset)
1521 // Note: pot item coords are already in full room pixel space
1522 auto [canvas_x, canvas_y] =
1523 DungeonRenderingHelpers::RoomToCanvasCoordinates(pixel_x / 8,
1524 pixel_y / 8);
1525
1526 // Adaptive entity size for touch devices
1527 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
1528 const int entity_size = is_touch ? 24 : 16;
1529
1530 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
1531 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
1532 // Draw colored square
1533 const auto& theme = AgentUI::GetTheme();
1534 ImVec4 pot_item_color;
1535 if (pot_item.item == 0) {
1536 pot_item_color = theme.status_inactive; // Muted color for Nothing
1537 pot_item_color.w = 0.4f;
1538 } else {
1539 pot_item_color = theme.item_color; // Gold/Yellow for items
1540 pot_item_color.w = 0.75f;
1541 }
1542
1543 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
1544 pot_item_color);
1545
1546 // Get item name
1547 std::string item_name;
1548 if (pot_item.item < kPotItemNameCount) {
1549 item_name = kPotItemNames[pot_item.item];
1550 } else {
1551 item_name = absl::StrFormat("Unk%02X", pot_item.item);
1552 }
1553
1554 // Draw label above the box
1555 std::string item_text =
1556 absl::StrFormat("%02X %s", pot_item.item, item_name.c_str());
1557 gui::DrawText(rt, item_text, canvas_x, canvas_y - 10);
1558 }
1559 }
1560}
1561
1562void DungeonCanvasViewer::RenderEntityOverlay(const gui::CanvasRuntime& rt,
1563 const zelda3::Room& room) {
1564 // Render all entity overlays using runtime-based helpers
1565 RenderSprites(rt, room);
1566 RenderPotItems(rt, room);
1567}
1568
1569void DungeonCanvasViewer::HandleTouchLongPressContextMenu(
1570 const gui::CanvasRuntime& rt, const zelda3::Room& room) {
1571 constexpr const char* kPopupId = "##TouchEntityContextMenu";
1572 const ImGuiIO& io = ImGui::GetIO();
1573 const bool touch_context_click =
1574 rt.hovered && io.MouseSource == ImGuiMouseSource_TouchScreen &&
1575 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
1576 const bool gesture_long_press = touch_handler_.WasLongPressed();
1577 const bool should_open_context = gesture_long_press || touch_context_click;
1578
1579 // On long-press, hit-test entities at the gesture position and open popup.
1580 // iOS maps long-press to right-click; treat that as a touch context gesture.
1581 if (should_open_context) {
1582 ImVec2 gesture_pos = gesture_long_press
1583 ? touch_handler_.GetGesturePosition()
1584 : ImGui::GetMousePos();
1585 float scale = rt.scale > 0.0f ? rt.scale : 1.0f;
1586
1587 // Convert screen position to room pixel coordinates
1588 float rel_x = (gesture_pos.x - rt.canvas_p0.x) / scale;
1589 float rel_y = (gesture_pos.y - rt.canvas_p0.y) / scale;
1590
1591 // Adaptive hit-test size for touch devices
1592 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
1593 const int hit_size = is_touch ? 24 : 16;
1594
1595 // Hit-test sprites
1596 const auto& sprites = room.GetSprites();
1597 for (size_t idx = 0; idx < sprites.size(); ++idx) {
1598 int sprite_px = sprites[idx].x() * 16;
1599 int sprite_py = sprites[idx].y() * 16;
1600 if (rel_x >= sprite_px && rel_x < sprite_px + hit_size &&
1601 rel_y >= sprite_py && rel_y < sprite_py + hit_size) {
1602 object_interaction_.SelectEntity(EntityType::Sprite, idx);
1603 ImGui::OpenPopup(kPopupId);
1604 break;
1605 }
1606 }
1607
1608 // Hit-test pot items
1609 if (!ImGui::IsPopupOpen(kPopupId)) {
1610 const auto& pot_items = room.GetPotItems();
1611 for (size_t idx = 0; idx < pot_items.size(); ++idx) {
1612 int item_px = pot_items[idx].GetPixelX();
1613 int item_py = pot_items[idx].GetPixelY();
1614 if (rel_x >= item_px && rel_x < item_px + hit_size &&
1615 rel_y >= item_py && rel_y < item_py + hit_size) {
1616 object_interaction_.SelectEntity(EntityType::Item, idx);
1617 ImGui::OpenPopup(kPopupId);
1618 break;
1619 }
1620 }
1621 }
1622
1623 // Hit-test tile objects (variable-size entities)
1624 if (!ImGui::IsPopupOpen(kPopupId)) {
1625 const auto& objects = room.GetTileObjects();
1626 for (size_t idx = 0; idx < objects.size(); ++idx) {
1627 const auto& obj = objects[idx];
1628 int obj_px = obj.x() * 8;
1629 int obj_py = obj.y() * 8;
1630 auto [obj_w, obj_h] =
1632 obj_w = std::max(obj_w, 8);
1633 obj_h = std::max(obj_h, 8);
1634 if (rel_x >= obj_px && rel_x < obj_px + obj_w && rel_y >= obj_py &&
1635 rel_y < obj_py + obj_h) {
1636 object_interaction_.SetSelectedObjects({idx});
1637 ImGui::OpenPopup(kPopupId);
1638 break;
1639 }
1640 }
1641 }
1642 }
1643
1644 // Render the context popup
1645 if (ImGui::BeginPopup(kPopupId)) {
1646 // Show actions based on what's selected
1647 if (object_interaction_.HasEntitySelection()) {
1648 auto sel = object_interaction_.GetSelectedEntity();
1649 if (sel.type == EntityType::Sprite) {
1650 const auto& sprites = room.GetSprites();
1651 if (sel.index < sprites.size()) {
1652 std::string label = zelda3::GetSpriteLabel(sprites[sel.index].id());
1653 ImGui::TextDisabled("Sprite: %02X %s", sprites[sel.index].id(),
1654 label.c_str());
1655 ImGui::Separator();
1656 }
1657 if (ImGui::MenuItem("Delete Sprite")) {
1658 object_interaction_.entity_coordinator().DeleteSelectedEntity();
1659 }
1660 } else if (sel.type == EntityType::Item) {
1661 ImGui::TextDisabled("Pot Item");
1662 ImGui::Separator();
1663 if (ImGui::MenuItem("Delete Item")) {
1664 object_interaction_.entity_coordinator().DeleteSelectedEntity();
1665 }
1666 }
1667 } else if (object_interaction_.GetSelectionCount() > 0) {
1668 const auto indices = object_interaction_.GetSelectedObjectIndices();
1669 if (indices.size() == 1) {
1670 const auto& objects = room.GetTileObjects();
1671 if (indices[0] < objects.size()) {
1672 std::string name = GetObjectName(objects[indices[0]].id_);
1673 ImGui::TextDisabled("Object: %03X %s", objects[indices[0]].id_,
1674 name.c_str());
1675 ImGui::Separator();
1676 }
1677 } else {
1678 ImGui::TextDisabled("%zu objects selected",
1679 object_interaction_.GetSelectionCount());
1680 ImGui::Separator();
1681 }
1682 if (ImGui::MenuItem("Delete")) {
1683 object_interaction_.HandleDeleteSelected();
1684 }
1685 if (ImGui::MenuItem("Copy")) {
1686 object_interaction_.HandleCopySelected();
1687 }
1688 ImGui::Separator();
1689 if (ImGui::MenuItem("Send to Front")) {
1690 object_interaction_.SendSelectedToFront();
1691 }
1692 if (ImGui::MenuItem("Send to Back")) {
1693 object_interaction_.SendSelectedToBack();
1694 }
1695 }
1696 ImGui::EndPopup();
1697 }
1698}
1699
1700// Room layout visualization
1701
1702// Object visualization methods
1703void DungeonCanvasViewer::DrawObjectPositionOutlines(
1704 const gui::CanvasRuntime& rt, const zelda3::Room& room) {
1705 // Draw colored rectangles showing object positions
1706 // This helps visualize object placement even if graphics don't render
1707 // correctly
1708
1709 const auto& theme = AgentUI::GetTheme();
1710 const auto& objects = room.GetTileObjects();
1711
1712 for (const auto& obj : objects) {
1713 // Filter by object type (default to true if unknown type)
1714 bool show_this_type = true; // Default to showing
1715 if (obj.id_ < 0x100) {
1716 show_this_type = object_outline_toggles_.show_type1_objects;
1717 } else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
1718 show_this_type = object_outline_toggles_.show_type2_objects;
1719 } else if (obj.id_ >= 0xF00) {
1720 show_this_type = object_outline_toggles_.show_type3_objects;
1721 }
1722 // else: unknown type, use default (true)
1723
1724 // Filter by layer (default to true if unknown layer)
1725 bool show_this_layer = true; // Default to showing
1726 if (obj.GetLayerValue() == 0) {
1727 show_this_layer = object_outline_toggles_.show_layer0_objects;
1728 } else if (obj.GetLayerValue() == 1) {
1729 show_this_layer = object_outline_toggles_.show_layer1_objects;
1730 } else if (obj.GetLayerValue() == 2) {
1731 show_this_layer = object_outline_toggles_.show_layer2_objects;
1732 }
1733 // else: unknown layer, use default (true)
1734
1735 // Skip if filtered out
1736 if (!show_this_type || !show_this_layer) {
1737 continue;
1738 }
1739
1740 // Use GetSelectionBoundsPixels which includes position offsets for objects
1741 // that extend in negative directions (diagonals, moving walls, etc.)
1742 auto [canvas_x, canvas_y, width, height] =
1744
1745 // IMPORTANT: Do NOT apply canvas scale here - DrawRect handles it
1746 // Clamp to reasonable sizes (in logical space)
1747 width = std::min(width, 512);
1748 height = std::min(height, 512);
1749
1750 // Color-code by layer
1751 ImVec4 outline_color;
1752 if (obj.GetLayerValue() == 0) {
1753 outline_color = theme.dungeon_outline_layer0; // Red for layer 0
1754 } else if (obj.GetLayerValue() == 1) {
1755 outline_color = theme.dungeon_outline_layer1; // Green for layer 1
1756 } else {
1757 outline_color = theme.dungeon_outline_layer2; // Blue for layer 2
1758 }
1759
1760 // Draw outline rectangle using runtime-based helper
1761 gui::DrawRect(rt, canvas_x, canvas_y, width, height, outline_color);
1762
1763 // Draw object ID label with hex ID, abbreviated name, and draw stream.
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%s [%dx%d]", obj.id_, name.c_str(),
1772 GetObjectStreamLabel(obj.GetLayerValue()), width,
1773 height);
1774 } else {
1775 label = absl::StrFormat("0x%02X\n%s\n%s [%dx%d]", obj.id_, name.c_str(),
1776 GetObjectStreamLabel(obj.GetLayerValue()), width,
1777 height);
1778 }
1779 gui::DrawText(rt, label, canvas_x + 1, canvas_y + 1);
1780 }
1781}
1782
1784DungeonCanvasViewer::GetCollisionOverlayCache(int room_id) {
1785 auto it = collision_overlay_cache_.find(room_id);
1786 if (it != collision_overlay_cache_.end()) {
1787 return it->second;
1788 }
1789
1791 cache.entries.clear();
1792
1793 if (!rom_ || !rom_->is_loaded()) {
1794 collision_overlay_cache_.emplace(room_id, cache);
1795 return collision_overlay_cache_.at(room_id);
1796 }
1797
1798 auto map_or = zelda3::LoadCustomCollisionMap(rom_, room_id);
1799 if (!map_or.ok()) {
1800 collision_overlay_cache_.emplace(room_id, cache);
1801 return collision_overlay_cache_.at(room_id);
1802 }
1803
1804 const auto& map = map_or.value();
1805 cache.has_data = map.has_data;
1806 if (cache.has_data && !track_collision_config_.IsEmpty()) {
1807 for (int y = 0; y < 64; ++y) {
1808 for (int x = 0; x < 64; ++x) {
1809 const uint8_t tile = map.tiles[static_cast<size_t>(y * 64 + x)];
1810 if (tile < 256 && (track_collision_config_.track_tiles[tile] ||
1811 track_collision_config_.stop_tiles[tile] ||
1812 track_collision_config_.switch_tiles[tile])) {
1813 cache.entries.push_back(
1815 static_cast<uint8_t>(x), static_cast<uint8_t>(y), tile});
1816 }
1817 }
1818 }
1819 }
1820
1821 collision_overlay_cache_.emplace(room_id, std::move(cache));
1822 return collision_overlay_cache_.at(room_id);
1823}
1824
1825// Room graphics management methods
1826absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(int room_id) {
1827 LOG_DEBUG("[LoadAndRender]", "START room_id=%d", room_id);
1828
1829 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
1830 LOG_DEBUG("[LoadAndRender]", "ERROR: Invalid room ID");
1831 return absl::InvalidArgumentError("Invalid room ID");
1832 }
1833
1834 if (!rom_ || !rom_->is_loaded()) {
1835 LOG_DEBUG("[LoadAndRender]", "ERROR: ROM not loaded");
1836 return absl::FailedPreconditionError("ROM not loaded");
1837 }
1838
1839 if (!rooms_) {
1840 LOG_DEBUG("[LoadAndRender]", "ERROR: Room data not available");
1841 return absl::FailedPreconditionError("Room data not available");
1842 }
1843
1844 auto& room = (*rooms_)[room_id];
1845 LOG_DEBUG("[LoadAndRender]", "Got room reference");
1846
1847 // Load the room's palette with bounds checking
1848 if (!game_data_) {
1849 LOG_ERROR("[LoadAndRender]", "GameData not available");
1850 return absl::FailedPreconditionError("GameData not available");
1851 }
1852 const auto& dungeon_main = game_data_->palette_groups.dungeon_main;
1853 if (!dungeon_main.empty()) {
1854 // Use Room's canonical two-level palette resolver; it already clamps to
1855 // the dungeon_main group size.
1856 current_palette_group_id_ =
1857 static_cast<uint64_t>(room.ResolveDungeonPaletteId());
1858
1859 auto full_palette = dungeon_main[current_palette_group_id_];
1860 ASSIGN_OR_RETURN(current_palette_group_,
1861 gfx::CreatePaletteGroupFromLargePalette(full_palette, 16));
1862 LOG_DEBUG("[LoadAndRender]", "Palette loaded: group_id=%zu",
1863 current_palette_group_id_);
1864 }
1865
1866 // Render the room graphics (self-contained - handles all palette application)
1867 LOG_DEBUG("[LoadAndRender]", "Calling room.RenderRoomGraphics()...");
1868 room.ReloadGraphics(room.blockset());
1869 LOG_DEBUG("[LoadAndRender]",
1870 "RenderRoomGraphics() complete - room buffers self-contained");
1871
1872 LOG_DEBUG("[LoadAndRender]", "SUCCESS");
1873 return absl::OkStatus();
1874}
1875
1876void DungeonCanvasViewer::DrawRoomBackgroundLayers(int room_id) {
1877 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms || !rooms_)
1878 return;
1879
1880 auto& room = (*rooms_)[room_id];
1881 auto& layer_mgr = GetRoomLayerManager(room_id);
1882
1883 // Apply room's layer merging settings to the manager
1884 layer_mgr.ApplyLayerMerging(room.layer_merging());
1885 layer_mgr.ApplyRoomEffect(room.effect());
1886
1887 float scale = canvas_.global_scale();
1888
1889 // Always use composite mode: single merged bitmap with back-to-front layer order
1890 // This matches SNES hardware behavior where BG2 is drawn first, then BG1 on top
1891 auto& composite = room.GetCompositeBitmap(layer_mgr);
1892 if (composite.is_active() && composite.width() > 0) {
1893 // Ensure texture exists or is updated when bitmap data changes
1894 if (!composite.texture()) {
1897 composite.set_modified(false);
1898 } else if (composite.modified()) {
1899 // Update texture when bitmap was regenerated
1902 composite.set_modified(false);
1903 }
1904 if (composite.texture()) {
1905 canvas_.DrawBitmap(composite, 0, 0, scale, 255);
1906 }
1907 }
1908}
1909
1910void DungeonCanvasViewer::DrawMaskHighlights(const gui::CanvasRuntime& rt,
1911 const zelda3::Room& room) {
1912 // Draw semi-transparent blue overlay on BG2/Layer 1 objects when mask mode
1913 // is active. This helps identify which objects are the "overlay" content
1914 // (platforms, statues, stairs) that create transparency holes in BG1.
1915 const auto& objects = room.GetTileObjects();
1916
1917 // Create ObjectDrawer for dimension calculation
1918 zelda3::ObjectDrawer drawer(const_cast<zelda3::Room&>(room).rom(), room.id(),
1919 nullptr);
1920
1921 // Mask highlight color: semi-transparent cyan/blue
1922 // DrawRect draws a filled rectangle with a black outline
1923 ImVec4 mask_color(0.2f, 0.6f, 1.0f, 0.4f); // Light blue, 40% opacity
1924
1925 for (const auto& obj : objects) {
1926 // Only highlight Layer 1 (BG2) objects - these are the mask/overlay objects
1927 if (obj.GetLayerValue() != 1) {
1928 continue;
1929 }
1930
1931 // Convert object position to canvas coordinates
1932 auto [canvas_x, canvas_y] =
1933 DungeonRenderingHelpers::RoomToCanvasCoordinates(obj.x(), obj.y());
1934
1935 // Calculate object dimensions via DimensionService
1936 auto [width, height] =
1938
1939 // Clamp to reasonable sizes
1940 width = std::min(width, 512);
1941 height = std::min(height, 512);
1942
1943 // Draw filled rectangle with semi-transparent overlay (includes black outline)
1944 gui::DrawRect(rt, canvas_x, canvas_y, width, height, mask_color);
1945 }
1946}
1947
1948void DungeonCanvasViewer::DrawRoomHeader(zelda3::Room& room, int room_id) {
1949 ImGui::Separator();
1950 if (header_read_only_)
1951 ImGui::BeginDisabled();
1952
1953 constexpr ImGuiTableFlags kPropsTableFlags =
1954 ImGuiTableFlags_NoPadOuterX | ImGuiTableFlags_NoBordersInBody;
1955
1956 if (ImGui::BeginTable("##RoomPropsTable", 2, kPropsTableFlags)) {
1957 const float nav_col_width = (ImGui::GetFrameHeight() * 4.0f) +
1958 (ImGui::GetStyle().ItemSpacing.x * 3.0f) +
1959 (ImGui::GetStyle().FramePadding.x * 2.0f);
1960 ImGui::TableSetupColumn("NavCol", ImGuiTableColumnFlags_WidthFixed,
1961 nav_col_width);
1962 ImGui::TableSetupColumn("PropsCol", ImGuiTableColumnFlags_WidthStretch);
1963
1964 ImGui::TableNextRow();
1965 ImGui::TableNextColumn();
1966 DrawRoomNavigation(room_id);
1967 ImGui::TableNextColumn();
1968 DrawRoomPropertyTable(room, room_id);
1969
1970 if (!compact_header_mode_ || show_room_details_) {
1971 ImGui::TableNextRow();
1972 ImGui::TableNextColumn();
1973 ImGui::TextDisabled(ICON_MD_SELECT_ALL " Select");
1974 ImGui::TableNextColumn();
1975 DrawLayerControls(room, room_id);
1976 }
1977
1978 ImGui::EndTable();
1979 }
1980
1981 if (header_read_only_)
1982 ImGui::EndDisabled();
1983}
1984
1985void DungeonCanvasViewer::DrawRoomNavigation(int room_id) {
1986 if (!room_swap_callback_ && !room_navigation_callback_)
1987 return;
1988
1989 const int col = room_id % kRoomMatrixCols;
1990 const int row = room_id / kRoomMatrixCols;
1991
1992 auto room_if_valid = [](int candidate) -> std::optional<int> {
1993 if (candidate < 0 || candidate >= zelda3::kNumberOfRooms) {
1994 return std::nullopt;
1995 }
1996 return candidate;
1997 };
1998
1999 const auto north = room_if_valid(row > 0 ? room_id - kRoomMatrixCols : -1);
2000 const auto south =
2001 room_if_valid(row < kRoomMatrixRows - 1 ? room_id + kRoomMatrixCols : -1);
2002 const auto west = room_if_valid(col > 0 ? room_id - 1 : -1);
2003 const auto east = room_if_valid(col < kRoomMatrixCols - 1 ? room_id + 1 : -1);
2004
2005 auto make_tooltip = [](const std::optional<int>& target,
2006 const char* direction) -> std::string {
2007 if (!target.has_value())
2008 return "";
2009 return absl::StrFormat("%s: [%03X] %s", direction, *target,
2010 zelda3::GetRoomLabel(*target));
2011 };
2012
2013 auto nav_button = [&](const char* id, ImGuiDir dir,
2014 const std::optional<int>& target,
2015 const std::string& tooltip) {
2016 const bool enabled = target.has_value();
2017 if (!enabled) {
2018 ImGui::BeginDisabled();
2019 }
2020 const bool pressed = ImGui::ArrowButton(id, dir);
2021 if (!enabled) {
2022 ImGui::EndDisabled();
2023 }
2024 if (enabled && ImGui::IsItemHovered() && !tooltip.empty()) {
2025 ImGui::SetTooltip("%s", tooltip.c_str());
2026 }
2027 if (pressed && enabled) {
2028 if (room_swap_callback_) {
2029 room_swap_callback_(room_id, *target);
2030 } else if (room_navigation_callback_) {
2031 room_navigation_callback_(*target);
2032 }
2033 }
2034 };
2035
2036 ImGui::PushID(room_id);
2037 ImGui::BeginGroup();
2038 nav_button("##RoomNavWest", ImGuiDir_Left, west, make_tooltip(west, "West"));
2039 ImGui::SameLine();
2040 nav_button("##RoomNavNorth", ImGuiDir_Up, north,
2041 make_tooltip(north, "North"));
2042 ImGui::SameLine();
2043 nav_button("##RoomNavSouth", ImGuiDir_Down, south,
2044 make_tooltip(south, "South"));
2045 ImGui::SameLine();
2046 nav_button("##RoomNavEast", ImGuiDir_Right, east, make_tooltip(east, "East"));
2047 ImGui::EndGroup();
2048 ImGui::PopID();
2049}
2050
2051void DungeonCanvasViewer::DrawRoomPropertyTable(zelda3::Room& room,
2052 int room_id) {
2053 ImGui::AlignTextToFramePadding();
2054 ImGui::Text(ICON_MD_TUNE " %03X", room_id);
2055 ImGui::SameLine();
2056
2057 if (pin_callback_) {
2059 is_pinned_ ? "Unpin Room" : "Pin Room",
2060 ImVec2(0, 0), is_pinned_)) {
2061 pin_callback_(!is_pinned_);
2062 }
2063 ImGui::SameLine();
2064 }
2065
2067 show_room_details_ ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE,
2068 show_room_details_ ? "Hide Details" : "Show Details")) {
2069 show_room_details_ = !show_room_details_;
2070 }
2071 ImGui::SameLine();
2072
2073 // Core properties with human-readable names
2074 auto hex_input = [&](const char* label, const char* icon, uint8_t* val,
2075 uint8_t max, const char* tooltip) {
2076 ImGui::TextDisabled("%s", icon);
2077 ImGui::SameLine(0, 2);
2078
2079 // Apply flash feedback to the background of the input
2080 const std::string anim_id = std::string(label) + "_Flash";
2081 const ImVec4 flash_color = gui::GetAnimator().AnimateColor(
2082 "##RoomProps", anim_id, ImVec4(0, 0, 0, 0), 8.0f);
2083
2084 if (flash_color.w > 0.01f) {
2085 ImGui::PushStyleColor(ImGuiCol_FrameBg, flash_color);
2086 }
2087
2088 auto res = gui::InputHexByteEx(label, val, max, 32.f, true);
2089 bool changed = res.ShouldApply();
2090
2091 if (flash_color.w > 0.01f) {
2092 ImGui::PopStyleColor();
2093 }
2094
2095 gui::ValueChangeFlash(changed, anim_id.c_str());
2096
2097 if (changed) {
2098 return true;
2099 }
2100 if (ImGui::IsItemHovered())
2101 ImGui::SetTooltip("%s", tooltip);
2102 return false;
2103 };
2104
2105 uint8_t bs = room.blockset();
2106 if (hex_input("##BS", ICON_MD_VIEW_MODULE, &bs, 81, "Blockset")) {
2107 room.SetBlockset(bs);
2108 if (room.rom() && room.rom()->is_loaded())
2109 room.RenderRoomGraphics();
2110 }
2111 // Show dungeon name after blockset hex input
2112 ImGui::SameLine(0, 2);
2113 ImGui::TextDisabled("(%s)", DungeonRoomSelector::GetBlocksetGroupName(bs));
2114 ImGui::SameLine();
2115
2116 uint8_t pal = room.palette();
2117 if (hex_input("##Pal", ICON_MD_PALETTE, &pal, 71, "Palette")) {
2118 room.SetPalette(pal);
2119 if (room.rom() && room.rom()->is_loaded())
2120 room.RenderRoomGraphics();
2121 }
2122 ImGui::SameLine();
2123
2124 uint8_t lyr = room.layout_id();
2125 if (hex_input("##Lyr", ICON_MD_GRID_VIEW, &lyr, 7, "Layout")) {
2126 room.SetLayoutId(lyr);
2127 room.MarkLayoutDirty();
2128 if (room.rom() && room.rom()->is_loaded())
2129 room.RenderRoomGraphics();
2130 }
2131 ImGui::SameLine();
2132
2133 uint8_t ss = room.spriteset();
2134 if (hex_input("##SS", ICON_MD_PEST_CONTROL, &ss, 143, "Spriteset")) {
2135 room.SetSpriteset(ss);
2136 if (room.rom() && room.rom()->is_loaded())
2137 room.RenderRoomGraphics();
2138 }
2139
2140 if (show_room_details_) {
2141 // Show extended properties
2142 ImGui::TextDisabled("Floor: %d | Effect: %d | Tag1: %d | Tag2: %d",
2143 room.floor1(), room.effect(), room.tag1(), room.tag2());
2144 }
2145}
2146
2147void DungeonCanvasViewer::DrawCompactLayerToggles(int room_id) {
2148 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
2149 return;
2150 }
2151
2152 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
2153 const float compact_gap =
2154 std::max(2.0f, gui::LayoutHelpers::GetStandardSpacing() * 0.25f);
2155 const float compact_padding =
2156 std::clamp(gui::LayoutHelpers::GetButtonPadding(), 2.0f, 6.0f);
2157
2158 gui::StyleVarGuard compact_style({
2159 {ImGuiStyleVar_FramePadding,
2160 ImVec2(compact_padding, compact_padding * 0.5f)},
2161 {ImGuiStyleVar_ItemSpacing, ImVec2(compact_gap, 0.0f)},
2162 });
2163
2164 auto as_button_color = [](ImVec4 color, float alpha) {
2165 color.w = alpha;
2166 return color;
2167 };
2168
2169 const ImVec4 inactive_color =
2170 as_button_color(gui::ConvertColorToImVec4(theme.frame_bg), 0.55f);
2171 const ImVec4 inactive_hover =
2172 as_button_color(gui::ConvertColorToImVec4(theme.frame_bg_hovered), 0.7f);
2173 const ImVec4 inactive_active =
2174 as_button_color(gui::ConvertColorToImVec4(theme.frame_bg_active), 0.85f);
2175
2176 auto draw_toggle = [&](const char* label, bool enabled, ImVec4 active_color,
2177 const char* tooltip, auto&& on_toggle) {
2178 const ImVec4 button = enabled ? active_color : inactive_color;
2179 const ImVec4 hovered =
2180 enabled ? as_button_color(
2181 gui::ConvertColorToImVec4(theme.button_hovered), 0.95f)
2182 : inactive_hover;
2183 const ImVec4 pressed =
2184 enabled ? as_button_color(
2185 gui::ConvertColorToImVec4(theme.button_active), 1.0f)
2186 : inactive_active;
2187
2188 gui::StyleColorGuard button_colors({
2189 {ImGuiCol_Button, button},
2190 {ImGuiCol_ButtonHovered, hovered},
2191 {ImGuiCol_ButtonActive, pressed},
2192 });
2193
2194 if (ImGui::SmallButton(label)) {
2195 on_toggle();
2196 }
2197 if (ImGui::IsItemHovered()) {
2198 ImGui::SetTooltip("%s", tooltip);
2199 }
2200 };
2201
2202 const bool bg1_visible = IsBG1Visible(room_id);
2203 draw_toggle("BG1##LayerToggleBG1", bg1_visible,
2204 as_button_color(gui::ConvertColorToImVec4(theme.info), 0.9f),
2205 "Toggle BG1 (main layer) visibility",
2206 [&]() { SetBG1Visible(room_id, !bg1_visible); });
2207
2208 ImGui::SameLine();
2209 const bool bg2_visible = IsBG2Visible(room_id);
2210 draw_toggle("BG2##LayerToggleBG2", bg2_visible,
2211 as_button_color(gui::ConvertColorToImVec4(theme.warning), 0.9f),
2212 "Toggle BG2 (overlay layer) visibility",
2213 [&]() { SetBG2Visible(room_id, !bg2_visible); });
2214
2215 ImGui::SameLine();
2216 const bool sprites_visible = entity_visibility_.show_sprites;
2217 draw_toggle(ICON_MD_PEST_CONTROL "##LayerToggleSprites", sprites_visible,
2218 as_button_color(gui::ConvertColorToImVec4(theme.success), 0.9f),
2219 "Toggle sprite visibility", [&]() {
2220 entity_visibility_.show_sprites =
2221 !entity_visibility_.show_sprites;
2222 });
2223
2224 ImGui::SameLine();
2225 draw_toggle(ICON_MD_GRID_ON "##LayerToggleGrid", show_grid_,
2226 as_button_color(gui::ConvertColorToImVec4(theme.secondary), 0.9f),
2227 "Toggle grid overlay", [&]() { show_grid_ = !show_grid_; });
2228
2229 ImGui::SameLine();
2230 draw_toggle(
2231 ICON_MD_CROP_FREE "##LayerToggleBounds", show_object_bounds_,
2232 as_button_color(gui::ConvertColorToImVec4(theme.selection_primary), 0.9f),
2233 "Toggle object bounds overlay",
2234 [&]() { show_object_bounds_ = !show_object_bounds_; });
2235
2236 ImGui::SameLine();
2237 const bool pots_visible = entity_visibility_.show_pot_items;
2238 draw_toggle(ICON_MD_INVENTORY_2 "##LayerTogglePots", pots_visible,
2239 as_button_color(gui::ConvertColorToImVec4(theme.success), 0.9f),
2240 "Toggle pot item markers", [&]() {
2241 entity_visibility_.show_pot_items =
2242 !entity_visibility_.show_pot_items;
2243 });
2244
2245 ImGui::SameLine();
2246 draw_toggle(
2247 ICON_MD_FILTER_CENTER_FOCUS "##LayerToggleCollision",
2248 show_custom_collision_overlay_,
2249 as_button_color(gui::ConvertColorToImVec4(theme.warning), 0.9f),
2250 "Toggle custom collision overlay", [&]() {
2251 show_custom_collision_overlay_ = !show_custom_collision_overlay_;
2252 });
2253}
2254
2255void DungeonCanvasViewer::DrawLayerControls(zelda3::Room& /*room*/,
2256 int room_id) {
2257 auto& interaction = object_interaction_;
2258
2259 interaction.SetLayersMerged(GetRoomLayerManager(room_id).AreLayersMerged());
2260 int current_filter = interaction.GetLayerFilter();
2261
2262 auto radio = [&](const char* label, int filter) {
2263 if (ImGui::RadioButton(label, current_filter == filter)) {
2264 interaction.SetLayerFilter(filter);
2265 }
2266 ImGui::SameLine();
2267 };
2268
2269 radio("All", ObjectSelection::kLayerAll);
2270 radio("L1", ObjectSelection::kLayer1);
2271 radio("L2", ObjectSelection::kLayer2);
2272 radio("L3", ObjectSelection::kLayer3);
2273}
2274
2275} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:132
void SetProject(const project::YazeProject *project)
std::function< void(int, const zelda3::RoomObject &) edit_graphics_callback_)
DungeonObjectInteraction object_interaction_
const project::YazeProject * project_
std::function< void()> show_object_panel_callback_
std::unordered_map< int, DungeonRenderingHelpers::CollisionOverlayCache > collision_overlay_cache_
std::function< void()> show_room_graphics_callback_
void DrawRoomHeader(zelda3::Room &room, int room_id)
std::function< void()> show_item_panel_callback_
std::function< void()> show_sprite_panel_callback_
DungeonRenderingHelpers::TrackCollisionConfig track_collision_config_
std::vector< size_t > GetSelectedObjectIndices() const
static void DrawCustomCollisionOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const zelda3::Room &room)
static void DrawTrackGapOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const zelda3::Room &room, const CollisionOverlayCache &cache)
static void DrawTrackCollisionOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const CollisionOverlayCache &cache, const TrackCollisionConfig &config, bool direction_map_enabled, const std::vector< uint16_t > &track_tile_order, const std::vector< uint16_t > &switch_tile_order, bool show_legend)
static void DrawCameraQuadrantOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const zelda3::Room &room)
static void DrawTrackRouteOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const CollisionOverlayCache &cache)
static void DrawMinecartSpriteOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const zelda3::Room &room, const std::bitset< 256 > &minecart_sprite_ids, const TrackCollisionConfig &config)
static void DrawWaterFillOverlay(ImDrawList *draw_list, const ImVec2 &canvas_pos, float scale, const zelda3::Room &room)
static std::pair< int, int > ScreenToRoomCoordinates(const ImVec2 &screen_pos, const ImVec2 &zero_point, float scale)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
ImVec4 AnimateColor(const std::string &panel_id, const std::string &anim_id, ImVec4 target, float speed=5.0f)
Definition animator.cc:64
void ClearContextMenuItems()
Definition canvas.cc:858
void AddContextMenuItem(const gui::CanvasMenuItem &item)
Definition canvas.cc:835
void SetShowBuiltinContextMenu(bool show)
Definition canvas.h:301
static float GetButtonPadding()
static float GetStandardSpacing()
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
static const char * GetLayerName(LayerType layer)
Get human-readable name for layer type.
void MarkLayoutDirty()
Definition room.h:365
uint8_t blockset() const
Definition room.h:602
void SetLayoutId(uint8_t id)
Definition room.h:616
TagKey tag2() const
Definition room.h:589
uint8_t palette() const
Definition room.h:604
auto rom() const
Definition room.h:652
void RenderRoomGraphics()
Definition room.cc:739
TagKey tag1() const
Definition room.h:588
uint8_t spriteset() const
Definition room.h:603
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:228
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:330
EffectKey effect() const
Definition room.h:587
void SetSpriteset(uint8_t ss)
Definition room.h:517
uint8_t floor1() const
Definition room.h:624
void SetBlockset(uint8_t bs)
Definition room.h:511
void SetPalette(uint8_t pal)
Definition room.h:505
uint8_t layout_id() const
Definition room.h:611
const std::vector< PotItem > & GetPotItems() const
Definition room.h:324
int id() const
Definition room.h:599
#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_SETTINGS
Definition icons.h:1699
#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_FILTER_CENTER_FOCUS
Definition icons.h:764
#define ICON_MD_TRAIN
Definition icons.h:2005
#define ICON_MD_LOOKS_ONE
Definition icons.h:1154
#define ICON_MD_FILE_DOWNLOAD
Definition icons.h:744
#define ICON_MD_EXPAND_LESS
Definition icons.h:702
#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_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#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_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_HOME
Definition icons.h:953
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_LOOKS_3
Definition icons.h:1150
#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_PIN
Definition icons.h:1470
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_SAVE
Definition icons.h:1644
#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_DELETE_FOREVER
Definition icons.h:531
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
#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 ICON_MD_CROP_FREE
Definition icons.h:495
#define ICON_MD_EXPAND_MORE
Definition icons.h:703
#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
const AgentUITheme & GetTheme()
std::pair< uint16_t, uint16_t > TileToCameraCoords(int room_id, int tile_x, int tile_y)
Calculate camera coordinates from room and tile position.
CameraToLocalResult CameraToLocalCoords(uint16_t camera_x, uint16_t camera_y)
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.
bool ThemedIconButton(const char *icon, const char *tooltip, const ImVec2 &size, bool is_active, bool is_disabled, const char *panel_id, const char *anim_id)
Draw a standard icon button with theme-aware colors.
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
bool BeginRoomObjectDragSource(uint16_t object_id, int room_id, int pos_x, int pos_y)
Definition drag_drop.h:88
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1591
bool BeginSpriteDragSource(int sprite_id, int room_id)
Definition drag_drop.h:65
void ValueChangeFlash(bool changed, const char *id)
Provide visual "flash" feedback when a value changes.
void DrawRect(const CanvasRuntime &rt, int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:2264
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1568
ImVec2 ClampScroll(ImVec2 scroll, ImVec2 content_px, ImVec2 canvas_px)
Definition canvas.cc:1700
void DrawCanvasHUD(const char *label, const ImVec2 &pos, const ImVec2 &size, std::function< void()> draw_content)
Draw a stylized Heads-Up Display (HUD) for canvas status.
bool AcceptRoomObjectDrop(RoomObjectDragPayload *out)
Definition drag_drop.h:145
void DrawText(const CanvasRuntime &rt, const std::string &text, int x, int y)
Definition canvas.cc:2271
bool AcceptSpriteDrop(SpriteDragPayload *out)
Definition drag_drop.h:119
Animator & GetAnimator()
Definition animator.cc:318
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:407
ImVec4 GetInfoColor()
Definition ui_helpers.cc:63
@ 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.
int GetObjectSubtype(int object_id)
absl::StatusOr< std::string > ExportRoomLayoutTemplate(const Room &room)
Export a room's layout as a JSON template string.
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_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
constexpr int kNumberOfRooms
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
static CanvasMenuItem Disabled(const std::string &lbl)
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:101
std::vector< uint16_t > track_stop_tiles
Definition project.h:96
std::vector< uint16_t > track_tiles
Definition project.h:95
std::vector< uint16_t > track_switch_tiles
Definition project.h:97
Modern project structure with comprehensive settings consolidation.
Definition project.h:164
DungeonOverlaySettings dungeon_overlay
Definition project.h:194