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