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"
24#include "imgui/imgui.h"
26
27namespace yaze::editor {
28
29namespace {
30
31using namespace dungeon_ui; // NOLINT(google-build-using-namespace)
32
34 "##DungeonConnectedCurrentRoomHUD";
36 "##DungeonConnectedCanvasControls";
38 "##DungeonConnectedCanvasOverview";
39
40ImVec2 GetConnectedRoomOrigin(int grid_col, int grid_row, int min_col,
41 int min_row) {
42 const int col = grid_col - min_col;
43 const int row = grid_row - min_row;
44 const float step = kDungeonRoomPixelSize + kConnectedRoomGap;
45 return ImVec2(static_cast<float>(col) * step, static_cast<float>(row) * step);
46}
47
48ImVec2 GetConnectedRoomCenter(int grid_col, int grid_row, int min_col,
49 int min_row) {
50 const ImVec2 origin =
51 GetConnectedRoomOrigin(grid_col, grid_row, min_col, min_row);
52 return ImVec2(origin.x + (kDungeonRoomPixelSize * 0.5f),
53 origin.y + (kDungeonRoomPixelSize * 0.5f));
54}
55
56ImVec2 GetConnectedRoomAnchor(int grid_col, int grid_row,
57 zelda3::DoorDirection dir, int min_col,
58 int min_row) {
59 const ImVec2 origin =
60 GetConnectedRoomOrigin(grid_col, grid_row, min_col, min_row);
61 const float half = kDungeonRoomPixelSize * 0.5f;
62 switch (dir) {
64 return ImVec2(origin.x + half, origin.y);
66 return ImVec2(origin.x + half, origin.y + kDungeonRoomPixelSize);
68 return ImVec2(origin.x, origin.y + half);
70 return ImVec2(origin.x + kDungeonRoomPixelSize, origin.y + half);
71 }
72 return GetConnectedRoomCenter(grid_col, grid_row, min_col, min_row);
73}
74
76 int to_col, int to_row) {
77 const int delta_col = to_col - from_col;
78 const int delta_row = to_row - from_row;
79 if (std::abs(delta_col) >= std::abs(delta_row)) {
80 return delta_col >= 0 ? zelda3::DoorDirection::East
82 }
83 return delta_row >= 0 ? zelda3::DoorDirection::South
85}
86
88 const ImVec2& canvas_point) {
89 return ImVec2(canvas_rt.canvas_p0.x + canvas_point.x * canvas_rt.scale,
90 canvas_rt.canvas_p0.y + canvas_point.y * canvas_rt.scale);
91}
92
93std::pair<ImVec2, ImVec2> GetConnectedLinkEndpoints(
94 const DungeonConnectedRoomLink& link, int from_col, int from_row,
95 int to_col, int to_row, int min_col, int min_row) {
97 return {GetConnectedRoomAnchor(from_col, from_row, link.direction, min_col,
98 min_row),
99 GetConnectedRoomAnchor(to_col, to_row, OppositeDir(link.direction),
100 min_col, min_row)};
101 }
102
103 const zelda3::DoorDirection from_dir =
104 GetConnectedTravelDirection(from_col, from_row, to_col, to_row);
105 const zelda3::DoorDirection to_dir =
106 GetConnectedTravelDirection(to_col, to_row, from_col, from_row);
107 return {
108 GetConnectedRoomAnchor(from_col, from_row, from_dir, min_col, min_row),
109 GetConnectedRoomAnchor(to_col, to_row, to_dir, min_col, min_row)};
110}
111
114 switch (type) {
116 return ImGui::GetColorU32(ImVec4(theme.dungeon_object_door.x,
117 theme.dungeon_object_door.y,
118 theme.dungeon_object_door.z, 0.72f));
120 return ImGui::GetColorU32(ImVec4(theme.dungeon_object_stairs.x,
121 theme.dungeon_object_stairs.y,
122 theme.dungeon_object_stairs.z, 0.78f));
124 return ImGui::GetColorU32(ImVec4(theme.transport_color.x,
125 theme.transport_color.y,
126 theme.transport_color.z, 0.82f));
127 }
128 return ImGui::GetColorU32(theme.selection_primary);
129}
130
131ImVec4 WithAlpha(ImVec4 color, float alpha) {
132 color.w = alpha;
133 return color;
134}
135
137 const AgentUITheme& theme,
139 const std::string& floor_label) {
140 auto it = std::find(graph.floor_order.begin(), graph.floor_order.end(),
141 floor_label);
142 const int floor_index =
143 it == graph.floor_order.end()
144 ? 0
145 : static_cast<int>(std::distance(graph.floor_order.begin(), it));
146 switch (floor_index % 6) {
147 case 0:
148 return theme.selection_primary;
149 case 1:
150 return theme.transport_color;
151 case 2:
152 return theme.status_success;
153 case 3:
154 return theme.dungeon_object_stairs;
155 case 4:
156 return theme.dungeon_object_door;
157 default:
158 return theme.selection_secondary;
159 }
160}
161
163 const AgentUITheme& theme,
164 const DungeonCanvasViewer::ConnectedRoomGraphData& graph, int room_id,
165 float alpha) {
166 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
167 return ImGui::GetColorU32(WithAlpha(theme.text_secondary_gray, alpha));
168 }
169 const std::string& floor_label =
170 graph.room_floor_labels[static_cast<size_t>(room_id)];
171 if (floor_label.empty()) {
172 return ImGui::GetColorU32(WithAlpha(theme.text_secondary_gray, alpha));
173 }
174 return ImGui::GetColorU32(
175 WithAlpha(GetConnectedFloorBaseColor(theme, graph, floor_label), alpha));
176}
177
180 const std::string& floor_label) {
181 int count = 0;
182 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
183 if (!graph.room_mask[static_cast<size_t>(room_id)]) {
184 continue;
185 }
186 if (graph.room_floor_labels[static_cast<size_t>(room_id)] == floor_label) {
187 ++count;
188 }
189 }
190 return count;
191}
192
194 const std::vector<DungeonStaircaseIssue>& issues) {
195 return static_cast<int>(std::count_if(
196 issues.begin(), issues.end(), [](const DungeonStaircaseIssue& issue) {
197 return issue.kind == DungeonStaircaseIssueKind::UnusedHeader &&
198 issue.slot_index >= 0 && issue.slot_index < 4 &&
199 issue.header_room_id > 0;
200 }));
201}
202
203void DrawConnectedLegendItem(const char* label, ImU32 color,
205 constexpr float kSwatchWidth = 12.0f;
206 constexpr float kSwatchHeight = 10.0f;
207 const ImVec2 swatch_min = ImGui::GetCursorScreenPos();
208 const ImVec2 swatch_size(kSwatchWidth, kSwatchHeight);
209 ImGui::Dummy(swatch_size);
210
211 ImDrawList* draw_list = ImGui::GetWindowDrawList();
212 const float mid_y = swatch_min.y + (kSwatchHeight * 0.5f);
213 const ImVec2 line_start(swatch_min.x + 1.5f, mid_y);
214 const ImVec2 line_end(swatch_min.x + kSwatchWidth - 1.5f, mid_y);
215 draw_list->AddLine(line_start, line_end, color, 1.75f);
216 if (type != DungeonConnectedLinkType::Door) {
217 draw_list->AddCircleFilled(line_start, 1.8f, color, 10);
218 draw_list->AddCircleFilled(line_end, 1.8f, color, 10);
219 }
220
221 ImGui::SameLine(0.0f, 4.0f);
222 ImGui::TextDisabled("%s", label);
223}
224
225ImVec2 GetConnectedCanvasSize(int min_col, int max_col, int min_row,
226 int max_row) {
227 const int cols = std::max(1, (max_col - min_col) + 1);
228 const int rows = std::max(1, (max_row - min_row) + 1);
229 return ImVec2(
230 (cols * kDungeonRoomPixelSize) + ((cols - 1) * kConnectedRoomGap),
231 (rows * kDungeonRoomPixelSize) + ((rows - 1) * kConnectedRoomGap));
232}
233
235 const gui::CanvasRuntime& canvas_rt,
236 const ImVec2& content_size) {
237 if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) {
238 return;
239 }
240
241 const float wheel = ImGui::GetIO().MouseWheel;
242 if (std::abs(wheel) < 0.001f) {
243 return;
244 }
245
246 const float old_scale = canvas.global_scale();
247 const float zoom_factor = wheel > 0.0f ? 1.12f : (1.0f / 1.12f);
248 const float new_scale =
249 std::clamp(old_scale * zoom_factor, kConnectedCanvasMinScale,
251 if (std::abs(new_scale - old_scale) < 0.0001f) {
252 return;
253 }
254
255 const ImVec2 mouse_pos = ImGui::GetMousePos();
256 const ImVec2 mouse_local(mouse_pos.x - canvas_rt.canvas_p0.x,
257 mouse_pos.y - canvas_rt.canvas_p0.y);
258 const float world_x = (ImGui::GetScrollX() + mouse_local.x) / old_scale;
259 const float world_y = (ImGui::GetScrollY() + mouse_local.y) / old_scale;
260 canvas.set_global_scale(new_scale);
261
262 const float max_scroll_x =
263 std::max(0.0f, (content_size.x * new_scale) - canvas_rt.canvas_sz.x);
264 const float max_scroll_y =
265 std::max(0.0f, (content_size.y * new_scale) - canvas_rt.canvas_sz.y);
266 ImGui::SetScrollX(
267 std::clamp((world_x * new_scale) - mouse_local.x, 0.0f, max_scroll_x));
268 ImGui::SetScrollY(
269 std::clamp((world_y * new_scale) - mouse_local.y, 0.0f, max_scroll_y));
270}
271
272void HandleConnectedCanvasPan(int hovered_room_id) {
273 if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) {
274 return;
275 }
276
277 const bool drag_middle = ImGui::IsMouseDragging(ImGuiMouseButton_Middle);
278 const bool drag_right = ImGui::IsMouseDragging(ImGuiMouseButton_Right);
279 const bool drag_left_background =
280 hovered_room_id < 0 && ImGui::IsMouseDragging(ImGuiMouseButton_Left);
281 if (!drag_middle && !drag_right && !drag_left_background) {
282 return;
283 }
284
285 const ImVec2 delta = ImGui::GetIO().MouseDelta;
286 const float new_scroll_x =
287 std::clamp(ImGui::GetScrollX() - delta.x, 0.0f, ImGui::GetScrollMaxX());
288 const float new_scroll_y =
289 std::clamp(ImGui::GetScrollY() - delta.y, 0.0f, ImGui::GetScrollMaxY());
290 ImGui::SetScrollX(new_scroll_x);
291 ImGui::SetScrollY(new_scroll_y);
292}
293
295 bool fit = false;
296 bool reset = false;
297 bool reset_position = false;
298 bool hovered = false;
299 bool show_overview = true;
300 bool show_preview = true;
301 std::optional<ImVec2> moved_window_pos;
302};
303
305 bool fit = false;
306 bool reset = false;
307};
308
310 const auto& agent_theme = AgentUI::GetTheme();
312 "Door",
315 ImGui::SameLine(0.0f, 8.0f);
317 "Stair",
320 ImGui::SameLine(0.0f, 8.0f);
322 "Warp",
325}
326
328 float scale, int connected_room_count, bool* show_overview,
329 bool* show_preview) {
331 ImGui::TextDisabled(tr("%d room%s"), connected_room_count,
332 connected_room_count == 1 ? "" : "s");
333 ImGui::SameLine(0.0f, 8.0f);
334 ImGui::TextDisabled("%.0f%%", scale * 100.0f);
335 if (ImGui::IsItemHovered()) {
336 ImGui::SetTooltip(tr("Connected zoom\nF fit\n0 reset\n+/- zoom"));
337 }
338
339 ImGui::SameLine(0.0f, 6.0f);
340 if (ImGui::SmallButton(ICON_MD_FIT_SCREEN "##ConnectedFit")) {
341 actions.fit = true;
342 }
343 if (ImGui::IsItemHovered()) {
344 ImGui::SetTooltip(tr("Fit all connected rooms in view (F)"));
345 }
346
347 ImGui::SameLine();
348 if (ImGui::SmallButton(ICON_MD_RESTART_ALT "##ConnectedReset")) {
349 actions.reset = true;
350 }
351 if (ImGui::IsItemHovered()) {
352 ImGui::SetTooltip(tr("Reset zoom and recenter on current room (0)"));
353 }
354
355 if (show_overview) {
356 ImGui::SameLine();
357 ImGui::Checkbox(tr("Mini##ConnectedMini"), show_overview);
358 if (ImGui::IsItemHovered()) {
359 ImGui::SetTooltip(tr(
360 "Show connected overview map\nClick a room on the overview to jump"));
361 }
362 }
363
364 if (show_preview) {
365 ImGui::SameLine();
366 ImGui::Checkbox(tr("Room##ConnectedRoomPreview"), show_preview);
367 if (ImGui::IsItemHovered()) {
368 ImGui::SetTooltip(tr("Show full-resolution preview of the current room"));
369 }
370 }
371
372 ImGui::SameLine(0.0f, 8.0f);
374 return actions;
375}
376
378 bool hovered = false;
379 std::optional<ImVec2> target_scroll;
380 std::optional<int> activated_room_id;
381 int hovered_room_id = -1;
382};
383
385 const ImVec2& pos, const ImVec2& pivot, float scale,
386 int connected_room_count, bool show_overview, bool show_preview) {
388 actions.show_overview = show_overview;
389 actions.show_preview = show_preview;
390 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
391
392 ImGui::SetNextWindowPos(pos, ImGuiCond_Always, pivot);
393 ImGui::SetNextWindowBgAlpha(0.84f);
394
395 const ImGuiWindowFlags flags =
396 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
397 ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings |
398 ImGuiWindowFlags_AlwaysAutoResize;
399
400 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.76f));
401 ImGui::PushStyleColor(ImGuiCol_Border,
402 gui::ConvertColorToImVec4(theme.accent));
403 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
404 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
405 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 4.0f));
406 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5.0f, 2.0f));
407 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 4.0f));
408
409 if (ImGui::Begin(kConnectedCanvasControlsId, nullptr, flags)) {
410 actions.hovered =
411 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
412 if (ImGui::SmallButton("::")) {}
413 if (ImGui::IsItemHovered()) {
414 ImGui::SetTooltip(tr("Drag controls\nDouble-click to snap back"));
415 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
416 actions.reset_position = true;
417 }
418 }
419 if (ImGui::IsItemActive() &&
420 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
421 const ImVec2 window_pos = ImGui::GetWindowPos();
422 const ImVec2 new_pos(window_pos.x + ImGui::GetIO().MouseDelta.x,
423 window_pos.y + ImGui::GetIO().MouseDelta.y);
424 ImGui::SetWindowPos(new_pos);
425 actions.moved_window_pos = new_pos;
426 }
427 ImGui::SameLine(0.0f, 6.0f);
428 const auto compact_actions = DrawConnectedControlsCompact(
429 scale, connected_room_count, &actions.show_overview,
430 &actions.show_preview);
431 actions.fit = compact_actions.fit;
432 actions.reset = compact_actions.reset;
433 }
434 ImGui::End();
435
436 ImGui::PopStyleVar(5);
437 ImGui::PopStyleColor(2);
438 return actions;
439}
440
442 const DungeonCanvasViewer::ConnectedRoomGraphData& connected_graph,
443 const ImVec2& content_size, const ImVec2& viewport_size,
444 float content_scale, float scroll_x, float scroll_y, int center_room_id,
445 int highlighted_room_id, const ImVec2& window_pos) {
447 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
448
449 const float inner_padding = 10.0f;
450 const float header_height = 18.0f;
451 const float map_max_width =
452 kConnectedOverviewMaxWidth - (inner_padding * 2.0f);
453 const float map_max_height = kConnectedOverviewMaxHeight - header_height -
454 (inner_padding * 2.0f) - 8.0f;
455 const float overview_scale =
456 std::min(map_max_width / std::max(1.0f, content_size.x),
457 map_max_height / std::max(1.0f, content_size.y));
458 const ImVec2 map_size(std::max(72.0f, content_size.x * overview_scale),
459 std::max(56.0f, content_size.y * overview_scale));
460 const ImVec2 window_size(map_size.x + (inner_padding * 2.0f),
461 map_size.y + header_height + (inner_padding * 2.0f));
462 ImGui::SetNextWindowPos(window_pos);
463 ImGui::SetNextWindowSize(window_size);
464 ImGui::SetNextWindowBgAlpha(0.84f);
465
466 const ImGuiWindowFlags flags =
467 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
468 ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings;
469
470 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.78f));
471 ImGui::PushStyleColor(ImGuiCol_Border,
472 gui::ConvertColorToImVec4(theme.selection_primary));
473 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
474 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
475 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
476 ImVec2(inner_padding, inner_padding));
477
478 if (ImGui::Begin(kConnectedCanvasOverviewId, nullptr, flags)) {
479 actions.hovered =
480 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
481 ImGui::TextDisabled(tr("Overview"));
482 ImGui::InvisibleButton("##ConnectedOverviewMap", map_size,
483 ImGuiButtonFlags_MouseButtonLeft);
484
485 const ImVec2 map_min = ImGui::GetItemRectMin();
486 const ImVec2 map_max = ImGui::GetItemRectMax();
487 auto* draw_list = ImGui::GetWindowDrawList();
488
489 draw_list->AddRectFilled(map_min, map_max, IM_COL32(12, 16, 14, 230), 4.0f);
490 draw_list->AddRect(map_min, map_max,
491 ImGui::GetColorU32(theme.selection_primary), 4.0f);
492
493 auto overview_screen_point = [&](const ImVec2& canvas_point) {
494 return ImVec2(map_min.x + (canvas_point.x * overview_scale),
495 map_min.y + (canvas_point.y * overview_scale));
496 };
497
498 const bool map_hovered =
499 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
500 const ImVec2 mouse = ImGui::GetMousePos();
501 if (map_hovered) {
502 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
503 if (!connected_graph.room_mask[static_cast<size_t>(room_id)]) {
504 continue;
505 }
506 const auto& placement =
507 connected_graph.room_positions[static_cast<size_t>(room_id)];
508 if (!placement.placed) {
509 continue;
510 }
511
512 const ImVec2 room_origin = GetConnectedRoomOrigin(
513 placement.col, placement.row, connected_graph.min_col,
514 connected_graph.min_row);
515 const ImVec2 room_min = overview_screen_point(room_origin);
516 const ImVec2 room_max(
517 room_min.x + (kDungeonRoomPixelSize * overview_scale),
518 room_min.y + (kDungeonRoomPixelSize * overview_scale));
519 if (mouse.x >= room_min.x && mouse.x <= room_max.x &&
520 mouse.y >= room_min.y && mouse.y <= room_max.y) {
521 actions.hovered_room_id = room_id;
522 break;
523 }
524 }
525 }
526
527 for (const auto& link : connected_graph.links) {
528 const auto& from_placement =
529 connected_graph
530 .room_positions[static_cast<size_t>(link.from_room_id)];
531 const auto& to_placement =
532 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
533 if (!from_placement.placed || !to_placement.placed) {
534 continue;
535 }
536
537 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
538 link, from_placement.col, from_placement.row, to_placement.col,
539 to_placement.row, connected_graph.min_col, connected_graph.min_row);
540 draw_list->AddLine(overview_screen_point(canvas_start),
541 overview_screen_point(canvas_end),
543 1.0f);
544 }
545
546 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
547 if (!connected_graph.room_mask[static_cast<size_t>(room_id)]) {
548 continue;
549 }
550 const auto& placement =
551 connected_graph.room_positions[static_cast<size_t>(room_id)];
552 if (!placement.placed) {
553 continue;
554 }
555
556 const ImVec2 room_origin = GetConnectedRoomOrigin(
557 placement.col, placement.row, connected_graph.min_col,
558 connected_graph.min_row);
559 const ImVec2 room_min = overview_screen_point(room_origin);
560 const ImVec2 room_max(
561 room_min.x + (kDungeonRoomPixelSize * overview_scale),
562 room_min.y + (kDungeonRoomPixelSize * overview_scale));
563 const bool is_center = room_id == center_room_id;
564 const bool is_highlighted =
565 room_id == actions.hovered_room_id || room_id == highlighted_room_id;
566 const bool is_unlinked = !placement.connected_to_start;
567 const ImU32 fill =
568 is_center ? ImGui::GetColorU32(theme.selection_primary)
569 : is_highlighted
570 ? ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.warning))
571 : is_unlinked
572 ? ImGui::GetColorU32(
573 WithAlpha(AgentUI::GetTheme().status_warning, 0.52f))
574 : GetConnectedFloorColor(AgentUI::GetTheme(), connected_graph,
575 room_id, 0.52f);
576 const ImU32 outline = is_center || is_highlighted
577 ? IM_COL32(255, 255, 255, 255)
578 : IM_COL32(30, 38, 32, 220);
579 draw_list->AddRectFilled(room_min, room_max, fill, 1.5f);
580 draw_list->AddRect(room_min, room_max, outline, 1.5f);
581 }
582
583 auto draw_room_tag = [&](int room_id, ImU32 fill_color) {
584 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms ||
585 !connected_graph.room_mask[static_cast<size_t>(room_id)]) {
586 return;
587 }
588
589 const auto& placement =
590 connected_graph.room_positions[static_cast<size_t>(room_id)];
591 if (!placement.placed) {
592 return;
593 }
594
595 const ImVec2 room_origin = GetConnectedRoomOrigin(
596 placement.col, placement.row, connected_graph.min_col,
597 connected_graph.min_row);
598 const ImVec2 room_min = overview_screen_point(room_origin);
599
600 char label[8];
601 std::snprintf(label, sizeof(label), "%03X", room_id);
602 const float font_size = std::max(9.0f, ImGui::GetFontSize() - 1.0f);
603 const ImVec2 text_size = ImGui::GetFont()->CalcTextSizeA(
604 font_size, std::numeric_limits<float>::max(), 0.0f, label);
605 ImVec2 label_min(room_min.x + 2.0f, room_min.y + 2.0f);
606 const ImVec2 label_size(text_size.x + 8.0f, text_size.y + 6.0f);
607
608 if (label_min.x + label_size.x > map_max.x) {
609 label_min.x = map_max.x - label_size.x;
610 }
611 if (label_min.y + label_size.y > map_max.y) {
612 label_min.y = std::max(map_min.y, room_min.y - label_size.y - 2.0f);
613 }
614 label_min.x =
615 std::clamp(label_min.x, map_min.x, map_max.x - label_size.x);
616 label_min.y =
617 std::clamp(label_min.y, map_min.y, map_max.y - label_size.y);
618
619 const ImVec2 label_max(label_min.x + label_size.x,
620 label_min.y + label_size.y);
621 draw_list->AddRectFilled(label_min, label_max, fill_color, 2.0f);
622 draw_list->AddRect(label_min, label_max, IM_COL32(255, 255, 255, 235),
623 2.0f);
624 draw_list->AddText(ImGui::GetFont(), font_size,
625 ImVec2(label_min.x + 4.0f, label_min.y + 3.0f),
626 IM_COL32(255, 255, 255, 255), label);
627 };
628
629 draw_room_tag(center_room_id, ImGui::GetColorU32(theme.selection_primary));
630 const int tagged_hover_room = actions.hovered_room_id >= 0
631 ? actions.hovered_room_id
632 : highlighted_room_id;
633 if (tagged_hover_room >= 0 && tagged_hover_room != center_room_id) {
634 draw_room_tag(
635 tagged_hover_room,
636 ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.warning)));
637 }
638
639 const ImVec2 viewport_min(
640 map_min.x + ((scroll_x / content_scale) * overview_scale),
641 map_min.y + ((scroll_y / content_scale) * overview_scale));
642 const ImVec2 viewport_rect_size(
643 (viewport_size.x / content_scale) * overview_scale,
644 (viewport_size.y / content_scale) * overview_scale);
645 const ImVec2 viewport_rect_max(viewport_min.x + viewport_rect_size.x,
646 viewport_min.y + viewport_rect_size.y);
647 draw_list->AddRectFilled(viewport_min, viewport_rect_max,
648 IM_COL32(255, 255, 255, 20), 2.0f);
649 draw_list->AddRect(viewport_min, viewport_rect_max,
650 IM_COL32(255, 255, 255, 220), 2.0f, 0, 1.5f);
651
652 if (map_hovered) {
653 actions.hovered = true;
654 const bool dragging = ImGui::IsMouseDragging(ImGuiMouseButton_Left);
655 if (actions.hovered_room_id >= 0 &&
656 ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !dragging) {
657 actions.activated_room_id = actions.hovered_room_id;
658 } else if ((actions.hovered_room_id < 0 &&
659 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) ||
660 dragging) {
661 const ImVec2 current_mouse = ImGui::GetMousePos();
662 const float content_x =
663 std::clamp((current_mouse.x - map_min.x) / overview_scale, 0.0f,
664 content_size.x);
665 const float content_y =
666 std::clamp((current_mouse.y - map_min.y) / overview_scale, 0.0f,
667 content_size.y);
668 const float max_scroll_x =
669 std::max(0.0f, (content_size.x * content_scale) - viewport_size.x);
670 const float max_scroll_y =
671 std::max(0.0f, (content_size.y * content_scale) - viewport_size.y);
672 actions.target_scroll = ImVec2(
673 std::clamp((content_x * content_scale) - (viewport_size.x * 0.5f),
674 0.0f, max_scroll_x),
675 std::clamp((content_y * content_scale) - (viewport_size.y * 0.5f),
676 0.0f, max_scroll_y));
677 }
678
679 if (actions.hovered_room_id >= 0) {
680 ImGui::SetTooltip(
681 tr("[%03X] %s\nRelease to open room view"), actions.hovered_room_id,
682 zelda3::GetRoomLabel(actions.hovered_room_id).c_str());
683 }
684 }
685 }
686 ImGui::End();
687
688 ImGui::PopStyleVar(3);
689 ImGui::PopStyleColor(2);
690 return actions;
691}
692
693} // namespace
694
696 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms ||
697 !rooms_) {
698 return ImVec2(0.0f, 0.0f);
699 }
700 if (connected_graph_cache_start_room_id_ != center_room_id) {
703 }
704 const auto& g = connected_graph_cache_;
705 return GetConnectedCanvasSize(g.min_col, g.max_col, g.min_row, g.max_row);
706}
707
709 return std::clamp(connected_canvas_.global_scale(), kConnectedCanvasMinScale,
710 kConnectedCanvasMaxScale);
711}
712
714 if (center_room_id >= 0 && center_room_id < zelda3::kNumberOfRooms && rom_ &&
715 rom_->is_loaded() && rooms_) {
716 if (connected_graph_cache_start_room_id_ != center_room_id) {
719 }
720 }
721
722 const int connected_room_count =
725 : 0;
726 ImGui::TextDisabled(tr("%d room%s"), connected_room_count,
727 connected_room_count == 1 ? "" : "s");
728 if (connected_graph_cache_start_room_id_ == center_room_id &&
730 ImGui::SameLine(0.0f, 6.0f);
731 ImGui::TextColored(AgentUI::GetTheme().status_warning, tr("%d unlinked"),
733 if (ImGui::IsItemHovered()) {
734 ImGui::SetTooltip(
735 tr("Project rooms shown in this dungeon scope but not reachable from "
736 "the current room through resolved ROM doors, staircases, or "
737 "holewarps. They stay visible so broken connections are easier to "
738 "repair."));
739 }
740 }
741 if (connected_graph_cache_start_room_id_ == center_room_id &&
743 const auto& agent_theme = AgentUI::GetTheme();
744 for (const std::string& floor_label : connected_graph_cache_.floor_order) {
745 const int floor_count =
746 CountConnectedRoomsOnFloor(connected_graph_cache_, floor_label);
747 if (floor_count <= 0) {
748 continue;
749 }
750 ImGui::SameLine(0.0f, 6.0f);
751 ImGui::TextColored(GetConnectedFloorBaseColor(
752 agent_theme, connected_graph_cache_, floor_label),
753 "%s:%d", floor_label.c_str(), floor_count);
754 if (ImGui::IsItemHovered()) {
755 ImGui::SetTooltip(tr("%s rooms in this unified connected view"),
756 floor_label.c_str());
757 }
758 }
759 }
760 const size_t issue_count =
763 : 0;
764 if (issue_count > 0) {
765 ImGui::SameLine(0.0f, 6.0f);
766 ImGui::TextColored(gui::ConvertColorToImVec4(
767 gui::ThemeManager::Get().GetCurrentTheme().warning),
768 tr("%zu issue%s"), issue_count,
769 issue_count == 1 ? "" : "s");
770 if (ImGui::IsItemHovered()) {
771 std::string tooltip(
772 "Staircase configuration anomalies in this connected component:\n"
773 " Stale headers (no consuming object), missing destinations\n"
774 " (placed object but header is 0/invalid), and extras (objects\n"
775 " beyond the 4 header slots). Hover a room to localise.");
776 for (const auto& issue : connected_graph_cache_.staircase_issues) {
777 absl::StrAppend(
778 &tooltip,
779 absl::StrFormat("\n[%03X] %s", issue.from_room_id,
781 }
782 ImGui::SetTooltip("%s", tooltip.c_str());
783 }
784 }
785 const int auto_fixable_count =
787 ? CountAutoFixableStaircaseIssues(
789 : 0;
790 if (auto_fixable_count > 0) {
791 ImGui::SameLine(0.0f, 6.0f);
792 if (ImGui::SmallButton(ICON_MD_CLEAR " Clear stale##ConnectedFixStale")) {
794 }
795 if (ImGui::IsItemHovered()) {
796 ImGui::SetTooltip(tr(
797 "Clear stale non-zero staircase header slots that no placed "
798 "interroom-stair object consumes. This marks room headers dirty but "
799 "does not guess missing destinations or delete extra objects."));
800 }
801 }
802 const size_t out_of_scope_count =
805 : 0;
806 if (connected_graph_cache_.dungeon_scope_active && out_of_scope_count > 0) {
807 ImGui::SameLine(0.0f, 6.0f);
808 ImGui::TextColored(gui::ConvertColorToImVec4(
809 gui::ThemeManager::Get().GetCurrentTheme().info),
810 tr("%zu out-of-scope"), out_of_scope_count);
811 if (ImGui::IsItemHovered()) {
812 std::string tooltip(
813 "Resolved links that leave the current dungeon group. Hidden from\n"
814 "the visual matrix to keep the view scoped to one dungeon, but\n"
815 "still surfaced here so you can see cross-dungeon connections.");
816 for (const auto& link : connected_graph_cache_.out_of_scope_links) {
817 absl::StrAppend(
818 &tooltip,
819 absl::StrFormat("\n[%03X] %s", link.from_room_id,
821 }
822 ImGui::SetTooltip("%s", tooltip.c_str());
823 }
824 }
826 ImGui::SameLine(0.0f, 6.0f);
827 ImGui::TextColored(connected_action_status_is_error_
828 ? AgentUI::GetTheme().status_error
831 }
832 ImGui::SameLine(0.0f, 8.0f);
833 const auto compact_actions = DrawConnectedControlsCompact(
834 ConnectedCanvasScale(), connected_room_count, &show_connected_overview_,
836 if (compact_actions.fit) {
838 }
839 if (compact_actions.reset) {
841 }
842}
843
845 int center_room_id) {
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((ImTextureID)(intptr_t)composite->texture(),
1180 screen_min, screen_max, ImVec2(0, 0),
1181 ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
1182 } else {
1183 canvas_rt.draw_list->AddRectFilled(screen_min, screen_max,
1184 placeholder_fill, 6.0f);
1185 }
1186
1187 if (!placement.connected_to_start) {
1188 canvas_rt.draw_list->AddRectFilled(
1189 screen_min, screen_max,
1190 ImGui::GetColorU32(WithAlpha(theme.status_warning, 0.12f)), 6.0f);
1191 }
1192
1193 const std::string& floor_label =
1194 connected_graph.room_floor_labels[static_cast<size_t>(room_id)];
1195 if (!floor_label.empty()) {
1196 const float strip_width = std::clamp(6.0f * canvas_rt.scale, 3.0f, 8.0f);
1197 canvas_rt.draw_list->AddRectFilled(
1198 screen_min, ImVec2(screen_min.x + strip_width, screen_max.y),
1199 GetConnectedFloorColor(theme, connected_graph, room_id, 0.82f), 6.0f,
1200 ImDrawFlags_RoundCornersLeft);
1201 }
1202
1203 const bool hovered =
1204 mouse_in_map_viewport &&
1205 ImGui::IsMouseHoveringRect(screen_min, screen_max, false) &&
1206 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
1207 if (hovered) {
1208 hovered_room_id = room_id;
1209 }
1210
1211 const ImU32 border_color = room_id == center_room_id ? active_outline
1212 : hovered ? hover_outline
1213 : outline_color;
1214 const float border_thickness = room_id == center_room_id
1215 ? kConnectedRoomHighlightThickness
1216 : kConnectedRoomOutlineThickness;
1217 canvas_rt.draw_list->AddRect(screen_min, screen_max, border_color, 6.0f, 0,
1218 border_thickness);
1219
1220 char room_label[192];
1221 if (floor_label.empty()) {
1222 std::snprintf(
1223 room_label, sizeof(room_label), "[%03X] %s", room_id,
1225 } else {
1226 std::snprintf(
1227 room_label, sizeof(room_label), "[%03X] %s - %s", room_id,
1228 floor_label.c_str(),
1230 }
1231 const ImVec2 label_size = ImGui::CalcTextSize(room_label);
1232 const ImVec2 label_min(screen_min.x + kConnectedRoomLabelPadding,
1233 screen_min.y + kConnectedRoomLabelPadding);
1234 const ImVec2 label_max(label_min.x + label_size.x + 12.0f,
1235 label_min.y + kConnectedRoomLabelHeight);
1236 canvas_rt.draw_list->AddRectFilled(label_min, label_max, label_bg, 5.0f);
1237 canvas_rt.draw_list->AddText(ImVec2(label_min.x + 6.0f, label_min.y + 6.0f),
1238 ImGui::GetColorU32(theme.text_primary),
1239 room_label);
1240 }
1241
1242 for (const auto& link : connected_graph.links) {
1243 if (link.type == DungeonConnectedLinkType::Door) {
1244 continue;
1245 }
1246
1247 const auto& from_placement =
1248 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1249 const auto& to_placement =
1250 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1251 if (!from_placement.placed || !to_placement.placed) {
1252 continue;
1253 }
1254
1255 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1256 link, from_placement.col, from_placement.row, to_placement.col,
1257 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1258 const ImVec2 screen_start =
1259 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1260 const ImVec2 screen_end =
1261 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1262 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1263 canvas_rt.draw_list->AddCircleFilled(screen_start, transport_node_radius,
1264 link_color, 12);
1265 canvas_rt.draw_list->AddCircleFilled(screen_end, transport_node_radius,
1266 link_color, 12);
1267 canvas_rt.draw_list->AddCircle(screen_start, transport_node_radius + 1.5f,
1268 link_shadow, 12, 1.25f);
1269 canvas_rt.draw_list->AddCircle(screen_end, transport_node_radius + 1.5f,
1270 link_shadow, 12, 1.25f);
1271 }
1272
1273 ConnectedCanvasControlActions control_actions;
1275 const ImVec2 connected_controls_pos =
1276 using_connected_side_panel ? side_panel_screen_min
1279 : ImVec2(viewport_screen_max.x - 12.0f,
1280 viewport_screen_min.y + 12.0f);
1281 const ImVec2 connected_controls_pivot =
1282 using_connected_side_panel || connected_controls_custom_position_
1283 ? ImVec2(0.0f, 0.0f)
1284 : ImVec2(1.0f, 0.0f);
1285 control_actions = DrawConnectedCanvasControls(
1286 connected_controls_pos, connected_controls_pivot,
1287 connected_canvas_.global_scale(), connected_room_count,
1289 if (!using_connected_side_panel &&
1290 control_actions.moved_window_pos.has_value()) {
1293 *control_actions.moved_window_pos;
1294 }
1295 if (control_actions.reset_position) {
1297 connected_controls_custom_position_value_ = ImVec2(0.0f, 0.0f);
1298 }
1299 show_connected_overview_ = control_actions.show_overview;
1300 show_connected_current_room_preview_ = control_actions.show_preview;
1301 if (control_actions.fit) {
1302 apply_fit();
1303 }
1304 if (control_actions.reset) {
1305 apply_reset();
1306 }
1307 }
1308
1309 ConnectedCanvasOverviewActions overview_actions;
1311 overview_actions = DrawConnectedCanvasOverview(
1312 connected_graph, content_size, map_viewport_size,
1313 connected_canvas_.global_scale(), ImGui::GetScrollX(),
1314 ImGui::GetScrollY(), center_room_id, hovered_room_id,
1315 using_connected_side_panel
1316 ? ImVec2(side_panel_screen_min.x,
1317 side_panel_screen_min.y + kConnectedControlsApproxHeight +
1318 8.0f)
1319 : ImVec2(
1320 viewport_screen_max.x - kConnectedOverviewMaxWidth - 12.0f,
1321 viewport_screen_max.y - kConnectedOverviewMaxHeight - 12.0f));
1322 }
1323 if (overview_actions.target_scroll.has_value()) {
1324 ImGui::SetScrollX(overview_actions.target_scroll->x);
1325 ImGui::SetScrollY(overview_actions.target_scroll->y);
1326 }
1327 if (overview_actions.activated_room_id.has_value()) {
1328 if (*overview_actions.activated_room_id != center_room_id) {
1329 NavigateToRoom(*overview_actions.activated_room_id);
1330 }
1331 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1332 return *overview_actions.activated_room_id;
1333 }
1334
1335 if (!control_actions.hovered && !overview_actions.hovered &&
1336 hovered_room_id >= 0) {
1337 if (rooms_ && hovered_room_id < static_cast<int>(rooms_->size())) {
1338 std::string tooltip = absl::StrFormat(
1339 "[%03X] %s", hovered_room_id,
1341 .c_str());
1342 const auto& hovered_placement =
1343 connected_graph.room_positions[static_cast<size_t>(hovered_room_id)];
1344 const std::string& floor_label =
1345 connected_graph
1346 .room_floor_labels[static_cast<size_t>(hovered_room_id)];
1347 if (!floor_label.empty()) {
1348 absl::StrAppend(&tooltip, "\nFloor: ", floor_label);
1349 }
1350 if (!hovered_placement.connected_to_start) {
1351 absl::StrAppend(
1352 &tooltip,
1353 "\nUnlinked: no resolved ROM path from the current room.");
1354 }
1355 if (zelda3::Room* hovered_room =
1356 EnsureRoomLoadedForConnectedView(hovered_room_id)) {
1357 const auto diagnostics = CollectDungeonConnectedRoomLinkDiagnostics(
1358 hovered_room_id, *hovered_room,
1359 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
1360 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
1361 });
1362 if (!diagnostics.links.empty()) {
1363 absl::StrAppend(&tooltip, "\nConnections:");
1364 for (const auto& link : diagnostics.links) {
1365 absl::StrAppend(&tooltip, "\n ",
1367 }
1368 }
1369 if (!diagnostics.staircase_issues.empty()) {
1370 absl::StrAppend(&tooltip, "\nStaircase issues:");
1371 for (const auto& issue : diagnostics.staircase_issues) {
1372 absl::StrAppend(&tooltip, "\n ",
1374 }
1375 }
1376 }
1377 absl::StrAppend(&tooltip, "\nClick to open room view");
1378 ImGui::SetTooltip("%s", tooltip.c_str());
1379 }
1380 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1381 if (hovered_room_id != center_room_id) {
1382 NavigateToRoom(hovered_room_id);
1383 }
1384 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1385 return hovered_room_id;
1386 }
1387 }
1388
1390 if (gfx::Bitmap* current_room =
1392 current_room && current_room->texture()) {
1393 const float preview_scale =
1394 kConnectedCurrentRoomPreviewWidth /
1395 std::max(1.0f, static_cast<float>(current_room->width()));
1396 const ImVec2 preview_size(current_room->width() * preview_scale,
1397 current_room->height() * preview_scale);
1398 const ImVec2 preview_pos(
1399 using_connected_side_panel ? side_panel_screen_min.x
1400 : viewport_screen_max.x - preview_size.x -
1401 kConnectedCurrentRoomPreviewPadding,
1402 using_connected_side_panel
1403 ? side_panel_screen_max.y - kConnectedPreviewApproxHeight
1404 : viewport_screen_min.y + kConnectedCurrentRoomPreviewPadding);
1406 kConnectedCurrentRoomHudId, preview_pos, ImVec2(0, 0), [&]() {
1407 ImGui::Text(tr("Current Room"));
1408 ImGui::TextDisabled(
1409 "[%03X] %s", center_room_id,
1411 .c_str());
1412 ImGui::Image((ImTextureID)(intptr_t)current_room->texture(),
1413 preview_size);
1414 });
1415 }
1416 }
1417
1418 const bool side_panel_hovered = using_connected_side_panel &&
1419 mouse_pos.x >= side_panel_screen_min.x &&
1420 mouse_pos.x <= side_panel_screen_max.x &&
1421 mouse_pos.y >= side_panel_screen_min.y &&
1422 mouse_pos.y <= side_panel_screen_max.y;
1423 HandleConnectedCanvasPan((control_actions.hovered ||
1424 overview_actions.hovered || side_panel_hovered)
1425 ? center_room_id
1426 : hovered_room_id);
1427
1428 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1429 return std::nullopt;
1430}
1431
1432} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:155
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
int ApplyConnectedStaircaseIssueAutoFixes(int center_room_id)
std::optional< int > DrawConnectedRoomMatrix(int center_room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ConnectedRoomGraphData connected_graph_cache_
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:69
TextureHandle texture() const
Definition bitmap.h:403
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
auto global_scale() const
Definition canvas.h:403
void set_global_scale(float scale)
Definition canvas.cc:239
CanvasConfig & GetConfig()
Definition canvas.h:233
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_FIT_SCREEN
Definition icons.h:781
#define ICON_MD_CLEAR
Definition icons.h:416
#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)
int CountAutoFixableStaircaseIssues(const std::vector< DungeonStaircaseIssue > &issues)
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 kDungeonRoomPixelSize
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:134
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