yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_connected_matrix.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cmath>
6#include <cstdio>
7#include <limits>
8#include <map>
9#include <optional>
10#include <string>
11#include <utility>
12#include <vector>
13
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
20#include "app/gui/core/style.h"
25#include "imgui/imgui.h"
27
28namespace yaze::editor {
29
30namespace {
31
32using namespace dungeon_ui; // NOLINT(google-build-using-namespace)
33
35 "##DungeonConnectedCurrentRoomHUD";
37 "##DungeonConnectedCanvasControls";
39 "##DungeonConnectedCanvasOverview";
40
41ImVec2 GetConnectedRoomOrigin(int grid_col, int grid_row, int min_col,
42 int min_row) {
43 const int col = grid_col - min_col;
44 const int row = grid_row - min_row;
45 const float step = kDungeonRoomPixelSize + kConnectedRoomGap;
46 return ImVec2(static_cast<float>(col) * step, static_cast<float>(row) * step);
47}
48
49ImVec2 GetConnectedRoomCenter(int grid_col, int grid_row, int min_col,
50 int min_row) {
51 const ImVec2 origin =
52 GetConnectedRoomOrigin(grid_col, grid_row, min_col, min_row);
53 return ImVec2(origin.x + (kDungeonRoomPixelSize * 0.5f),
54 origin.y + (kDungeonRoomPixelSize * 0.5f));
55}
56
57ImVec2 GetConnectedRoomAnchor(int grid_col, int grid_row,
58 zelda3::DoorDirection dir, int min_col,
59 int min_row) {
60 const ImVec2 origin =
61 GetConnectedRoomOrigin(grid_col, grid_row, min_col, min_row);
62 const float half = kDungeonRoomPixelSize * 0.5f;
63 switch (dir) {
65 return ImVec2(origin.x + half, origin.y);
67 return ImVec2(origin.x + half, origin.y + kDungeonRoomPixelSize);
69 return ImVec2(origin.x, origin.y + half);
71 return ImVec2(origin.x + kDungeonRoomPixelSize, origin.y + half);
72 }
73 return GetConnectedRoomCenter(grid_col, grid_row, min_col, min_row);
74}
75
77 int to_col, int to_row) {
78 const int delta_col = to_col - from_col;
79 const int delta_row = to_row - from_row;
80 if (std::abs(delta_col) >= std::abs(delta_row)) {
81 return delta_col >= 0 ? zelda3::DoorDirection::East
83 }
84 return delta_row >= 0 ? zelda3::DoorDirection::South
86}
87
89 const ImVec2& canvas_point) {
90 return ImVec2(canvas_rt.canvas_p0.x + canvas_point.x * canvas_rt.scale,
91 canvas_rt.canvas_p0.y + canvas_point.y * canvas_rt.scale);
92}
93
94std::pair<ImVec2, ImVec2> GetConnectedLinkEndpoints(
95 const DungeonConnectedRoomLink& link, int from_col, int from_row,
96 int to_col, int to_row, int min_col, int min_row) {
98 return {GetConnectedRoomAnchor(from_col, from_row, link.direction, min_col,
99 min_row),
100 GetConnectedRoomAnchor(to_col, to_row, OppositeDir(link.direction),
101 min_col, min_row)};
102 }
103
104 const zelda3::DoorDirection from_dir =
105 GetConnectedTravelDirection(from_col, from_row, to_col, to_row);
106 const zelda3::DoorDirection to_dir =
107 GetConnectedTravelDirection(to_col, to_row, from_col, from_row);
108 return {
109 GetConnectedRoomAnchor(from_col, from_row, from_dir, min_col, min_row),
110 GetConnectedRoomAnchor(to_col, to_row, to_dir, min_col, min_row)};
111}
112
115 switch (type) {
117 return ImGui::GetColorU32(ImVec4(theme.dungeon_object_door.x,
118 theme.dungeon_object_door.y,
119 theme.dungeon_object_door.z, 0.72f));
121 return ImGui::GetColorU32(ImVec4(theme.dungeon_object_stairs.x,
122 theme.dungeon_object_stairs.y,
123 theme.dungeon_object_stairs.z, 0.78f));
125 return ImGui::GetColorU32(ImVec4(theme.transport_color.x,
126 theme.transport_color.y,
127 theme.transport_color.z, 0.82f));
128 }
129 return ImGui::GetColorU32(theme.selection_primary);
130}
131
132ImVec4 WithAlpha(ImVec4 color, float alpha) {
133 color.w = alpha;
134 return color;
135}
136
138 const AgentUITheme& theme,
140 const std::string& floor_label) {
141 auto it = std::find(graph.floor_order.begin(), graph.floor_order.end(),
142 floor_label);
143 const int floor_index =
144 it == graph.floor_order.end()
145 ? 0
146 : static_cast<int>(std::distance(graph.floor_order.begin(), it));
147 switch (floor_index % 6) {
148 case 0:
149 return theme.selection_primary;
150 case 1:
151 return theme.transport_color;
152 case 2:
153 return theme.status_success;
154 case 3:
155 return theme.dungeon_object_stairs;
156 case 4:
157 return theme.dungeon_object_door;
158 default:
159 return theme.selection_secondary;
160 }
161}
162
164 const AgentUITheme& theme,
165 const DungeonCanvasViewer::ConnectedRoomGraphData& graph, int room_id,
166 float alpha) {
167 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
168 return ImGui::GetColorU32(WithAlpha(theme.text_secondary_gray, alpha));
169 }
170 const std::string& floor_label =
171 graph.room_floor_labels[static_cast<size_t>(room_id)];
172 if (floor_label.empty()) {
173 return ImGui::GetColorU32(WithAlpha(theme.text_secondary_gray, alpha));
174 }
175 return ImGui::GetColorU32(
176 WithAlpha(GetConnectedFloorBaseColor(theme, graph, floor_label), alpha));
177}
178
181 const std::string& floor_label) {
182 int count = 0;
183 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
184 if (!graph.room_mask[static_cast<size_t>(room_id)]) {
185 continue;
186 }
187 if (graph.room_floor_labels[static_cast<size_t>(room_id)] == floor_label) {
188 ++count;
189 }
190 }
191 return count;
192}
193
194void DrawConnectedLegendItem(const char* label, ImU32 color,
196 constexpr float kSwatchWidth = 12.0f;
197 constexpr float kSwatchHeight = 10.0f;
198 const ImVec2 swatch_min = ImGui::GetCursorScreenPos();
199 const ImVec2 swatch_size(kSwatchWidth, kSwatchHeight);
200 ImGui::Dummy(swatch_size);
201
202 ImDrawList* draw_list = ImGui::GetWindowDrawList();
203 const float mid_y = swatch_min.y + (kSwatchHeight * 0.5f);
204 const ImVec2 line_start(swatch_min.x + 1.5f, mid_y);
205 const ImVec2 line_end(swatch_min.x + kSwatchWidth - 1.5f, mid_y);
206 draw_list->AddLine(line_start, line_end, color, 1.75f);
207 if (type != DungeonConnectedLinkType::Door) {
208 draw_list->AddCircleFilled(line_start, 1.8f, color, 10);
209 draw_list->AddCircleFilled(line_end, 1.8f, color, 10);
210 }
211
212 ImGui::SameLine(0.0f, 4.0f);
213 ImGui::TextDisabled("%s", label);
214}
215
216ImVec2 GetConnectedCanvasSize(int min_col, int max_col, int min_row,
217 int max_row) {
218 const int cols = std::max(1, (max_col - min_col) + 1);
219 const int rows = std::max(1, (max_row - min_row) + 1);
220 return ImVec2(
221 (cols * kDungeonRoomPixelSize) + ((cols - 1) * kConnectedRoomGap),
222 (rows * kDungeonRoomPixelSize) + ((rows - 1) * kConnectedRoomGap));
223}
224
226 const gui::CanvasRuntime& canvas_rt,
227 const ImVec2& content_size) {
228 if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) {
229 return;
230 }
231
232 const float wheel = ImGui::GetIO().MouseWheel;
233 if (std::abs(wheel) < 0.001f) {
234 return;
235 }
236
237 const float old_scale = canvas.global_scale();
238 const float zoom_factor = wheel > 0.0f ? 1.12f : (1.0f / 1.12f);
239 const float new_scale =
240 std::clamp(old_scale * zoom_factor, kConnectedCanvasMinScale,
242 if (std::abs(new_scale - old_scale) < 0.0001f) {
243 return;
244 }
245
246 const ImVec2 mouse_pos = ImGui::GetMousePos();
247 const ImVec2 mouse_local(mouse_pos.x - canvas_rt.canvas_p0.x,
248 mouse_pos.y - canvas_rt.canvas_p0.y);
249 const float world_x = (ImGui::GetScrollX() + mouse_local.x) / old_scale;
250 const float world_y = (ImGui::GetScrollY() + mouse_local.y) / old_scale;
251 canvas.set_global_scale(new_scale);
252
253 const float max_scroll_x =
254 std::max(0.0f, (content_size.x * new_scale) - canvas_rt.canvas_sz.x);
255 const float max_scroll_y =
256 std::max(0.0f, (content_size.y * new_scale) - canvas_rt.canvas_sz.y);
257 ImGui::SetScrollX(
258 std::clamp((world_x * new_scale) - mouse_local.x, 0.0f, max_scroll_x));
259 ImGui::SetScrollY(
260 std::clamp((world_y * new_scale) - mouse_local.y, 0.0f, max_scroll_y));
261}
262
263void HandleConnectedCanvasPan(int hovered_room_id) {
264 if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) {
265 return;
266 }
267
268 const bool drag_middle = ImGui::IsMouseDragging(ImGuiMouseButton_Middle);
269 const bool drag_right = ImGui::IsMouseDragging(ImGuiMouseButton_Right);
270 const bool drag_left_background =
271 hovered_room_id < 0 && ImGui::IsMouseDragging(ImGuiMouseButton_Left);
272 if (!drag_middle && !drag_right && !drag_left_background) {
273 return;
274 }
275
276 const ImVec2 delta = ImGui::GetIO().MouseDelta;
277 const float new_scroll_x =
278 std::clamp(ImGui::GetScrollX() - delta.x, 0.0f, ImGui::GetScrollMaxX());
279 const float new_scroll_y =
280 std::clamp(ImGui::GetScrollY() - delta.y, 0.0f, ImGui::GetScrollMaxY());
281 ImGui::SetScrollX(new_scroll_x);
282 ImGui::SetScrollY(new_scroll_y);
283}
284
286 bool fit = false;
287 bool reset = false;
288 bool reset_position = false;
289 bool hovered = false;
290 bool show_overview = true;
291 bool show_preview = true;
292 std::optional<ImVec2> moved_window_pos;
293};
294
296 bool fit = false;
297 bool reset = false;
298};
299
301 const auto& agent_theme = AgentUI::GetTheme();
303 "Door",
306 ImGui::SameLine(0.0f, 8.0f);
308 "Stair",
311 ImGui::SameLine(0.0f, 8.0f);
313 "Warp",
316}
317
319 float scale, int connected_room_count, bool* show_overview,
320 bool* show_preview) {
322 ImGui::TextDisabled(tr("%d room%s"), connected_room_count,
323 connected_room_count == 1 ? "" : "s");
324 ImGui::SameLine(0.0f, 8.0f);
325 ImGui::TextDisabled("%.0f%%", scale * 100.0f);
326 if (ImGui::IsItemHovered()) {
327 ImGui::SetTooltip(tr("Connected zoom\nF fit\n0 reset\n+/- zoom"));
328 }
329
330 ImGui::SameLine(0.0f, 6.0f);
331 if (ImGui::SmallButton(ICON_MD_FIT_SCREEN "##ConnectedFit")) {
332 actions.fit = true;
333 }
334 if (ImGui::IsItemHovered()) {
335 ImGui::SetTooltip(tr("Fit all connected rooms in view (F)"));
336 }
337
338 ImGui::SameLine();
339 if (ImGui::SmallButton(ICON_MD_RESTART_ALT "##ConnectedReset")) {
340 actions.reset = true;
341 }
342 if (ImGui::IsItemHovered()) {
343 ImGui::SetTooltip(tr("Reset zoom and recenter on current room (0)"));
344 }
345
346 if (show_overview) {
347 ImGui::SameLine();
348 ImGui::Checkbox(tr("Mini##ConnectedMini"), show_overview);
349 if (ImGui::IsItemHovered()) {
350 ImGui::SetTooltip(tr(
351 "Show connected overview map\nClick a room on the overview to jump"));
352 }
353 }
354
355 if (show_preview) {
356 ImGui::SameLine();
357 ImGui::Checkbox(tr("Room##ConnectedRoomPreview"), show_preview);
358 if (ImGui::IsItemHovered()) {
359 ImGui::SetTooltip(tr("Show full-resolution preview of the current room"));
360 }
361 }
362
363 ImGui::SameLine(0.0f, 8.0f);
365 return actions;
366}
367
369 bool hovered = false;
370 std::optional<ImVec2> target_scroll;
371 std::optional<int> activated_room_id;
372 int hovered_room_id = -1;
373};
374
376 const ImVec2& pos, const ImVec2& pivot, float scale,
377 int connected_room_count, bool show_overview, bool show_preview) {
379 actions.show_overview = show_overview;
380 actions.show_preview = show_preview;
381 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
382
383 ImGui::SetNextWindowPos(pos, ImGuiCond_Always, pivot);
384 ImGui::SetNextWindowBgAlpha(0.84f);
385
386 const ImGuiWindowFlags flags =
387 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
388 ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings |
389 ImGuiWindowFlags_AlwaysAutoResize;
390
391 ImVec4 controls_bg = gui::ConvertColorToImVec4(theme.editor_background);
392 controls_bg.w = 0.76f;
393 const gui::StyleColorGuard controls_colors({
394 {ImGuiCol_WindowBg, controls_bg},
395 {ImGuiCol_Border, gui::ConvertColorToImVec4(theme.accent)},
396 });
397 const gui::StyleVarGuard controls_vars({
398 {ImGuiStyleVar_WindowRounding, 4.0f},
399 {ImGuiStyleVar_WindowBorderSize, 1.0f},
400 {ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 4.0f)},
401 {ImGuiStyleVar_FramePadding, ImVec2(5.0f, 2.0f)},
402 {ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 4.0f)},
403 });
404
405 if (ImGui::Begin(kConnectedCanvasControlsId, nullptr, flags)) {
406 actions.hovered =
407 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
408 if (ImGui::SmallButton("::")) {}
409 if (ImGui::IsItemHovered()) {
410 ImGui::SetTooltip(tr("Drag controls\nDouble-click to snap back"));
411 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
412 actions.reset_position = true;
413 }
414 }
415 if (ImGui::IsItemActive() &&
416 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
417 const ImVec2 window_pos = ImGui::GetWindowPos();
418 const ImVec2 new_pos(window_pos.x + ImGui::GetIO().MouseDelta.x,
419 window_pos.y + ImGui::GetIO().MouseDelta.y);
420 ImGui::SetWindowPos(new_pos);
421 actions.moved_window_pos = new_pos;
422 }
423 ImGui::SameLine(0.0f, 6.0f);
424 const auto compact_actions = DrawConnectedControlsCompact(
425 scale, connected_room_count, &actions.show_overview,
426 &actions.show_preview);
427 actions.fit = compact_actions.fit;
428 actions.reset = compact_actions.reset;
429 }
430 ImGui::End();
431
432 return actions;
433}
434
436 const DungeonCanvasViewer::ConnectedRoomGraphData& connected_graph,
437 const ImVec2& content_size, const ImVec2& viewport_size,
438 float content_scale, float scroll_x, float scroll_y, int center_room_id,
439 int highlighted_room_id, const ImVec2& window_pos) {
441 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
442
443 const float inner_padding = 10.0f;
444 const float header_height = 18.0f;
445 const float map_max_width =
446 kConnectedOverviewMaxWidth - (inner_padding * 2.0f);
447 const float map_max_height = kConnectedOverviewMaxHeight - header_height -
448 (inner_padding * 2.0f) - 8.0f;
449 const float overview_scale =
450 std::min(map_max_width / std::max(1.0f, content_size.x),
451 map_max_height / std::max(1.0f, content_size.y));
452 const ImVec2 map_size(std::max(72.0f, content_size.x * overview_scale),
453 std::max(56.0f, content_size.y * overview_scale));
454 const ImVec2 window_size(map_size.x + (inner_padding * 2.0f),
455 map_size.y + header_height + (inner_padding * 2.0f));
456 ImGui::SetNextWindowPos(window_pos);
457 ImGui::SetNextWindowSize(window_size);
458 ImGui::SetNextWindowBgAlpha(0.84f);
459
460 const ImGuiWindowFlags flags =
461 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
462 ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings;
463
464 ImVec4 overview_bg = gui::ConvertColorToImVec4(theme.editor_background);
465 overview_bg.w = 0.78f;
466 const gui::StyleColorGuard overview_colors({
467 {ImGuiCol_WindowBg, overview_bg},
468 {ImGuiCol_Border, gui::ConvertColorToImVec4(theme.selection_primary)},
469 });
470 const gui::StyleVarGuard overview_vars({
471 {ImGuiStyleVar_WindowRounding, 4.0f},
472 {ImGuiStyleVar_WindowBorderSize, 1.0f},
473 {ImGuiStyleVar_WindowPadding, ImVec2(inner_padding, inner_padding)},
474 });
475
476 if (ImGui::Begin(kConnectedCanvasOverviewId, nullptr, flags)) {
477 actions.hovered =
478 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
479 ImGui::TextDisabled(tr("Overview"));
480 ImGui::InvisibleButton("##ConnectedOverviewMap", map_size,
481 ImGuiButtonFlags_MouseButtonLeft);
482
483 const ImVec2 map_min = ImGui::GetItemRectMin();
484 const ImVec2 map_max = ImGui::GetItemRectMax();
485 auto* draw_list = ImGui::GetWindowDrawList();
486
487 ImVec4 map_fill = gui::ConvertColorToImVec4(theme.editor_background);
488 map_fill.w = 230.0f / 255.0f;
489 draw_list->AddRectFilled(map_min, map_max, ImGui::GetColorU32(map_fill),
490 4.0f);
491 draw_list->AddRect(map_min, map_max,
492 ImGui::GetColorU32(theme.selection_primary), 4.0f);
493
494 auto overview_screen_point = [&](const ImVec2& canvas_point) {
495 return ImVec2(map_min.x + (canvas_point.x * overview_scale),
496 map_min.y + (canvas_point.y * overview_scale));
497 };
498
499 const bool map_hovered =
500 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
501 const ImVec2 mouse = ImGui::GetMousePos();
502 if (map_hovered) {
503 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
504 if (!connected_graph.room_mask[static_cast<size_t>(room_id)]) {
505 continue;
506 }
507 const auto& placement =
508 connected_graph.room_positions[static_cast<size_t>(room_id)];
509 if (!placement.placed) {
510 continue;
511 }
512
513 const ImVec2 room_origin = GetConnectedRoomOrigin(
514 placement.col, placement.row, connected_graph.min_col,
515 connected_graph.min_row);
516 const ImVec2 room_min = overview_screen_point(room_origin);
517 const ImVec2 room_max(
518 room_min.x + (kDungeonRoomPixelSize * overview_scale),
519 room_min.y + (kDungeonRoomPixelSize * overview_scale));
520 if (mouse.x >= room_min.x && mouse.x <= room_max.x &&
521 mouse.y >= room_min.y && mouse.y <= room_max.y) {
522 actions.hovered_room_id = room_id;
523 break;
524 }
525 }
526 }
527
528 for (const auto& link : connected_graph.links) {
529 const auto& from_placement =
530 connected_graph
531 .room_positions[static_cast<size_t>(link.from_room_id)];
532 const auto& to_placement =
533 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
534 if (!from_placement.placed || !to_placement.placed) {
535 continue;
536 }
537
538 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
539 link, from_placement.col, from_placement.row, to_placement.col,
540 to_placement.row, connected_graph.min_col, connected_graph.min_row);
541 draw_list->AddLine(overview_screen_point(canvas_start),
542 overview_screen_point(canvas_end),
544 1.0f);
545 }
546
547 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
548 if (!connected_graph.room_mask[static_cast<size_t>(room_id)]) {
549 continue;
550 }
551 const auto& placement =
552 connected_graph.room_positions[static_cast<size_t>(room_id)];
553 if (!placement.placed) {
554 continue;
555 }
556
557 const ImVec2 room_origin = GetConnectedRoomOrigin(
558 placement.col, placement.row, connected_graph.min_col,
559 connected_graph.min_row);
560 const ImVec2 room_min = overview_screen_point(room_origin);
561 const ImVec2 room_max(
562 room_min.x + (kDungeonRoomPixelSize * overview_scale),
563 room_min.y + (kDungeonRoomPixelSize * overview_scale));
564 const bool is_center = room_id == center_room_id;
565 const bool is_highlighted =
566 room_id == actions.hovered_room_id || room_id == highlighted_room_id;
567 const bool is_unlinked = !placement.connected_to_start;
568 const ImU32 fill =
569 is_center ? ImGui::GetColorU32(theme.selection_primary)
570 : is_highlighted
571 ? ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.warning))
572 : is_unlinked
573 ? ImGui::GetColorU32(
574 WithAlpha(AgentUI::GetTheme().status_warning, 0.52f))
575 : GetConnectedFloorColor(AgentUI::GetTheme(), connected_graph,
576 room_id, 0.52f);
577 ImVec4 dim_outline = gui::ConvertColorToImVec4(theme.editor_background);
578 dim_outline.w = 220.0f / 255.0f;
579 const ImU32 outline = is_center || is_highlighted
580 ? ImGui::GetColorU32(gui::ConvertColorToImVec4(
581 theme.text_primary))
582 : ImGui::GetColorU32(dim_outline);
583 draw_list->AddRectFilled(room_min, room_max, fill, 1.5f);
584 draw_list->AddRect(room_min, room_max, outline, 1.5f);
585 }
586
587 auto draw_room_tag = [&](int room_id, ImU32 fill_color) {
588 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms ||
589 !connected_graph.room_mask[static_cast<size_t>(room_id)]) {
590 return;
591 }
592
593 const auto& placement =
594 connected_graph.room_positions[static_cast<size_t>(room_id)];
595 if (!placement.placed) {
596 return;
597 }
598
599 const ImVec2 room_origin = GetConnectedRoomOrigin(
600 placement.col, placement.row, connected_graph.min_col,
601 connected_graph.min_row);
602 const ImVec2 room_min = overview_screen_point(room_origin);
603
604 char label[8];
605 std::snprintf(label, sizeof(label), "%03X", room_id);
606 const float font_size = std::max(9.0f, ImGui::GetFontSize() - 1.0f);
607 const ImVec2 text_size = ImGui::GetFont()->CalcTextSizeA(
608 font_size, std::numeric_limits<float>::max(), 0.0f, label);
609 ImVec2 label_min(room_min.x + 2.0f, room_min.y + 2.0f);
610 const ImVec2 label_size(text_size.x + 8.0f, text_size.y + 6.0f);
611
612 if (label_min.x + label_size.x > map_max.x) {
613 label_min.x = map_max.x - label_size.x;
614 }
615 if (label_min.y + label_size.y > map_max.y) {
616 label_min.y = std::max(map_min.y, room_min.y - label_size.y - 2.0f);
617 }
618 label_min.x =
619 std::clamp(label_min.x, map_min.x, map_max.x - label_size.x);
620 label_min.y =
621 std::clamp(label_min.y, map_min.y, map_max.y - label_size.y);
622
623 const ImVec2 label_max(label_min.x + label_size.x,
624 label_min.y + label_size.y);
625 draw_list->AddRectFilled(label_min, label_max, fill_color, 2.0f);
626 draw_list->AddRect(label_min, label_max, IM_COL32(255, 255, 255, 235),
627 2.0f);
628 draw_list->AddText(ImGui::GetFont(), font_size,
629 ImVec2(label_min.x + 4.0f, label_min.y + 3.0f),
630 IM_COL32(255, 255, 255, 255), label);
631 };
632
633 draw_room_tag(center_room_id, ImGui::GetColorU32(theme.selection_primary));
634 const int tagged_hover_room = actions.hovered_room_id >= 0
635 ? actions.hovered_room_id
636 : highlighted_room_id;
637 if (tagged_hover_room >= 0 && tagged_hover_room != center_room_id) {
638 draw_room_tag(
639 tagged_hover_room,
640 ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.warning)));
641 }
642
643 const ImVec2 viewport_min(
644 map_min.x + ((scroll_x / content_scale) * overview_scale),
645 map_min.y + ((scroll_y / content_scale) * overview_scale));
646 const ImVec2 viewport_rect_size(
647 (viewport_size.x / content_scale) * overview_scale,
648 (viewport_size.y / content_scale) * overview_scale);
649 const ImVec2 viewport_rect_max(viewport_min.x + viewport_rect_size.x,
650 viewport_min.y + viewport_rect_size.y);
651 draw_list->AddRectFilled(viewport_min, viewport_rect_max,
652 IM_COL32(255, 255, 255, 20), 2.0f);
653 draw_list->AddRect(viewport_min, viewport_rect_max,
654 IM_COL32(255, 255, 255, 220), 2.0f, 0, 1.5f);
655
656 if (map_hovered) {
657 actions.hovered = true;
658 const bool dragging = ImGui::IsMouseDragging(ImGuiMouseButton_Left);
659 if (actions.hovered_room_id >= 0 &&
660 ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !dragging) {
661 actions.activated_room_id = actions.hovered_room_id;
662 } else if ((actions.hovered_room_id < 0 &&
663 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) ||
664 dragging) {
665 const ImVec2 current_mouse = ImGui::GetMousePos();
666 const float content_x =
667 std::clamp((current_mouse.x - map_min.x) / overview_scale, 0.0f,
668 content_size.x);
669 const float content_y =
670 std::clamp((current_mouse.y - map_min.y) / overview_scale, 0.0f,
671 content_size.y);
672 const float max_scroll_x =
673 std::max(0.0f, (content_size.x * content_scale) - viewport_size.x);
674 const float max_scroll_y =
675 std::max(0.0f, (content_size.y * content_scale) - viewport_size.y);
676 actions.target_scroll = ImVec2(
677 std::clamp((content_x * content_scale) - (viewport_size.x * 0.5f),
678 0.0f, max_scroll_x),
679 std::clamp((content_y * content_scale) - (viewport_size.y * 0.5f),
680 0.0f, max_scroll_y));
681 }
682
683 if (actions.hovered_room_id >= 0) {
684 ImGui::SetTooltip(
685 tr("[%03X] %s\nRelease to open room view"), actions.hovered_room_id,
686 zelda3::GetRoomLabel(actions.hovered_room_id).c_str());
687 }
688 }
689 }
690 ImGui::End();
691
692 return actions;
693}
694
695} // namespace
696
698 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms ||
699 !rooms_) {
700 return ImVec2(0.0f, 0.0f);
701 }
702 if (connected_graph_cache_start_room_id_ != center_room_id) {
705 }
706 const auto& g = connected_graph_cache_;
707 return GetConnectedCanvasSize(g.min_col, g.max_col, g.min_row, g.max_row);
708}
709
711 return std::clamp(connected_canvas_.global_scale(), kConnectedCanvasMinScale,
712 kConnectedCanvasMaxScale);
713}
714
715void DungeonCanvasViewer::DrawNearbyRoomPreviews(int room_id, float height) {
716 if (!ImGui::GetCurrentContext() || !rom_ || !rom_->is_loaded() || !rooms_ ||
717 room_id < 0 || room_id >= zelda3::kNumberOfRooms ||
718 !std::isfinite(height) || height <= 0.0f) {
719 return;
720 }
721
722 const float gap = ImGui::GetStyle().ItemSpacing.x;
723 const float caption_height = ImGui::GetTextLineHeight();
724 const float caption_gap = ImGui::GetStyle().ItemSpacing.y;
725 const ImVec2 available = ImGui::GetContentRegionAvail();
726 height = std::min(height, available.y);
727 if (available.x <= 0.0f || height <= caption_height + caption_gap) {
728 return;
729 }
730
734 }
735 std::array<DungeonConnectedRoomLink, 4> neighbors;
736 size_t count = 0;
737 for (const auto& link : connected_graph_cache_.links) {
738 if (link.from_room_id != room_id || link.to_room_id == room_id ||
739 link.to_room_id < 0 || link.to_room_id >= zelda3::kNumberOfRooms) {
740 continue;
741 }
742 if (std::any_of(neighbors.begin(), neighbors.begin() + count,
743 [&](const auto& neighbor) {
744 return neighbor.to_room_id == link.to_room_id;
745 })) {
746 continue;
747 }
748 neighbors[count++] = link;
749 if (count == neighbors.size()) {
750 break;
751 }
752 }
753 if (count == 0) {
754 return;
755 }
756
757 const float cell_width = (available.x - gap * static_cast<float>(count - 1)) /
758 static_cast<float>(count);
759 if (cell_width <= 0.0f) {
760 return;
761 }
762
764 std::array<gfx::Bitmap*, 4> bitmaps{};
765 for (size_t i = 0; i < count; ++i) {
766 bitmaps[i] = PrepareConnectedRoomCompositeBitmap(neighbors[i].to_room_id);
767 }
768 if (renderer_) {
770 }
771
772 int activated_room = -1;
773 ImGui::PushID(room_id);
774 gui::StyleVarGuard padding(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
775 const bool open = ImGui::BeginChild(
776 "##NearbyRoomPreviews", ImVec2(available.x, height), false,
777 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
778 if (open) {
779 const ImVec2 origin = ImGui::GetCursorScreenPos();
780 const ImVec2 content = ImGui::GetContentRegionAvail();
781 const float actual_cell_width =
782 (content.x - gap * static_cast<float>(count - 1)) /
783 static_cast<float>(count);
784 const float thumbnail_size = std::max(
785 1.0f,
786 std::min(actual_cell_width, content.y - caption_height - caption_gap));
787 const auto& theme = AgentUI::GetTheme();
788 ImDrawList* draw_list = ImGui::GetWindowDrawList();
789 for (size_t i = 0; i < count; ++i) {
790 const int neighbor_id = neighbors[i].to_room_id;
791 const ImVec2 cell_origin(
792 origin.x + static_cast<float>(i) * (actual_cell_width + gap),
793 origin.y);
794 ImGui::SetCursorScreenPos(cell_origin);
795 ImGui::PushID(neighbor_id);
796 ImGui::BeginDisabled(!CanNavigateRooms());
797 if (ImGui::InvisibleButton(
798 "##NearbyRoom",
799 ImVec2(std::max(1.0f, actual_cell_width),
800 thumbnail_size + caption_gap + caption_height))) {
801 activated_room = neighbor_id;
802 }
803 ImGui::EndDisabled();
804 const bool hovered = ImGui::IsItemHovered();
805 const ImVec2 image_min(
806 cell_origin.x + (actual_cell_width - thumbnail_size) * 0.5f,
807 cell_origin.y);
808 const ImVec2 image_max(image_min.x + thumbnail_size,
809 image_min.y + thumbnail_size);
810 if (bitmaps[i] && bitmaps[i]->texture()) {
811 draw_list->AddImage((ImTextureID)(intptr_t)bitmaps[i]->texture(),
812 image_min, image_max);
813 } else {
814 draw_list->AddRectFilled(image_min, image_max,
815 ImGui::GetColorU32(theme.editor_background));
816 }
817 draw_list->AddRect(image_min, image_max,
818 ImGui::GetColorU32(hovered ? theme.selection_primary
820 const std::string label = absl::StrFormat(
821 "[%03X] %s", neighbor_id,
823 const ImVec2 caption_pos(cell_origin.x, image_max.y + caption_gap);
824 const ImVec4 caption_clip(cell_origin.x, caption_pos.y,
825 cell_origin.x + actual_cell_width,
826 caption_pos.y + caption_height);
827 draw_list->AddText(nullptr, 0.0f, caption_pos,
828 ImGui::GetColorU32(ImGuiCol_Text), label.c_str(),
829 nullptr, 0.0f, &caption_clip);
830 if (hovered) {
831 ImGui::SetTooltip(
832 "%s\n%s\n%s", label.c_str(),
833 FormatDungeonConnectedLinkDescription(neighbors[i]).c_str(),
834 tr("Click to open room view"));
835 }
836 ImGui::PopID();
837 }
838 }
839 ImGui::EndChild();
840 ImGui::PopID();
841 if (activated_room >= 0) {
842 NavigateToRoom(activated_room);
843 }
844}
845
847 if (center_room_id >= 0 && center_room_id < zelda3::kNumberOfRooms && rom_ &&
848 rom_->is_loaded() && rooms_) {
849 if (connected_graph_cache_start_room_id_ != center_room_id) {
852 }
853 }
854
855 const int connected_room_count =
858 : 0;
859 ImGui::TextDisabled(tr("%d room%s"), connected_room_count,
860 connected_room_count == 1 ? "" : "s");
861 if (connected_graph_cache_start_room_id_ == center_room_id &&
863 ImGui::SameLine(0.0f, 6.0f);
864 ImGui::TextColored(AgentUI::GetTheme().status_warning, tr("%d unlinked"),
866 if (ImGui::IsItemHovered()) {
867 ImGui::SetTooltip(
868 tr("Project rooms shown in this dungeon scope but not reachable from "
869 "the current room through resolved ROM doors, staircases, or "
870 "holewarps. They stay visible so broken connections are easier to "
871 "repair."));
872 }
873 }
874 if (connected_graph_cache_start_room_id_ == center_room_id &&
876 const auto& agent_theme = AgentUI::GetTheme();
877 for (const std::string& floor_label : connected_graph_cache_.floor_order) {
878 const int floor_count =
879 CountConnectedRoomsOnFloor(connected_graph_cache_, floor_label);
880 if (floor_count <= 0) {
881 continue;
882 }
883 ImGui::SameLine(0.0f, 6.0f);
884 ImGui::TextColored(GetConnectedFloorBaseColor(
885 agent_theme, connected_graph_cache_, floor_label),
886 "%s:%d", floor_label.c_str(), floor_count);
887 if (ImGui::IsItemHovered()) {
888 ImGui::SetTooltip(tr("%s rooms in this unified connected view"),
889 floor_label.c_str());
890 }
891 }
892 }
893 const size_t issue_count =
896 : 0;
897 if (issue_count > 0) {
898 ImGui::SameLine(0.0f, 6.0f);
899 ImGui::TextColored(gui::ConvertColorToImVec4(
900 gui::ThemeManager::Get().GetCurrentTheme().warning),
901 tr("%zu possible issue%s"), issue_count,
902 issue_count == 1 ? "" : "s");
903 if (ImGui::IsItemHovered()) {
904 std::string tooltip(
905 "Provisional staircase diagnostics based on placement order.\n"
906 "These do not establish runtime slot use. Room 000 is valid.\n"
907 "Review header destinations manually; automatic clearing is "
908 "disabled.");
909 for (const auto& issue : connected_graph_cache_.staircase_issues) {
910 absl::StrAppend(
911 &tooltip,
912 absl::StrFormat("\n[%03X] %s", issue.from_room_id,
914 }
915 ImGui::SetTooltip("%s", tooltip.c_str());
916 }
917 }
918 ImGui::SameLine(0.0f, 6.0f);
919 ImGui::TextDisabled("%s", tr("Stair mapping unverified"));
920 if (ImGui::IsItemHovered()) {
921 ImGui::SetTooltip(
922 "%s",
923 tr("Staircase links and unused-slot notices are placement-order "
924 "estimates. "
925 "The game uses staircase-family counters and collision indices. "
926 "Automatic clearing is disabled; review Destinations in the room "
927 "inspector."));
928 }
929 const size_t out_of_scope_count =
932 : 0;
933 if (connected_graph_cache_.dungeon_scope_active && out_of_scope_count > 0) {
934 ImGui::SameLine(0.0f, 6.0f);
935 ImGui::TextColored(gui::ConvertColorToImVec4(
936 gui::ThemeManager::Get().GetCurrentTheme().info),
937 tr("%zu out-of-scope"), out_of_scope_count);
938 if (ImGui::IsItemHovered()) {
939 std::string tooltip(
940 "Resolved links that leave the current dungeon group. Hidden from\n"
941 "the visual matrix to keep the view scoped to one dungeon, but\n"
942 "still surfaced here so you can see cross-dungeon connections.");
943 for (const auto& link : connected_graph_cache_.out_of_scope_links) {
944 absl::StrAppend(
945 &tooltip,
946 absl::StrFormat("\n[%03X] %s", link.from_room_id,
948 }
949 ImGui::SetTooltip("%s", tooltip.c_str());
950 }
951 }
953 ImGui::SameLine(0.0f, 6.0f);
954 ImGui::TextColored(connected_action_status_is_error_
955 ? AgentUI::GetTheme().status_error
958 }
959 ImGui::SameLine(0.0f, 8.0f);
960 const auto compact_actions = DrawConnectedControlsCompact(
961 ConnectedCanvasScale(), connected_room_count, &show_connected_overview_,
963 if (compact_actions.fit) {
965 }
966 if (compact_actions.reset) {
968 }
969}
970
972 int center_room_id) {
974 // This is a read-only overview. A release here must not complete a gesture
975 // that began in the editable room canvas when the user returns to that view.
978 current_room_id_ = center_room_id;
979 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms) {
980 ImGui::Text(tr("Invalid room ID: %d"), center_room_id);
981 return std::nullopt;
982 }
983 if (!rom_ || !rom_->is_loaded()) {
984 ImGui::Text(tr("ROM not loaded"));
985 return std::nullopt;
986 }
987 if (!rooms_) {
988 ImGui::TextDisabled(tr("Room data unavailable"));
989 return std::nullopt;
990 }
992
994 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
995 // The connected-room matrix is a read-only graph view of neighbor rooms;
996 // editing happens in the main canvas only. Declaring kPreviewOnly lets
997 // future canvas surfaces (cursors, context-menu defaults) skip
998 // edit-affordances on this surface without each call site repeating it.
1001 }
1003 std::clamp(connected_canvas_.global_scale(), kConnectedCanvasMinScale,
1004 kConnectedCanvasMaxScale));
1005
1006 if (connected_graph_cache_start_room_id_ != center_room_id) {
1008 connected_graph_cache_start_room_id_ = center_room_id;
1009 }
1010 const ConnectedRoomGraphData& connected_graph = connected_graph_cache_;
1011 const auto& connected_rooms = connected_graph.room_mask;
1012 const int connected_room_count = connected_graph.room_count;
1013
1014 const ImVec2 content_size =
1015 GetConnectedCanvasSize(connected_graph.min_col, connected_graph.max_col,
1016 connected_graph.min_row, connected_graph.max_row);
1017
1019 gui::CanvasFrameOptions frame_opts;
1020 frame_opts.canvas_size = content_size;
1021 frame_opts.draw_grid = false;
1022 frame_opts.draw_context_menu = false;
1023 frame_opts.draw_overlay = true;
1024 frame_opts.render_popups = true;
1025
1026 auto canvas_rt = gui::BeginCanvas(connected_canvas_, frame_opts);
1028
1029 HandleConnectedCanvasZoom(connected_canvas_, canvas_rt, content_size);
1030 canvas_rt.scale = connected_canvas_.global_scale();
1031
1032 const ImVec2 window_pos = ImGui::GetWindowPos();
1033 const ImVec2 window_content_min = ImGui::GetWindowContentRegionMin();
1034 const ImVec2 window_content_max = ImGui::GetWindowContentRegionMax();
1035 const ImVec2 viewport_screen_min(window_pos.x + window_content_min.x,
1036 window_pos.y + window_content_min.y);
1037 const ImVec2 viewport_screen_max(window_pos.x + window_content_max.x,
1038 window_pos.y + window_content_max.y);
1039 const ImVec2 viewport_size(
1040 std::max(1.0f, window_content_max.x - window_content_min.x),
1041 std::max(1.0f, window_content_max.y - window_content_min.y));
1042 const bool reserve_connected_side_panel =
1044 const float desired_side_panel_width =
1045 std::max(kConnectedOverviewMaxWidth + 20.0f,
1046 kConnectedCurrentRoomPreviewWidth + 20.0f);
1047 float connected_side_panel_width = 0.0f;
1048 if (reserve_connected_side_panel) {
1049 const float raw_side_width = std::min(
1050 desired_side_panel_width, std::max(0.0f, viewport_size.x - 320.0f));
1051 const float min_threshold = connected_side_panel_visible_last_frame_
1052 ? kConnectedSidePanelApproxMinWidth -
1053 kConnectedSidePanelHideHysteresis
1054 : kConnectedSidePanelApproxMinWidth;
1055 if (raw_side_width >= min_threshold) {
1056 connected_side_panel_width = raw_side_width;
1057 }
1058 }
1059 connected_side_panel_visible_last_frame_ = connected_side_panel_width > 0.0f;
1060 const float reserved_side_width =
1061 connected_side_panel_width > 0.0f
1062 ? connected_side_panel_width + kConnectedSidePanelGap
1063 : 0.0f;
1064 const ImVec2 map_viewport_size(
1065 std::max(1.0f, viewport_size.x - reserved_side_width), viewport_size.y);
1066 const ImVec2 map_viewport_screen_max(
1067 viewport_screen_min.x + map_viewport_size.x, viewport_screen_max.y);
1068 const ImVec2 side_panel_screen_min(
1069 map_viewport_screen_max.x + kConnectedSidePanelGap,
1070 viewport_screen_min.y);
1071 const ImVec2 side_panel_screen_max(viewport_screen_max.x,
1072 viewport_screen_max.y);
1073 const bool using_connected_side_panel = connected_side_panel_width > 0.0f;
1074 const ImVec2 mouse_pos = ImGui::GetMousePos();
1075 const bool mouse_in_map_viewport = mouse_pos.x >= viewport_screen_min.x &&
1076 mouse_pos.x <= map_viewport_screen_max.x &&
1077 mouse_pos.y >= viewport_screen_min.y &&
1078 mouse_pos.y <= viewport_screen_max.y;
1079
1080 auto center_room_scroll = [&](float scale_value, int room_id) {
1081 const auto& placement =
1082 connected_graph.room_positions[static_cast<size_t>(room_id)];
1083 const ImVec2 room_origin = GetConnectedRoomOrigin(
1084 placement.col, placement.row, connected_graph.min_col,
1085 connected_graph.min_row);
1086 const float max_scroll_x =
1087 std::max(0.0f, (content_size.x * scale_value) - map_viewport_size.x);
1088 const float max_scroll_y =
1089 std::max(0.0f, (content_size.y * scale_value) - map_viewport_size.y);
1090 return ImVec2(
1091 std::clamp(
1092 ((room_origin.x + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
1093 (map_viewport_size.x * 0.5f),
1094 0.0f, max_scroll_x),
1095 std::clamp(
1096 ((room_origin.y + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
1097 (map_viewport_size.y * 0.5f),
1098 0.0f, max_scroll_y));
1099 };
1100
1101 auto center_content_scroll = [&](float scale_value) {
1102 return ImVec2(
1103 std::max(0.0f,
1104 ((content_size.x * scale_value) - map_viewport_size.x) * 0.5f),
1105 std::max(0.0f, ((content_size.y * scale_value) - map_viewport_size.y) *
1106 0.5f));
1107 };
1108
1109 auto set_scale_around_view_center = [&](float new_scale) {
1110 const float old_scale = connected_canvas_.global_scale();
1111 const float clamped_scale = std::clamp(new_scale, kConnectedCanvasMinScale,
1112 kConnectedCanvasMaxScale);
1113 if (std::abs(clamped_scale - old_scale) < 0.0001f) {
1114 return;
1115 }
1116
1117 const float world_x =
1118 (ImGui::GetScrollX() + (map_viewport_size.x * 0.5f)) / old_scale;
1119 const float world_y =
1120 (ImGui::GetScrollY() + (map_viewport_size.y * 0.5f)) / old_scale;
1121 connected_canvas_.set_global_scale(clamped_scale);
1122 const float max_scroll_x =
1123 std::max(0.0f, (content_size.x * clamped_scale) - map_viewport_size.x);
1124 const float max_scroll_y =
1125 std::max(0.0f, (content_size.y * clamped_scale) - map_viewport_size.y);
1126 ImGui::SetScrollX(
1127 std::clamp((world_x * clamped_scale) - (map_viewport_size.x * 0.5f),
1128 0.0f, max_scroll_x));
1129 ImGui::SetScrollY(
1130 std::clamp((world_y * clamped_scale) - (map_viewport_size.y * 0.5f),
1131 0.0f, max_scroll_y));
1132 canvas_rt.scale = clamped_scale;
1133 };
1134
1135 auto apply_fit = [&]() {
1136 const auto fit =
1137 gui::ComputeZoomToFit(content_size, map_viewport_size, 24.0f);
1138 const float fit_scale = std::clamp(fit.scale, kConnectedCanvasMinScale,
1139 kConnectedCanvasMaxScale);
1141 const ImVec2 fit_scroll = center_content_scroll(fit_scale);
1142 ImGui::SetScrollX(fit_scroll.x);
1143 ImGui::SetScrollY(fit_scroll.y);
1144 canvas_rt.scale = fit_scale;
1145 };
1146
1147 auto apply_reset = [&]() {
1148 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
1149 const ImVec2 reset_scroll =
1150 center_room_scroll(kConnectedCanvasDefaultScale, center_room_id);
1151 ImGui::SetScrollX(reset_scroll.x);
1152 ImGui::SetScrollY(reset_scroll.y);
1153 canvas_rt.scale = kConnectedCanvasDefaultScale;
1155 };
1156
1157 const ImGuiIO& io = ImGui::GetIO();
1158 const bool allow_connected_shortcuts = ImGui::IsWindowFocused() &&
1159 !io.WantTextInput && !io.KeyCtrl &&
1160 !io.KeyAlt && !io.KeySuper;
1161 if (allow_connected_shortcuts) {
1162 if (ImGui::IsKeyPressed(ImGuiKey_F, false)) {
1163 apply_fit();
1164 } else if (ImGui::IsKeyPressed(ImGuiKey_0, false) ||
1165 ImGui::IsKeyPressed(ImGuiKey_Keypad0, false)) {
1166 apply_reset();
1167 } else if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
1168 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
1169 set_scale_around_view_center(connected_canvas_.global_scale() * 1.12f);
1170 } else if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
1171 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
1172 set_scale_around_view_center(connected_canvas_.global_scale() / 1.12f);
1173 }
1174 }
1175
1177 apply_fit();
1179 }
1181 apply_reset();
1183 }
1184
1185 if (last_connected_matrix_room_id_ != center_room_id) {
1186 const ImVec2 target_scroll =
1187 center_room_scroll(canvas_rt.scale, center_room_id);
1188 ImGui::SetScrollX(target_scroll.x);
1189 ImGui::SetScrollY(target_scroll.y);
1190 last_connected_matrix_room_id_ = center_room_id;
1191 }
1192
1193 const float scaled_step =
1194 (kDungeonRoomPixelSize + kConnectedRoomGap) * canvas_rt.scale;
1195 const float scroll_x = ImGui::GetScrollX();
1196 const float scroll_y = ImGui::GetScrollY();
1197 const int connected_cols =
1198 std::max(1, (connected_graph.max_col - connected_graph.min_col) + 1);
1199 const int connected_rows =
1200 std::max(1, (connected_graph.max_row - connected_graph.min_row) + 1);
1201 const int first_col =
1202 std::max(0, static_cast<int>(std::floor(scroll_x / scaled_step)) - 1);
1203 const int first_row =
1204 std::max(0, static_cast<int>(std::floor(scroll_y / scaled_step)) - 1);
1205 const int last_col = std::min(
1206 connected_cols - 1, static_cast<int>(std::ceil(
1207 (scroll_x + map_viewport_size.x) / scaled_step)) +
1208 1);
1209 const int last_row = std::min(
1210 connected_rows - 1, static_cast<int>(std::ceil(
1211 (scroll_y + map_viewport_size.y) / scaled_step)) +
1212 1);
1213
1214 std::vector<int> visible_rooms;
1215 visible_rooms.reserve(static_cast<size_t>(connected_room_count));
1216 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
1217 if (!connected_rooms[static_cast<size_t>(room_id)]) {
1218 continue;
1219 }
1220
1221 const auto& placement =
1222 connected_graph.room_positions[static_cast<size_t>(room_id)];
1223 if (!placement.placed) {
1224 continue;
1225 }
1226
1227 const int local_col = placement.col - connected_graph.min_col;
1228 const int local_row = placement.row - connected_graph.min_row;
1229 if (local_col < first_col || local_col > last_col ||
1230 local_row < first_row || local_row > last_row) {
1231 continue;
1232 }
1233
1234 visible_rooms.push_back(room_id);
1236 }
1237
1238 if (renderer_) {
1240 }
1241
1242 const auto& theme = AgentUI::GetTheme();
1243 const ImU32 outline_color = ImGui::GetColorU32(
1245 gui::GetOutlineVec4().z, 0.55f));
1246 const ImU32 active_outline = ImGui::GetColorU32(theme.selection_primary);
1247 const ImU32 hover_outline = ImGui::GetColorU32(theme.status_warning);
1248 const ImU32 label_bg = ImGui::GetColorU32(ImVec4(0.05f, 0.08f, 0.06f, 0.88f));
1249 const ImU32 placeholder_fill = ImGui::GetColorU32(
1250 ImVec4(theme.editor_background.x, theme.editor_background.y,
1251 theme.editor_background.z, 0.95f));
1252 const ImU32 link_shadow = ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.32f));
1253 const float link_thickness =
1254 std::clamp(1.1f + (canvas_rt.scale * 3.4f), 1.35f, 3.0f);
1255 const float transport_node_radius =
1256 std::clamp(2.4f + (canvas_rt.scale * 7.0f), 3.0f, 6.0f);
1257
1258 if (using_connected_side_panel) {
1259 canvas_rt.draw_list->AddRectFilled(side_panel_screen_min,
1260 side_panel_screen_max,
1261 IM_COL32(10, 14, 12, 232), 6.0f);
1262 canvas_rt.draw_list->AddLine(
1263 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1264 side_panel_screen_min.y),
1265 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1266 side_panel_screen_max.y),
1267 ImGui::GetColorU32(ImVec4(gui::GetOutlineVec4().x,
1269 gui::GetOutlineVec4().z, 0.4f)),
1270 1.0f);
1271 }
1272
1273 for (const auto& link : connected_graph.links) {
1274 const auto& from_placement =
1275 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1276 const auto& to_placement =
1277 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1278 if (!from_placement.placed || !to_placement.placed) {
1279 continue;
1280 }
1281 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1282 link, from_placement.col, from_placement.row, to_placement.col,
1283 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1284 const ImVec2 screen_start =
1285 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1286 const ImVec2 screen_end =
1287 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1288 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1289 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_shadow,
1290 link_thickness + 2.0f);
1291 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_color,
1292 link_thickness);
1293 }
1294
1295 int hovered_room_id = -1;
1296 for (int room_id : visible_rooms) {
1297 const auto& placement =
1298 connected_graph.room_positions[static_cast<size_t>(room_id)];
1299 const ImVec2 room_origin = GetConnectedRoomOrigin(
1300 placement.col, placement.row, connected_graph.min_col,
1301 connected_graph.min_row);
1302 const ImVec2 screen_min(
1303 canvas_rt.canvas_p0.x + room_origin.x * canvas_rt.scale,
1304 canvas_rt.canvas_p0.y + room_origin.y * canvas_rt.scale);
1305 const ImVec2 screen_max(
1306 screen_min.x + (kDungeonRoomPixelSize * canvas_rt.scale),
1307 screen_min.y + (kDungeonRoomPixelSize * canvas_rt.scale));
1308
1309 if (gfx::Bitmap* composite = PrepareConnectedRoomCompositeBitmap(room_id);
1310 composite && composite->texture()) {
1311 canvas_rt.draw_list->AddImage(
1312 (ImTextureID)(intptr_t)composite->texture(), screen_min, screen_max,
1313 ImVec2(0, 0), ImVec2(1, 1),
1314 ImGui::GetColorU32(IM_COL32(255, 255, 255, 255)));
1315 } else {
1316 canvas_rt.draw_list->AddRectFilled(screen_min, screen_max,
1317 placeholder_fill, 6.0f);
1318 }
1319
1320 if (!placement.connected_to_start) {
1321 canvas_rt.draw_list->AddRectFilled(
1322 screen_min, screen_max,
1323 ImGui::GetColorU32(WithAlpha(theme.status_warning, 0.12f)), 6.0f);
1324 }
1325
1326 const std::string& floor_label =
1327 connected_graph.room_floor_labels[static_cast<size_t>(room_id)];
1328 if (!floor_label.empty()) {
1329 const float strip_width = std::clamp(6.0f * canvas_rt.scale, 3.0f, 8.0f);
1330 canvas_rt.draw_list->AddRectFilled(
1331 screen_min, ImVec2(screen_min.x + strip_width, screen_max.y),
1332 GetConnectedFloorColor(theme, connected_graph, room_id, 0.82f), 6.0f,
1333 ImDrawFlags_RoundCornersLeft);
1334 }
1335
1336 const bool hovered =
1337 mouse_in_map_viewport &&
1338 ImGui::IsMouseHoveringRect(screen_min, screen_max, false) &&
1339 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
1340 if (hovered) {
1341 hovered_room_id = room_id;
1342 }
1343
1344 const ImU32 border_color = room_id == center_room_id ? active_outline
1345 : hovered ? hover_outline
1346 : outline_color;
1347 const float border_thickness = room_id == center_room_id
1348 ? kConnectedRoomHighlightThickness
1349 : kConnectedRoomOutlineThickness;
1350 canvas_rt.draw_list->AddRect(screen_min, screen_max, border_color, 6.0f, 0,
1351 border_thickness);
1352
1353 char room_label[192];
1354 if (floor_label.empty()) {
1355 std::snprintf(
1356 room_label, sizeof(room_label), "[%03X] %s", room_id,
1358 } else {
1359 std::snprintf(
1360 room_label, sizeof(room_label), "[%03X] %s - %s", room_id,
1361 floor_label.c_str(),
1363 }
1364 const ImVec2 label_size = ImGui::CalcTextSize(room_label);
1365 const ImVec2 label_min(screen_min.x + kConnectedRoomLabelPadding,
1366 screen_min.y + kConnectedRoomLabelPadding);
1367 const ImVec2 label_max(label_min.x + label_size.x + 12.0f,
1368 label_min.y + kConnectedRoomLabelHeight);
1369 canvas_rt.draw_list->AddRectFilled(label_min, label_max, label_bg, 5.0f);
1370 canvas_rt.draw_list->AddText(ImVec2(label_min.x + 6.0f, label_min.y + 6.0f),
1371 ImGui::GetColorU32(theme.text_primary),
1372 room_label);
1373 }
1374
1375 for (const auto& link : connected_graph.links) {
1376 if (link.type == DungeonConnectedLinkType::Door) {
1377 continue;
1378 }
1379
1380 const auto& from_placement =
1381 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1382 const auto& to_placement =
1383 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1384 if (!from_placement.placed || !to_placement.placed) {
1385 continue;
1386 }
1387
1388 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1389 link, from_placement.col, from_placement.row, to_placement.col,
1390 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1391 const ImVec2 screen_start =
1392 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1393 const ImVec2 screen_end =
1394 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1395 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1396 canvas_rt.draw_list->AddCircleFilled(screen_start, transport_node_radius,
1397 link_color, 12);
1398 canvas_rt.draw_list->AddCircleFilled(screen_end, transport_node_radius,
1399 link_color, 12);
1400 canvas_rt.draw_list->AddCircle(screen_start, transport_node_radius + 1.5f,
1401 link_shadow, 12, 1.25f);
1402 canvas_rt.draw_list->AddCircle(screen_end, transport_node_radius + 1.5f,
1403 link_shadow, 12, 1.25f);
1404 }
1405
1406 ConnectedCanvasControlActions control_actions;
1408 const ImVec2 connected_controls_pos =
1409 using_connected_side_panel ? side_panel_screen_min
1412 : ImVec2(viewport_screen_max.x - 12.0f,
1413 viewport_screen_min.y + 12.0f);
1414 const ImVec2 connected_controls_pivot =
1415 using_connected_side_panel || connected_controls_custom_position_
1416 ? ImVec2(0.0f, 0.0f)
1417 : ImVec2(1.0f, 0.0f);
1418 control_actions = DrawConnectedCanvasControls(
1419 connected_controls_pos, connected_controls_pivot,
1420 connected_canvas_.global_scale(), connected_room_count,
1422 if (!using_connected_side_panel &&
1423 control_actions.moved_window_pos.has_value()) {
1426 *control_actions.moved_window_pos;
1427 }
1428 if (control_actions.reset_position) {
1430 connected_controls_custom_position_value_ = ImVec2(0.0f, 0.0f);
1431 }
1432 show_connected_overview_ = control_actions.show_overview;
1433 show_connected_current_room_preview_ = control_actions.show_preview;
1434 if (control_actions.fit) {
1435 apply_fit();
1436 }
1437 if (control_actions.reset) {
1438 apply_reset();
1439 }
1440 }
1441
1442 ConnectedCanvasOverviewActions overview_actions;
1444 overview_actions = DrawConnectedCanvasOverview(
1445 connected_graph, content_size, map_viewport_size,
1446 connected_canvas_.global_scale(), ImGui::GetScrollX(),
1447 ImGui::GetScrollY(), center_room_id, hovered_room_id,
1448 using_connected_side_panel
1449 ? ImVec2(side_panel_screen_min.x,
1450 side_panel_screen_min.y + kConnectedControlsApproxHeight +
1451 8.0f)
1452 : ImVec2(
1453 viewport_screen_max.x - kConnectedOverviewMaxWidth - 12.0f,
1454 viewport_screen_max.y - kConnectedOverviewMaxHeight - 12.0f));
1455 }
1456 if (overview_actions.target_scroll.has_value()) {
1457 ImGui::SetScrollX(overview_actions.target_scroll->x);
1458 ImGui::SetScrollY(overview_actions.target_scroll->y);
1459 }
1460 if (overview_actions.activated_room_id.has_value()) {
1461 if (*overview_actions.activated_room_id != center_room_id) {
1462 NavigateToRoom(*overview_actions.activated_room_id);
1463 }
1464 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1465 return *overview_actions.activated_room_id;
1466 }
1467
1468 if (!control_actions.hovered && !overview_actions.hovered &&
1469 hovered_room_id >= 0) {
1470 if (rooms_ && hovered_room_id < static_cast<int>(rooms_->size())) {
1471 std::string tooltip = absl::StrFormat(
1472 "[%03X] %s", hovered_room_id,
1474 .c_str());
1475 const auto& hovered_placement =
1476 connected_graph.room_positions[static_cast<size_t>(hovered_room_id)];
1477 const std::string& floor_label =
1478 connected_graph
1479 .room_floor_labels[static_cast<size_t>(hovered_room_id)];
1480 if (!floor_label.empty()) {
1481 absl::StrAppend(&tooltip, "\nFloor: ", floor_label);
1482 }
1483 if (!hovered_placement.connected_to_start) {
1484 absl::StrAppend(
1485 &tooltip,
1486 "\nUnlinked: no resolved ROM path from the current room.");
1487 }
1488 if (const auto it =
1489 connected_graph.room_diagnostics.find(hovered_room_id);
1490 it != connected_graph.room_diagnostics.end()) {
1491 const auto& diagnostics = it->second;
1492 if (!diagnostics.links.empty()) {
1493 absl::StrAppend(&tooltip, "\nConnections:");
1494 for (const auto& link : diagnostics.links) {
1495 absl::StrAppend(&tooltip, "\n ",
1497 }
1498 }
1499 if (!diagnostics.staircase_issues.empty()) {
1500 absl::StrAppend(&tooltip, "\nStaircase issues:");
1501 for (const auto& issue : diagnostics.staircase_issues) {
1502 absl::StrAppend(&tooltip, "\n ",
1504 }
1505 }
1506 }
1507 absl::StrAppend(&tooltip, "\nClick to open room view");
1508 ImGui::SetTooltip("%s", tooltip.c_str());
1509 }
1510 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1511 if (hovered_room_id != center_room_id) {
1512 NavigateToRoom(hovered_room_id);
1513 }
1514 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1515 return hovered_room_id;
1516 }
1517 }
1518
1520 if (gfx::Bitmap* current_room =
1522 current_room && current_room->texture()) {
1523 const float preview_scale =
1524 kConnectedCurrentRoomPreviewWidth /
1525 std::max(1.0f, static_cast<float>(current_room->width()));
1526 const ImVec2 preview_size(current_room->width() * preview_scale,
1527 current_room->height() * preview_scale);
1528 const ImVec2 preview_pos(
1529 using_connected_side_panel ? side_panel_screen_min.x
1530 : viewport_screen_max.x - preview_size.x -
1531 kConnectedCurrentRoomPreviewPadding,
1532 using_connected_side_panel
1533 ? side_panel_screen_max.y - kConnectedPreviewApproxHeight
1534 : viewport_screen_min.y + kConnectedCurrentRoomPreviewPadding);
1536 kConnectedCurrentRoomHudId, preview_pos, ImVec2(0, 0), [&]() {
1537 ImGui::Text(tr("Current Room"));
1538 ImGui::TextDisabled(
1539 "[%03X] %s", center_room_id,
1541 .c_str());
1542 ImGui::Image((ImTextureID)(intptr_t)current_room->texture(),
1543 preview_size);
1544 });
1545 }
1546 }
1547
1548 const bool side_panel_hovered = using_connected_side_panel &&
1549 mouse_pos.x >= side_panel_screen_min.x &&
1550 mouse_pos.x <= side_panel_screen_max.x &&
1551 mouse_pos.y >= side_panel_screen_min.y &&
1552 mouse_pos.y <= side_panel_screen_max.y;
1553 HandleConnectedCanvasPan((control_actions.hovered ||
1554 overview_actions.hovered || side_panel_hovered)
1555 ? center_room_id
1556 : hovered_room_id);
1557
1558 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1559 return std::nullopt;
1560}
1561
1562} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:199
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
std::optional< int > DrawConnectedRoomMatrix(int center_room_id)
ConnectedRoomGraphData BuildConnectedRoomGraph(int start_room_id)
const project::YazeProject * project_
void DrawNearbyRoomPreviews(int room_id, float height)
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
TextureHandle texture() const
Definition bitmap.h:410
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
auto global_scale() const
Definition canvas.h:413
void set_global_scale(float scale)
Definition canvas.cc:324
CanvasConfig & GetConfig()
Definition canvas.h:243
RAII guard for ImGui style colors.
Definition style_guard.h:29
RAII guard for ImGui style vars.
Definition style_guard.h:71
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_FIT_SCREEN
Definition icons.h:781
#define ICON_MD_RESTART_ALT
Definition icons.h:1602
const AgentUITheme & GetTheme()
ImVec2 ToConnectedCanvasScreenPoint(const gui::CanvasRuntime &canvas_rt, const ImVec2 &canvas_point)
ConnectedControlsCompactActions DrawConnectedControlsCompact(float scale, int connected_room_count, bool *show_overview, bool *show_preview)
ImU32 GetConnectedFloorColor(const AgentUITheme &theme, const DungeonCanvasViewer::ConnectedRoomGraphData &graph, int room_id, float alpha)
zelda3::DoorDirection GetConnectedTravelDirection(int from_col, int from_row, int to_col, int to_row)
ImVec4 GetConnectedFloorBaseColor(const AgentUITheme &theme, const DungeonCanvasViewer::ConnectedRoomGraphData &graph, const std::string &floor_label)
ConnectedCanvasOverviewActions DrawConnectedCanvasOverview(const DungeonCanvasViewer::ConnectedRoomGraphData &connected_graph, const ImVec2 &content_size, const ImVec2 &viewport_size, float content_scale, float scroll_x, float scroll_y, int center_room_id, int highlighted_room_id, const ImVec2 &window_pos)
ImVec2 GetConnectedRoomAnchor(int grid_col, int grid_row, zelda3::DoorDirection dir, int min_col, int min_row)
ImU32 GetConnectedLinkColor(const AgentUITheme &theme, DungeonConnectedLinkType type)
int CountConnectedRoomsOnFloor(const DungeonCanvasViewer::ConnectedRoomGraphData &graph, const std::string &floor_label)
void DrawConnectedLegendItem(const char *label, ImU32 color, DungeonConnectedLinkType type)
ImVec2 GetConnectedRoomOrigin(int grid_col, int grid_row, int min_col, int min_row)
void HandleConnectedCanvasZoom(gui::Canvas &canvas, const gui::CanvasRuntime &canvas_rt, const ImVec2 &content_size)
ConnectedCanvasControlActions DrawConnectedCanvasControls(const ImVec2 &pos, const ImVec2 &pivot, float scale, int connected_room_count, bool show_overview, bool show_preview)
std::pair< ImVec2, ImVec2 > GetConnectedLinkEndpoints(const DungeonConnectedRoomLink &link, int from_col, int from_row, int to_col, int to_row, int min_col, int min_row)
ImVec2 GetConnectedCanvasSize(int min_col, int max_col, int min_row, int max_row)
ImVec2 GetConnectedRoomCenter(int grid_col, int grid_row, int min_col, int min_row)
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
constexpr float kConnectedCanvasMinScale
constexpr float kConnectedRoomGap
constexpr float kConnectedCanvasMaxScale
constexpr float kConnectedOverviewMaxWidth
constexpr float kConnectedOverviewMaxHeight
Editors are the view controllers for the application.
std::string FormatDungeonConnectedLinkDescription(const DungeonConnectedRoomLink &link)
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
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.
void EndNoPadding()
Definition style.cc:311
void BeginNoPadding()
Definition style.cc:307
ZoomToFitResult ComputeZoomToFit(ImVec2 content_px, ImVec2 canvas_px, float padding_px)
ImVec4 GetOutlineVec4()
std::string GetRoomLabel(int id)
Convenience function to get a room label.
constexpr int kNumberOfRooms
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19
std::array< RoomPlacement, zelda3::kNumberOfRooms > room_positions
std::array< std::string, zelda3::kNumberOfRooms > room_floor_labels
std::map< int, DungeonConnectedRoomLinkDiagnostics > room_diagnostics