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
716 if (center_room_id >= 0 && center_room_id < zelda3::kNumberOfRooms && rom_ &&
717 rom_->is_loaded() && rooms_) {
718 if (connected_graph_cache_start_room_id_ != center_room_id) {
721 }
722 }
723
724 const int connected_room_count =
727 : 0;
728 ImGui::TextDisabled(tr("%d room%s"), connected_room_count,
729 connected_room_count == 1 ? "" : "s");
730 if (connected_graph_cache_start_room_id_ == center_room_id &&
732 ImGui::SameLine(0.0f, 6.0f);
733 ImGui::TextColored(AgentUI::GetTheme().status_warning, tr("%d unlinked"),
735 if (ImGui::IsItemHovered()) {
736 ImGui::SetTooltip(
737 tr("Project rooms shown in this dungeon scope but not reachable from "
738 "the current room through resolved ROM doors, staircases, or "
739 "holewarps. They stay visible so broken connections are easier to "
740 "repair."));
741 }
742 }
743 if (connected_graph_cache_start_room_id_ == center_room_id &&
745 const auto& agent_theme = AgentUI::GetTheme();
746 for (const std::string& floor_label : connected_graph_cache_.floor_order) {
747 const int floor_count =
748 CountConnectedRoomsOnFloor(connected_graph_cache_, floor_label);
749 if (floor_count <= 0) {
750 continue;
751 }
752 ImGui::SameLine(0.0f, 6.0f);
753 ImGui::TextColored(GetConnectedFloorBaseColor(
754 agent_theme, connected_graph_cache_, floor_label),
755 "%s:%d", floor_label.c_str(), floor_count);
756 if (ImGui::IsItemHovered()) {
757 ImGui::SetTooltip(tr("%s rooms in this unified connected view"),
758 floor_label.c_str());
759 }
760 }
761 }
762 const size_t issue_count =
765 : 0;
766 if (issue_count > 0) {
767 ImGui::SameLine(0.0f, 6.0f);
768 ImGui::TextColored(gui::ConvertColorToImVec4(
769 gui::ThemeManager::Get().GetCurrentTheme().warning),
770 tr("%zu possible issue%s"), issue_count,
771 issue_count == 1 ? "" : "s");
772 if (ImGui::IsItemHovered()) {
773 std::string tooltip(
774 "Provisional staircase diagnostics based on placement order.\n"
775 "These do not establish runtime slot use. Room 000 is valid.\n"
776 "Review header destinations manually; automatic clearing is "
777 "disabled.");
778 for (const auto& issue : connected_graph_cache_.staircase_issues) {
779 absl::StrAppend(
780 &tooltip,
781 absl::StrFormat("\n[%03X] %s", issue.from_room_id,
783 }
784 ImGui::SetTooltip("%s", tooltip.c_str());
785 }
786 }
787 ImGui::SameLine(0.0f, 6.0f);
788 ImGui::TextDisabled("%s", tr("Stair mapping unverified"));
789 if (ImGui::IsItemHovered()) {
790 ImGui::SetTooltip(
791 "%s",
792 tr("Staircase links and unused-slot notices are placement-order "
793 "estimates. "
794 "The game uses staircase-family counters and collision indices. "
795 "Automatic clearing is disabled; review Destinations in the room "
796 "inspector."));
797 }
798 const size_t out_of_scope_count =
801 : 0;
802 if (connected_graph_cache_.dungeon_scope_active && out_of_scope_count > 0) {
803 ImGui::SameLine(0.0f, 6.0f);
804 ImGui::TextColored(gui::ConvertColorToImVec4(
805 gui::ThemeManager::Get().GetCurrentTheme().info),
806 tr("%zu out-of-scope"), out_of_scope_count);
807 if (ImGui::IsItemHovered()) {
808 std::string tooltip(
809 "Resolved links that leave the current dungeon group. Hidden from\n"
810 "the visual matrix to keep the view scoped to one dungeon, but\n"
811 "still surfaced here so you can see cross-dungeon connections.");
812 for (const auto& link : connected_graph_cache_.out_of_scope_links) {
813 absl::StrAppend(
814 &tooltip,
815 absl::StrFormat("\n[%03X] %s", link.from_room_id,
817 }
818 ImGui::SetTooltip("%s", tooltip.c_str());
819 }
820 }
822 ImGui::SameLine(0.0f, 6.0f);
823 ImGui::TextColored(connected_action_status_is_error_
824 ? AgentUI::GetTheme().status_error
827 }
828 ImGui::SameLine(0.0f, 8.0f);
829 const auto compact_actions = DrawConnectedControlsCompact(
830 ConnectedCanvasScale(), connected_room_count, &show_connected_overview_,
832 if (compact_actions.fit) {
834 }
835 if (compact_actions.reset) {
837 }
838}
839
841 int center_room_id) {
842 // This is a read-only overview. A release here must not complete a gesture
843 // that began in the editable room canvas when the user returns to that view.
846 current_room_id_ = center_room_id;
847 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms) {
848 ImGui::Text(tr("Invalid room ID: %d"), center_room_id);
849 return std::nullopt;
850 }
851 if (!rom_ || !rom_->is_loaded()) {
852 ImGui::Text(tr("ROM not loaded"));
853 return std::nullopt;
854 }
855 if (!rooms_) {
856 ImGui::TextDisabled(tr("Room data unavailable"));
857 return std::nullopt;
858 }
860
862 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
863 // The connected-room matrix is a read-only graph view of neighbor rooms;
864 // editing happens in the main canvas only. Declaring kPreviewOnly lets
865 // future canvas surfaces (cursors, context-menu defaults) skip
866 // edit-affordances on this surface without each call site repeating it.
869 }
871 std::clamp(connected_canvas_.global_scale(), kConnectedCanvasMinScale,
872 kConnectedCanvasMaxScale));
873
874 if (connected_graph_cache_start_room_id_ != center_room_id) {
877 }
878 const ConnectedRoomGraphData& connected_graph = connected_graph_cache_;
879 const auto& connected_rooms = connected_graph.room_mask;
880 const int connected_room_count = connected_graph.room_count;
881
882 const ImVec2 content_size =
883 GetConnectedCanvasSize(connected_graph.min_col, connected_graph.max_col,
884 connected_graph.min_row, connected_graph.max_row);
885
887 gui::CanvasFrameOptions frame_opts;
888 frame_opts.canvas_size = content_size;
889 frame_opts.draw_grid = false;
890 frame_opts.draw_context_menu = false;
891 frame_opts.draw_overlay = true;
892 frame_opts.render_popups = true;
893
894 auto canvas_rt = gui::BeginCanvas(connected_canvas_, frame_opts);
896
897 HandleConnectedCanvasZoom(connected_canvas_, canvas_rt, content_size);
898 canvas_rt.scale = connected_canvas_.global_scale();
899
900 const ImVec2 window_pos = ImGui::GetWindowPos();
901 const ImVec2 window_content_min = ImGui::GetWindowContentRegionMin();
902 const ImVec2 window_content_max = ImGui::GetWindowContentRegionMax();
903 const ImVec2 viewport_screen_min(window_pos.x + window_content_min.x,
904 window_pos.y + window_content_min.y);
905 const ImVec2 viewport_screen_max(window_pos.x + window_content_max.x,
906 window_pos.y + window_content_max.y);
907 const ImVec2 viewport_size(
908 std::max(1.0f, window_content_max.x - window_content_min.x),
909 std::max(1.0f, window_content_max.y - window_content_min.y));
910 const bool reserve_connected_side_panel =
912 const float desired_side_panel_width =
913 std::max(kConnectedOverviewMaxWidth + 20.0f,
914 kConnectedCurrentRoomPreviewWidth + 20.0f);
915 float connected_side_panel_width = 0.0f;
916 if (reserve_connected_side_panel) {
917 const float raw_side_width = std::min(
918 desired_side_panel_width, std::max(0.0f, viewport_size.x - 320.0f));
919 const float min_threshold = connected_side_panel_visible_last_frame_
920 ? kConnectedSidePanelApproxMinWidth -
921 kConnectedSidePanelHideHysteresis
922 : kConnectedSidePanelApproxMinWidth;
923 if (raw_side_width >= min_threshold) {
924 connected_side_panel_width = raw_side_width;
925 }
926 }
927 connected_side_panel_visible_last_frame_ = connected_side_panel_width > 0.0f;
928 const float reserved_side_width =
929 connected_side_panel_width > 0.0f
930 ? connected_side_panel_width + kConnectedSidePanelGap
931 : 0.0f;
932 const ImVec2 map_viewport_size(
933 std::max(1.0f, viewport_size.x - reserved_side_width), viewport_size.y);
934 const ImVec2 map_viewport_screen_max(
935 viewport_screen_min.x + map_viewport_size.x, viewport_screen_max.y);
936 const ImVec2 side_panel_screen_min(
937 map_viewport_screen_max.x + kConnectedSidePanelGap,
938 viewport_screen_min.y);
939 const ImVec2 side_panel_screen_max(viewport_screen_max.x,
940 viewport_screen_max.y);
941 const bool using_connected_side_panel = connected_side_panel_width > 0.0f;
942 const ImVec2 mouse_pos = ImGui::GetMousePos();
943 const bool mouse_in_map_viewport = mouse_pos.x >= viewport_screen_min.x &&
944 mouse_pos.x <= map_viewport_screen_max.x &&
945 mouse_pos.y >= viewport_screen_min.y &&
946 mouse_pos.y <= viewport_screen_max.y;
947
948 auto center_room_scroll = [&](float scale_value, int room_id) {
949 const auto& placement =
950 connected_graph.room_positions[static_cast<size_t>(room_id)];
951 const ImVec2 room_origin = GetConnectedRoomOrigin(
952 placement.col, placement.row, connected_graph.min_col,
953 connected_graph.min_row);
954 const float max_scroll_x =
955 std::max(0.0f, (content_size.x * scale_value) - map_viewport_size.x);
956 const float max_scroll_y =
957 std::max(0.0f, (content_size.y * scale_value) - map_viewport_size.y);
958 return ImVec2(
959 std::clamp(
960 ((room_origin.x + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
961 (map_viewport_size.x * 0.5f),
962 0.0f, max_scroll_x),
963 std::clamp(
964 ((room_origin.y + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
965 (map_viewport_size.y * 0.5f),
966 0.0f, max_scroll_y));
967 };
968
969 auto center_content_scroll = [&](float scale_value) {
970 return ImVec2(
971 std::max(0.0f,
972 ((content_size.x * scale_value) - map_viewport_size.x) * 0.5f),
973 std::max(0.0f, ((content_size.y * scale_value) - map_viewport_size.y) *
974 0.5f));
975 };
976
977 auto set_scale_around_view_center = [&](float new_scale) {
978 const float old_scale = connected_canvas_.global_scale();
979 const float clamped_scale = std::clamp(new_scale, kConnectedCanvasMinScale,
980 kConnectedCanvasMaxScale);
981 if (std::abs(clamped_scale - old_scale) < 0.0001f) {
982 return;
983 }
984
985 const float world_x =
986 (ImGui::GetScrollX() + (map_viewport_size.x * 0.5f)) / old_scale;
987 const float world_y =
988 (ImGui::GetScrollY() + (map_viewport_size.y * 0.5f)) / old_scale;
989 connected_canvas_.set_global_scale(clamped_scale);
990 const float max_scroll_x =
991 std::max(0.0f, (content_size.x * clamped_scale) - map_viewport_size.x);
992 const float max_scroll_y =
993 std::max(0.0f, (content_size.y * clamped_scale) - map_viewport_size.y);
994 ImGui::SetScrollX(
995 std::clamp((world_x * clamped_scale) - (map_viewport_size.x * 0.5f),
996 0.0f, max_scroll_x));
997 ImGui::SetScrollY(
998 std::clamp((world_y * clamped_scale) - (map_viewport_size.y * 0.5f),
999 0.0f, max_scroll_y));
1000 canvas_rt.scale = clamped_scale;
1001 };
1002
1003 auto apply_fit = [&]() {
1004 const auto fit =
1005 gui::ComputeZoomToFit(content_size, map_viewport_size, 24.0f);
1006 const float fit_scale = std::clamp(fit.scale, kConnectedCanvasMinScale,
1007 kConnectedCanvasMaxScale);
1009 const ImVec2 fit_scroll = center_content_scroll(fit_scale);
1010 ImGui::SetScrollX(fit_scroll.x);
1011 ImGui::SetScrollY(fit_scroll.y);
1012 canvas_rt.scale = fit_scale;
1013 };
1014
1015 auto apply_reset = [&]() {
1016 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
1017 const ImVec2 reset_scroll =
1018 center_room_scroll(kConnectedCanvasDefaultScale, center_room_id);
1019 ImGui::SetScrollX(reset_scroll.x);
1020 ImGui::SetScrollY(reset_scroll.y);
1021 canvas_rt.scale = kConnectedCanvasDefaultScale;
1023 };
1024
1025 const ImGuiIO& io = ImGui::GetIO();
1026 const bool allow_connected_shortcuts = ImGui::IsWindowFocused() &&
1027 !io.WantTextInput && !io.KeyCtrl &&
1028 !io.KeyAlt && !io.KeySuper;
1029 if (allow_connected_shortcuts) {
1030 if (ImGui::IsKeyPressed(ImGuiKey_F, false)) {
1031 apply_fit();
1032 } else if (ImGui::IsKeyPressed(ImGuiKey_0, false) ||
1033 ImGui::IsKeyPressed(ImGuiKey_Keypad0, false)) {
1034 apply_reset();
1035 } else if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
1036 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
1037 set_scale_around_view_center(connected_canvas_.global_scale() * 1.12f);
1038 } else if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
1039 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
1040 set_scale_around_view_center(connected_canvas_.global_scale() / 1.12f);
1041 }
1042 }
1043
1045 apply_fit();
1047 }
1049 apply_reset();
1051 }
1052
1053 if (last_connected_matrix_room_id_ != center_room_id) {
1054 const ImVec2 target_scroll =
1055 center_room_scroll(canvas_rt.scale, center_room_id);
1056 ImGui::SetScrollX(target_scroll.x);
1057 ImGui::SetScrollY(target_scroll.y);
1058 last_connected_matrix_room_id_ = center_room_id;
1059 }
1060
1061 const float scaled_step =
1062 (kDungeonRoomPixelSize + kConnectedRoomGap) * canvas_rt.scale;
1063 const float scroll_x = ImGui::GetScrollX();
1064 const float scroll_y = ImGui::GetScrollY();
1065 const int connected_cols =
1066 std::max(1, (connected_graph.max_col - connected_graph.min_col) + 1);
1067 const int connected_rows =
1068 std::max(1, (connected_graph.max_row - connected_graph.min_row) + 1);
1069 const int first_col =
1070 std::max(0, static_cast<int>(std::floor(scroll_x / scaled_step)) - 1);
1071 const int first_row =
1072 std::max(0, static_cast<int>(std::floor(scroll_y / scaled_step)) - 1);
1073 const int last_col = std::min(
1074 connected_cols - 1, static_cast<int>(std::ceil(
1075 (scroll_x + map_viewport_size.x) / scaled_step)) +
1076 1);
1077 const int last_row = std::min(
1078 connected_rows - 1, static_cast<int>(std::ceil(
1079 (scroll_y + map_viewport_size.y) / scaled_step)) +
1080 1);
1081
1082 std::vector<int> visible_rooms;
1083 visible_rooms.reserve(static_cast<size_t>(connected_room_count));
1084 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
1085 if (!connected_rooms[static_cast<size_t>(room_id)]) {
1086 continue;
1087 }
1088
1089 const auto& placement =
1090 connected_graph.room_positions[static_cast<size_t>(room_id)];
1091 if (!placement.placed) {
1092 continue;
1093 }
1094
1095 const int local_col = placement.col - connected_graph.min_col;
1096 const int local_row = placement.row - connected_graph.min_row;
1097 if (local_col < first_col || local_col > last_col ||
1098 local_row < first_row || local_row > last_row) {
1099 continue;
1100 }
1101
1102 visible_rooms.push_back(room_id);
1104 }
1105
1106 if (renderer_) {
1108 }
1109
1110 const auto& theme = AgentUI::GetTheme();
1111 const ImU32 outline_color = ImGui::GetColorU32(
1113 gui::GetOutlineVec4().z, 0.55f));
1114 const ImU32 active_outline = ImGui::GetColorU32(theme.selection_primary);
1115 const ImU32 hover_outline = ImGui::GetColorU32(theme.status_warning);
1116 const ImU32 label_bg = ImGui::GetColorU32(ImVec4(0.05f, 0.08f, 0.06f, 0.88f));
1117 const ImU32 placeholder_fill = ImGui::GetColorU32(
1118 ImVec4(theme.editor_background.x, theme.editor_background.y,
1119 theme.editor_background.z, 0.95f));
1120 const ImU32 link_shadow = ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.32f));
1121 const float link_thickness =
1122 std::clamp(1.1f + (canvas_rt.scale * 3.4f), 1.35f, 3.0f);
1123 const float transport_node_radius =
1124 std::clamp(2.4f + (canvas_rt.scale * 7.0f), 3.0f, 6.0f);
1125
1126 if (using_connected_side_panel) {
1127 canvas_rt.draw_list->AddRectFilled(side_panel_screen_min,
1128 side_panel_screen_max,
1129 IM_COL32(10, 14, 12, 232), 6.0f);
1130 canvas_rt.draw_list->AddLine(
1131 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1132 side_panel_screen_min.y),
1133 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1134 side_panel_screen_max.y),
1135 ImGui::GetColorU32(ImVec4(gui::GetOutlineVec4().x,
1137 gui::GetOutlineVec4().z, 0.4f)),
1138 1.0f);
1139 }
1140
1141 for (const auto& link : connected_graph.links) {
1142 const auto& from_placement =
1143 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1144 const auto& to_placement =
1145 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1146 if (!from_placement.placed || !to_placement.placed) {
1147 continue;
1148 }
1149 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1150 link, from_placement.col, from_placement.row, to_placement.col,
1151 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1152 const ImVec2 screen_start =
1153 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1154 const ImVec2 screen_end =
1155 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1156 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1157 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_shadow,
1158 link_thickness + 2.0f);
1159 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_color,
1160 link_thickness);
1161 }
1162
1163 int hovered_room_id = -1;
1164 for (int room_id : visible_rooms) {
1165 const auto& placement =
1166 connected_graph.room_positions[static_cast<size_t>(room_id)];
1167 const ImVec2 room_origin = GetConnectedRoomOrigin(
1168 placement.col, placement.row, connected_graph.min_col,
1169 connected_graph.min_row);
1170 const ImVec2 screen_min(
1171 canvas_rt.canvas_p0.x + room_origin.x * canvas_rt.scale,
1172 canvas_rt.canvas_p0.y + room_origin.y * canvas_rt.scale);
1173 const ImVec2 screen_max(
1174 screen_min.x + (kDungeonRoomPixelSize * canvas_rt.scale),
1175 screen_min.y + (kDungeonRoomPixelSize * canvas_rt.scale));
1176
1177 if (gfx::Bitmap* composite = PrepareConnectedRoomCompositeBitmap(room_id);
1178 composite && composite->texture()) {
1179 canvas_rt.draw_list->AddImage(
1180 (ImTextureID)(intptr_t)composite->texture(), screen_min, screen_max,
1181 ImVec2(0, 0), ImVec2(1, 1),
1182 ImGui::GetColorU32(IM_COL32(255, 255, 255, 255)));
1183 } else {
1184 canvas_rt.draw_list->AddRectFilled(screen_min, screen_max,
1185 placeholder_fill, 6.0f);
1186 }
1187
1188 if (!placement.connected_to_start) {
1189 canvas_rt.draw_list->AddRectFilled(
1190 screen_min, screen_max,
1191 ImGui::GetColorU32(WithAlpha(theme.status_warning, 0.12f)), 6.0f);
1192 }
1193
1194 const std::string& floor_label =
1195 connected_graph.room_floor_labels[static_cast<size_t>(room_id)];
1196 if (!floor_label.empty()) {
1197 const float strip_width = std::clamp(6.0f * canvas_rt.scale, 3.0f, 8.0f);
1198 canvas_rt.draw_list->AddRectFilled(
1199 screen_min, ImVec2(screen_min.x + strip_width, screen_max.y),
1200 GetConnectedFloorColor(theme, connected_graph, room_id, 0.82f), 6.0f,
1201 ImDrawFlags_RoundCornersLeft);
1202 }
1203
1204 const bool hovered =
1205 mouse_in_map_viewport &&
1206 ImGui::IsMouseHoveringRect(screen_min, screen_max, false) &&
1207 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
1208 if (hovered) {
1209 hovered_room_id = room_id;
1210 }
1211
1212 const ImU32 border_color = room_id == center_room_id ? active_outline
1213 : hovered ? hover_outline
1214 : outline_color;
1215 const float border_thickness = room_id == center_room_id
1216 ? kConnectedRoomHighlightThickness
1217 : kConnectedRoomOutlineThickness;
1218 canvas_rt.draw_list->AddRect(screen_min, screen_max, border_color, 6.0f, 0,
1219 border_thickness);
1220
1221 char room_label[192];
1222 if (floor_label.empty()) {
1223 std::snprintf(
1224 room_label, sizeof(room_label), "[%03X] %s", room_id,
1226 } else {
1227 std::snprintf(
1228 room_label, sizeof(room_label), "[%03X] %s - %s", room_id,
1229 floor_label.c_str(),
1231 }
1232 const ImVec2 label_size = ImGui::CalcTextSize(room_label);
1233 const ImVec2 label_min(screen_min.x + kConnectedRoomLabelPadding,
1234 screen_min.y + kConnectedRoomLabelPadding);
1235 const ImVec2 label_max(label_min.x + label_size.x + 12.0f,
1236 label_min.y + kConnectedRoomLabelHeight);
1237 canvas_rt.draw_list->AddRectFilled(label_min, label_max, label_bg, 5.0f);
1238 canvas_rt.draw_list->AddText(ImVec2(label_min.x + 6.0f, label_min.y + 6.0f),
1239 ImGui::GetColorU32(theme.text_primary),
1240 room_label);
1241 }
1242
1243 for (const auto& link : connected_graph.links) {
1244 if (link.type == DungeonConnectedLinkType::Door) {
1245 continue;
1246 }
1247
1248 const auto& from_placement =
1249 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1250 const auto& to_placement =
1251 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1252 if (!from_placement.placed || !to_placement.placed) {
1253 continue;
1254 }
1255
1256 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1257 link, from_placement.col, from_placement.row, to_placement.col,
1258 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1259 const ImVec2 screen_start =
1260 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1261 const ImVec2 screen_end =
1262 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1263 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1264 canvas_rt.draw_list->AddCircleFilled(screen_start, transport_node_radius,
1265 link_color, 12);
1266 canvas_rt.draw_list->AddCircleFilled(screen_end, transport_node_radius,
1267 link_color, 12);
1268 canvas_rt.draw_list->AddCircle(screen_start, transport_node_radius + 1.5f,
1269 link_shadow, 12, 1.25f);
1270 canvas_rt.draw_list->AddCircle(screen_end, transport_node_radius + 1.5f,
1271 link_shadow, 12, 1.25f);
1272 }
1273
1274 ConnectedCanvasControlActions control_actions;
1276 const ImVec2 connected_controls_pos =
1277 using_connected_side_panel ? side_panel_screen_min
1280 : ImVec2(viewport_screen_max.x - 12.0f,
1281 viewport_screen_min.y + 12.0f);
1282 const ImVec2 connected_controls_pivot =
1283 using_connected_side_panel || connected_controls_custom_position_
1284 ? ImVec2(0.0f, 0.0f)
1285 : ImVec2(1.0f, 0.0f);
1286 control_actions = DrawConnectedCanvasControls(
1287 connected_controls_pos, connected_controls_pivot,
1288 connected_canvas_.global_scale(), connected_room_count,
1290 if (!using_connected_side_panel &&
1291 control_actions.moved_window_pos.has_value()) {
1294 *control_actions.moved_window_pos;
1295 }
1296 if (control_actions.reset_position) {
1298 connected_controls_custom_position_value_ = ImVec2(0.0f, 0.0f);
1299 }
1300 show_connected_overview_ = control_actions.show_overview;
1301 show_connected_current_room_preview_ = control_actions.show_preview;
1302 if (control_actions.fit) {
1303 apply_fit();
1304 }
1305 if (control_actions.reset) {
1306 apply_reset();
1307 }
1308 }
1309
1310 ConnectedCanvasOverviewActions overview_actions;
1312 overview_actions = DrawConnectedCanvasOverview(
1313 connected_graph, content_size, map_viewport_size,
1314 connected_canvas_.global_scale(), ImGui::GetScrollX(),
1315 ImGui::GetScrollY(), center_room_id, hovered_room_id,
1316 using_connected_side_panel
1317 ? ImVec2(side_panel_screen_min.x,
1318 side_panel_screen_min.y + kConnectedControlsApproxHeight +
1319 8.0f)
1320 : ImVec2(
1321 viewport_screen_max.x - kConnectedOverviewMaxWidth - 12.0f,
1322 viewport_screen_max.y - kConnectedOverviewMaxHeight - 12.0f));
1323 }
1324 if (overview_actions.target_scroll.has_value()) {
1325 ImGui::SetScrollX(overview_actions.target_scroll->x);
1326 ImGui::SetScrollY(overview_actions.target_scroll->y);
1327 }
1328 if (overview_actions.activated_room_id.has_value()) {
1329 if (*overview_actions.activated_room_id != center_room_id) {
1330 NavigateToRoom(*overview_actions.activated_room_id);
1331 }
1332 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1333 return *overview_actions.activated_room_id;
1334 }
1335
1336 if (!control_actions.hovered && !overview_actions.hovered &&
1337 hovered_room_id >= 0) {
1338 if (rooms_ && hovered_room_id < static_cast<int>(rooms_->size())) {
1339 std::string tooltip = absl::StrFormat(
1340 "[%03X] %s", hovered_room_id,
1342 .c_str());
1343 const auto& hovered_placement =
1344 connected_graph.room_positions[static_cast<size_t>(hovered_room_id)];
1345 const std::string& floor_label =
1346 connected_graph
1347 .room_floor_labels[static_cast<size_t>(hovered_room_id)];
1348 if (!floor_label.empty()) {
1349 absl::StrAppend(&tooltip, "\nFloor: ", floor_label);
1350 }
1351 if (!hovered_placement.connected_to_start) {
1352 absl::StrAppend(
1353 &tooltip,
1354 "\nUnlinked: no resolved ROM path from the current room.");
1355 }
1356 if (zelda3::Room* hovered_room =
1357 EnsureRoomLoadedForConnectedView(hovered_room_id)) {
1358 const auto diagnostics = CollectDungeonConnectedRoomLinkDiagnostics(
1359 hovered_room_id, *hovered_room,
1360 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
1361 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
1362 });
1363 if (!diagnostics.links.empty()) {
1364 absl::StrAppend(&tooltip, "\nConnections:");
1365 for (const auto& link : diagnostics.links) {
1366 absl::StrAppend(&tooltip, "\n ",
1368 }
1369 }
1370 if (!diagnostics.staircase_issues.empty()) {
1371 absl::StrAppend(&tooltip, "\nStaircase issues:");
1372 for (const auto& issue : diagnostics.staircase_issues) {
1373 absl::StrAppend(&tooltip, "\n ",
1375 }
1376 }
1377 }
1378 absl::StrAppend(&tooltip, "\nClick to open room view");
1379 ImGui::SetTooltip("%s", tooltip.c_str());
1380 }
1381 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1382 if (hovered_room_id != center_room_id) {
1383 NavigateToRoom(hovered_room_id);
1384 }
1385 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1386 return hovered_room_id;
1387 }
1388 }
1389
1391 if (gfx::Bitmap* current_room =
1393 current_room && current_room->texture()) {
1394 const float preview_scale =
1395 kConnectedCurrentRoomPreviewWidth /
1396 std::max(1.0f, static_cast<float>(current_room->width()));
1397 const ImVec2 preview_size(current_room->width() * preview_scale,
1398 current_room->height() * preview_scale);
1399 const ImVec2 preview_pos(
1400 using_connected_side_panel ? side_panel_screen_min.x
1401 : viewport_screen_max.x - preview_size.x -
1402 kConnectedCurrentRoomPreviewPadding,
1403 using_connected_side_panel
1404 ? side_panel_screen_max.y - kConnectedPreviewApproxHeight
1405 : viewport_screen_min.y + kConnectedCurrentRoomPreviewPadding);
1407 kConnectedCurrentRoomHudId, preview_pos, ImVec2(0, 0), [&]() {
1408 ImGui::Text(tr("Current Room"));
1409 ImGui::TextDisabled(
1410 "[%03X] %s", center_room_id,
1412 .c_str());
1413 ImGui::Image((ImTextureID)(intptr_t)current_room->texture(),
1414 preview_size);
1415 });
1416 }
1417 }
1418
1419 const bool side_panel_hovered = using_connected_side_panel &&
1420 mouse_pos.x >= side_panel_screen_min.x &&
1421 mouse_pos.x <= side_panel_screen_max.x &&
1422 mouse_pos.y >= side_panel_screen_min.y &&
1423 mouse_pos.y <= side_panel_screen_max.y;
1424 HandleConnectedCanvasPan((control_actions.hovered ||
1425 overview_actions.hovered || side_panel_hovered)
1426 ? center_room_id
1427 : hovered_room_id);
1428
1429 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1430 return std::nullopt;
1431}
1432
1433} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:187
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
std::optional< int > DrawConnectedRoomMatrix(int center_room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ConnectedRoomGraphData BuildConnectedRoomGraph(int start_room_id)
const project::YazeProject * project_
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:323
CanvasConfig & GetConfig()
Definition canvas.h:243
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()
#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)
DungeonConnectedRoomLinkDiagnostics CollectDungeonConnectedRoomLinkDiagnostics(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
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:308
void BeginNoPadding()
Definition style.cc:304
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