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 }
859
861 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
862 // The connected-room matrix is a read-only graph view of neighbor rooms;
863 // editing happens in the main canvas only. Declaring kPreviewOnly lets
864 // future canvas surfaces (cursors, context-menu defaults) skip
865 // edit-affordances on this surface without each call site repeating it.
868 }
870 std::clamp(connected_canvas_.global_scale(), kConnectedCanvasMinScale,
871 kConnectedCanvasMaxScale));
872
873 if (connected_graph_cache_start_room_id_ != center_room_id) {
876 }
877 const ConnectedRoomGraphData& connected_graph = connected_graph_cache_;
878 const auto& connected_rooms = connected_graph.room_mask;
879 const int connected_room_count = connected_graph.room_count;
880
881 const ImVec2 content_size =
882 GetConnectedCanvasSize(connected_graph.min_col, connected_graph.max_col,
883 connected_graph.min_row, connected_graph.max_row);
884
886 gui::CanvasFrameOptions frame_opts;
887 frame_opts.canvas_size = content_size;
888 frame_opts.draw_grid = false;
889 frame_opts.draw_context_menu = false;
890 frame_opts.draw_overlay = true;
891 frame_opts.render_popups = true;
892
893 auto canvas_rt = gui::BeginCanvas(connected_canvas_, frame_opts);
895
896 HandleConnectedCanvasZoom(connected_canvas_, canvas_rt, content_size);
897 canvas_rt.scale = connected_canvas_.global_scale();
898
899 const ImVec2 window_pos = ImGui::GetWindowPos();
900 const ImVec2 window_content_min = ImGui::GetWindowContentRegionMin();
901 const ImVec2 window_content_max = ImGui::GetWindowContentRegionMax();
902 const ImVec2 viewport_screen_min(window_pos.x + window_content_min.x,
903 window_pos.y + window_content_min.y);
904 const ImVec2 viewport_screen_max(window_pos.x + window_content_max.x,
905 window_pos.y + window_content_max.y);
906 const ImVec2 viewport_size(
907 std::max(1.0f, window_content_max.x - window_content_min.x),
908 std::max(1.0f, window_content_max.y - window_content_min.y));
909 const bool reserve_connected_side_panel =
911 const float desired_side_panel_width =
912 std::max(kConnectedOverviewMaxWidth + 20.0f,
913 kConnectedCurrentRoomPreviewWidth + 20.0f);
914 float connected_side_panel_width = 0.0f;
915 if (reserve_connected_side_panel) {
916 const float raw_side_width = std::min(
917 desired_side_panel_width, std::max(0.0f, viewport_size.x - 320.0f));
918 const float min_threshold = connected_side_panel_visible_last_frame_
919 ? kConnectedSidePanelApproxMinWidth -
920 kConnectedSidePanelHideHysteresis
921 : kConnectedSidePanelApproxMinWidth;
922 if (raw_side_width >= min_threshold) {
923 connected_side_panel_width = raw_side_width;
924 }
925 }
926 connected_side_panel_visible_last_frame_ = connected_side_panel_width > 0.0f;
927 const float reserved_side_width =
928 connected_side_panel_width > 0.0f
929 ? connected_side_panel_width + kConnectedSidePanelGap
930 : 0.0f;
931 const ImVec2 map_viewport_size(
932 std::max(1.0f, viewport_size.x - reserved_side_width), viewport_size.y);
933 const ImVec2 map_viewport_screen_max(
934 viewport_screen_min.x + map_viewport_size.x, viewport_screen_max.y);
935 const ImVec2 side_panel_screen_min(
936 map_viewport_screen_max.x + kConnectedSidePanelGap,
937 viewport_screen_min.y);
938 const ImVec2 side_panel_screen_max(viewport_screen_max.x,
939 viewport_screen_max.y);
940 const bool using_connected_side_panel = connected_side_panel_width > 0.0f;
941 const ImVec2 mouse_pos = ImGui::GetMousePos();
942 const bool mouse_in_map_viewport = mouse_pos.x >= viewport_screen_min.x &&
943 mouse_pos.x <= map_viewport_screen_max.x &&
944 mouse_pos.y >= viewport_screen_min.y &&
945 mouse_pos.y <= viewport_screen_max.y;
946
947 auto center_room_scroll = [&](float scale_value, int room_id) {
948 const auto& placement =
949 connected_graph.room_positions[static_cast<size_t>(room_id)];
950 const ImVec2 room_origin = GetConnectedRoomOrigin(
951 placement.col, placement.row, connected_graph.min_col,
952 connected_graph.min_row);
953 const float max_scroll_x =
954 std::max(0.0f, (content_size.x * scale_value) - map_viewport_size.x);
955 const float max_scroll_y =
956 std::max(0.0f, (content_size.y * scale_value) - map_viewport_size.y);
957 return ImVec2(
958 std::clamp(
959 ((room_origin.x + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
960 (map_viewport_size.x * 0.5f),
961 0.0f, max_scroll_x),
962 std::clamp(
963 ((room_origin.y + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
964 (map_viewport_size.y * 0.5f),
965 0.0f, max_scroll_y));
966 };
967
968 auto center_content_scroll = [&](float scale_value) {
969 return ImVec2(
970 std::max(0.0f,
971 ((content_size.x * scale_value) - map_viewport_size.x) * 0.5f),
972 std::max(0.0f, ((content_size.y * scale_value) - map_viewport_size.y) *
973 0.5f));
974 };
975
976 auto set_scale_around_view_center = [&](float new_scale) {
977 const float old_scale = connected_canvas_.global_scale();
978 const float clamped_scale = std::clamp(new_scale, kConnectedCanvasMinScale,
979 kConnectedCanvasMaxScale);
980 if (std::abs(clamped_scale - old_scale) < 0.0001f) {
981 return;
982 }
983
984 const float world_x =
985 (ImGui::GetScrollX() + (map_viewport_size.x * 0.5f)) / old_scale;
986 const float world_y =
987 (ImGui::GetScrollY() + (map_viewport_size.y * 0.5f)) / old_scale;
988 connected_canvas_.set_global_scale(clamped_scale);
989 const float max_scroll_x =
990 std::max(0.0f, (content_size.x * clamped_scale) - map_viewport_size.x);
991 const float max_scroll_y =
992 std::max(0.0f, (content_size.y * clamped_scale) - map_viewport_size.y);
993 ImGui::SetScrollX(
994 std::clamp((world_x * clamped_scale) - (map_viewport_size.x * 0.5f),
995 0.0f, max_scroll_x));
996 ImGui::SetScrollY(
997 std::clamp((world_y * clamped_scale) - (map_viewport_size.y * 0.5f),
998 0.0f, max_scroll_y));
999 canvas_rt.scale = clamped_scale;
1000 };
1001
1002 auto apply_fit = [&]() {
1003 const auto fit =
1004 gui::ComputeZoomToFit(content_size, map_viewport_size, 24.0f);
1005 const float fit_scale = std::clamp(fit.scale, kConnectedCanvasMinScale,
1006 kConnectedCanvasMaxScale);
1008 const ImVec2 fit_scroll = center_content_scroll(fit_scale);
1009 ImGui::SetScrollX(fit_scroll.x);
1010 ImGui::SetScrollY(fit_scroll.y);
1011 canvas_rt.scale = fit_scale;
1012 };
1013
1014 auto apply_reset = [&]() {
1015 connected_canvas_.set_global_scale(kConnectedCanvasDefaultScale);
1016 const ImVec2 reset_scroll =
1017 center_room_scroll(kConnectedCanvasDefaultScale, center_room_id);
1018 ImGui::SetScrollX(reset_scroll.x);
1019 ImGui::SetScrollY(reset_scroll.y);
1020 canvas_rt.scale = kConnectedCanvasDefaultScale;
1022 };
1023
1024 const ImGuiIO& io = ImGui::GetIO();
1025 const bool allow_connected_shortcuts = ImGui::IsWindowFocused() &&
1026 !io.WantTextInput && !io.KeyCtrl &&
1027 !io.KeyAlt && !io.KeySuper;
1028 if (allow_connected_shortcuts) {
1029 if (ImGui::IsKeyPressed(ImGuiKey_F, false)) {
1030 apply_fit();
1031 } else if (ImGui::IsKeyPressed(ImGuiKey_0, false) ||
1032 ImGui::IsKeyPressed(ImGuiKey_Keypad0, false)) {
1033 apply_reset();
1034 } else if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
1035 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
1036 set_scale_around_view_center(connected_canvas_.global_scale() * 1.12f);
1037 } else if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
1038 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
1039 set_scale_around_view_center(connected_canvas_.global_scale() / 1.12f);
1040 }
1041 }
1042
1044 apply_fit();
1046 }
1048 apply_reset();
1050 }
1051
1052 if (last_connected_matrix_room_id_ != center_room_id) {
1053 const ImVec2 target_scroll =
1054 center_room_scroll(canvas_rt.scale, center_room_id);
1055 ImGui::SetScrollX(target_scroll.x);
1056 ImGui::SetScrollY(target_scroll.y);
1057 last_connected_matrix_room_id_ = center_room_id;
1058 }
1059
1060 const float scaled_step =
1061 (kDungeonRoomPixelSize + kConnectedRoomGap) * canvas_rt.scale;
1062 const float scroll_x = ImGui::GetScrollX();
1063 const float scroll_y = ImGui::GetScrollY();
1064 const int connected_cols =
1065 std::max(1, (connected_graph.max_col - connected_graph.min_col) + 1);
1066 const int connected_rows =
1067 std::max(1, (connected_graph.max_row - connected_graph.min_row) + 1);
1068 const int first_col =
1069 std::max(0, static_cast<int>(std::floor(scroll_x / scaled_step)) - 1);
1070 const int first_row =
1071 std::max(0, static_cast<int>(std::floor(scroll_y / scaled_step)) - 1);
1072 const int last_col = std::min(
1073 connected_cols - 1, static_cast<int>(std::ceil(
1074 (scroll_x + map_viewport_size.x) / scaled_step)) +
1075 1);
1076 const int last_row = std::min(
1077 connected_rows - 1, static_cast<int>(std::ceil(
1078 (scroll_y + map_viewport_size.y) / scaled_step)) +
1079 1);
1080
1081 std::vector<int> visible_rooms;
1082 visible_rooms.reserve(static_cast<size_t>(connected_room_count));
1083 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
1084 if (!connected_rooms[static_cast<size_t>(room_id)]) {
1085 continue;
1086 }
1087
1088 const auto& placement =
1089 connected_graph.room_positions[static_cast<size_t>(room_id)];
1090 if (!placement.placed) {
1091 continue;
1092 }
1093
1094 const int local_col = placement.col - connected_graph.min_col;
1095 const int local_row = placement.row - connected_graph.min_row;
1096 if (local_col < first_col || local_col > last_col ||
1097 local_row < first_row || local_row > last_row) {
1098 continue;
1099 }
1100
1101 visible_rooms.push_back(room_id);
1102 (void)PrepareRoomCompositeBitmap(room_id);
1103 }
1104
1105 if (renderer_) {
1107 }
1108
1109 const auto& theme = AgentUI::GetTheme();
1110 const ImU32 outline_color = ImGui::GetColorU32(
1112 gui::GetOutlineVec4().z, 0.55f));
1113 const ImU32 active_outline = ImGui::GetColorU32(theme.selection_primary);
1114 const ImU32 hover_outline = ImGui::GetColorU32(theme.status_warning);
1115 const ImU32 label_bg = ImGui::GetColorU32(ImVec4(0.05f, 0.08f, 0.06f, 0.88f));
1116 const ImU32 placeholder_fill = ImGui::GetColorU32(
1117 ImVec4(theme.editor_background.x, theme.editor_background.y,
1118 theme.editor_background.z, 0.95f));
1119 const ImU32 link_shadow = ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.32f));
1120 const float link_thickness =
1121 std::clamp(1.1f + (canvas_rt.scale * 3.4f), 1.35f, 3.0f);
1122 const float transport_node_radius =
1123 std::clamp(2.4f + (canvas_rt.scale * 7.0f), 3.0f, 6.0f);
1124
1125 if (using_connected_side_panel) {
1126 canvas_rt.draw_list->AddRectFilled(side_panel_screen_min,
1127 side_panel_screen_max,
1128 IM_COL32(10, 14, 12, 232), 6.0f);
1129 canvas_rt.draw_list->AddLine(
1130 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1131 side_panel_screen_min.y),
1132 ImVec2(side_panel_screen_min.x - (kConnectedSidePanelGap * 0.5f),
1133 side_panel_screen_max.y),
1134 ImGui::GetColorU32(ImVec4(gui::GetOutlineVec4().x,
1136 gui::GetOutlineVec4().z, 0.4f)),
1137 1.0f);
1138 }
1139
1140 for (const auto& link : connected_graph.links) {
1141 const auto& from_placement =
1142 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1143 const auto& to_placement =
1144 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1145 if (!from_placement.placed || !to_placement.placed) {
1146 continue;
1147 }
1148 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1149 link, from_placement.col, from_placement.row, to_placement.col,
1150 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1151 const ImVec2 screen_start =
1152 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1153 const ImVec2 screen_end =
1154 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1155 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1156 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_shadow,
1157 link_thickness + 2.0f);
1158 canvas_rt.draw_list->AddLine(screen_start, screen_end, link_color,
1159 link_thickness);
1160 }
1161
1162 int hovered_room_id = -1;
1163 for (int room_id : visible_rooms) {
1164 const auto& placement =
1165 connected_graph.room_positions[static_cast<size_t>(room_id)];
1166 const ImVec2 room_origin = GetConnectedRoomOrigin(
1167 placement.col, placement.row, connected_graph.min_col,
1168 connected_graph.min_row);
1169 const ImVec2 screen_min(
1170 canvas_rt.canvas_p0.x + room_origin.x * canvas_rt.scale,
1171 canvas_rt.canvas_p0.y + room_origin.y * canvas_rt.scale);
1172 const ImVec2 screen_max(
1173 screen_min.x + (kDungeonRoomPixelSize * canvas_rt.scale),
1174 screen_min.y + (kDungeonRoomPixelSize * canvas_rt.scale));
1175
1176 if (gfx::Bitmap* composite = PrepareRoomCompositeBitmap(room_id);
1177 composite && composite->texture()) {
1178 canvas_rt.draw_list->AddImage((ImTextureID)(intptr_t)composite->texture(),
1179 screen_min, screen_max, ImVec2(0, 0),
1180 ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
1181 } else {
1182 canvas_rt.draw_list->AddRectFilled(screen_min, screen_max,
1183 placeholder_fill, 6.0f);
1184 }
1185
1186 if (!placement.connected_to_start) {
1187 canvas_rt.draw_list->AddRectFilled(
1188 screen_min, screen_max,
1189 ImGui::GetColorU32(WithAlpha(theme.status_warning, 0.12f)), 6.0f);
1190 }
1191
1192 const std::string& floor_label =
1193 connected_graph.room_floor_labels[static_cast<size_t>(room_id)];
1194 if (!floor_label.empty()) {
1195 const float strip_width = std::clamp(6.0f * canvas_rt.scale, 3.0f, 8.0f);
1196 canvas_rt.draw_list->AddRectFilled(
1197 screen_min, ImVec2(screen_min.x + strip_width, screen_max.y),
1198 GetConnectedFloorColor(theme, connected_graph, room_id, 0.82f), 6.0f,
1199 ImDrawFlags_RoundCornersLeft);
1200 }
1201
1202 const bool hovered =
1203 mouse_in_map_viewport &&
1204 ImGui::IsMouseHoveringRect(screen_min, screen_max, false) &&
1205 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
1206 if (hovered) {
1207 hovered_room_id = room_id;
1208 }
1209
1210 const ImU32 border_color = room_id == center_room_id ? active_outline
1211 : hovered ? hover_outline
1212 : outline_color;
1213 const float border_thickness = room_id == center_room_id
1214 ? kConnectedRoomHighlightThickness
1215 : kConnectedRoomOutlineThickness;
1216 canvas_rt.draw_list->AddRect(screen_min, screen_max, border_color, 6.0f, 0,
1217 border_thickness);
1218
1219 char room_label[192];
1220 if (floor_label.empty()) {
1221 std::snprintf(
1222 room_label, sizeof(room_label), "[%03X] %s", room_id,
1224 } else {
1225 std::snprintf(
1226 room_label, sizeof(room_label), "[%03X] %s - %s", room_id,
1227 floor_label.c_str(),
1229 }
1230 const ImVec2 label_size = ImGui::CalcTextSize(room_label);
1231 const ImVec2 label_min(screen_min.x + kConnectedRoomLabelPadding,
1232 screen_min.y + kConnectedRoomLabelPadding);
1233 const ImVec2 label_max(label_min.x + label_size.x + 12.0f,
1234 label_min.y + kConnectedRoomLabelHeight);
1235 canvas_rt.draw_list->AddRectFilled(label_min, label_max, label_bg, 5.0f);
1236 canvas_rt.draw_list->AddText(ImVec2(label_min.x + 6.0f, label_min.y + 6.0f),
1237 ImGui::GetColorU32(theme.text_primary),
1238 room_label);
1239 }
1240
1241 for (const auto& link : connected_graph.links) {
1242 if (link.type == DungeonConnectedLinkType::Door) {
1243 continue;
1244 }
1245
1246 const auto& from_placement =
1247 connected_graph.room_positions[static_cast<size_t>(link.from_room_id)];
1248 const auto& to_placement =
1249 connected_graph.room_positions[static_cast<size_t>(link.to_room_id)];
1250 if (!from_placement.placed || !to_placement.placed) {
1251 continue;
1252 }
1253
1254 const auto [canvas_start, canvas_end] = GetConnectedLinkEndpoints(
1255 link, from_placement.col, from_placement.row, to_placement.col,
1256 to_placement.row, connected_graph.min_col, connected_graph.min_row);
1257 const ImVec2 screen_start =
1258 ToConnectedCanvasScreenPoint(canvas_rt, canvas_start);
1259 const ImVec2 screen_end =
1260 ToConnectedCanvasScreenPoint(canvas_rt, canvas_end);
1261 const ImU32 link_color = GetConnectedLinkColor(theme, link.type);
1262 canvas_rt.draw_list->AddCircleFilled(screen_start, transport_node_radius,
1263 link_color, 12);
1264 canvas_rt.draw_list->AddCircleFilled(screen_end, transport_node_radius,
1265 link_color, 12);
1266 canvas_rt.draw_list->AddCircle(screen_start, transport_node_radius + 1.5f,
1267 link_shadow, 12, 1.25f);
1268 canvas_rt.draw_list->AddCircle(screen_end, transport_node_radius + 1.5f,
1269 link_shadow, 12, 1.25f);
1270 }
1271
1272 ConnectedCanvasControlActions control_actions;
1274 const ImVec2 connected_controls_pos =
1275 using_connected_side_panel ? side_panel_screen_min
1278 : ImVec2(viewport_screen_max.x - 12.0f,
1279 viewport_screen_min.y + 12.0f);
1280 const ImVec2 connected_controls_pivot =
1281 using_connected_side_panel || connected_controls_custom_position_
1282 ? ImVec2(0.0f, 0.0f)
1283 : ImVec2(1.0f, 0.0f);
1284 control_actions = DrawConnectedCanvasControls(
1285 connected_controls_pos, connected_controls_pivot,
1286 connected_canvas_.global_scale(), connected_room_count,
1288 if (!using_connected_side_panel &&
1289 control_actions.moved_window_pos.has_value()) {
1292 *control_actions.moved_window_pos;
1293 }
1294 if (control_actions.reset_position) {
1296 connected_controls_custom_position_value_ = ImVec2(0.0f, 0.0f);
1297 }
1298 show_connected_overview_ = control_actions.show_overview;
1299 show_connected_current_room_preview_ = control_actions.show_preview;
1300 if (control_actions.fit) {
1301 apply_fit();
1302 }
1303 if (control_actions.reset) {
1304 apply_reset();
1305 }
1306 }
1307
1308 ConnectedCanvasOverviewActions overview_actions;
1310 overview_actions = DrawConnectedCanvasOverview(
1311 connected_graph, content_size, map_viewport_size,
1312 connected_canvas_.global_scale(), ImGui::GetScrollX(),
1313 ImGui::GetScrollY(), center_room_id, hovered_room_id,
1314 using_connected_side_panel
1315 ? ImVec2(side_panel_screen_min.x,
1316 side_panel_screen_min.y + kConnectedControlsApproxHeight +
1317 8.0f)
1318 : ImVec2(
1319 viewport_screen_max.x - kConnectedOverviewMaxWidth - 12.0f,
1320 viewport_screen_max.y - kConnectedOverviewMaxHeight - 12.0f));
1321 }
1322 if (overview_actions.target_scroll.has_value()) {
1323 ImGui::SetScrollX(overview_actions.target_scroll->x);
1324 ImGui::SetScrollY(overview_actions.target_scroll->y);
1325 }
1326 if (overview_actions.activated_room_id.has_value()) {
1327 if (*overview_actions.activated_room_id != center_room_id) {
1328 NavigateToRoom(*overview_actions.activated_room_id);
1329 }
1330 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1331 return *overview_actions.activated_room_id;
1332 }
1333
1334 if (!control_actions.hovered && !overview_actions.hovered &&
1335 hovered_room_id >= 0) {
1336 if (rooms_ && hovered_room_id < static_cast<int>(rooms_->size())) {
1337 std::string tooltip = absl::StrFormat(
1338 "[%03X] %s", hovered_room_id,
1340 .c_str());
1341 const auto& hovered_placement =
1342 connected_graph.room_positions[static_cast<size_t>(hovered_room_id)];
1343 const std::string& floor_label =
1344 connected_graph
1345 .room_floor_labels[static_cast<size_t>(hovered_room_id)];
1346 if (!floor_label.empty()) {
1347 absl::StrAppend(&tooltip, "\nFloor: ", floor_label);
1348 }
1349 if (!hovered_placement.connected_to_start) {
1350 absl::StrAppend(
1351 &tooltip,
1352 "\nUnlinked: no resolved ROM path from the current room.");
1353 }
1354 if (zelda3::Room* hovered_room =
1355 EnsureRoomLoadedForConnectedView(hovered_room_id)) {
1356 const auto diagnostics = CollectDungeonConnectedRoomLinkDiagnostics(
1357 hovered_room_id, *hovered_room,
1358 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
1359 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
1360 });
1361 if (!diagnostics.links.empty()) {
1362 absl::StrAppend(&tooltip, "\nConnections:");
1363 for (const auto& link : diagnostics.links) {
1364 absl::StrAppend(&tooltip, "\n ",
1366 }
1367 }
1368 if (!diagnostics.staircase_issues.empty()) {
1369 absl::StrAppend(&tooltip, "\nStaircase issues:");
1370 for (const auto& issue : diagnostics.staircase_issues) {
1371 absl::StrAppend(&tooltip, "\n ",
1373 }
1374 }
1375 }
1376 absl::StrAppend(&tooltip, "\nClick to open room view");
1377 ImGui::SetTooltip("%s", tooltip.c_str());
1378 }
1379 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1380 if (hovered_room_id != center_room_id) {
1381 NavigateToRoom(hovered_room_id);
1382 }
1383 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1384 return hovered_room_id;
1385 }
1386 }
1387
1389 if (gfx::Bitmap* current_room = PrepareRoomCompositeBitmap(center_room_id);
1390 current_room && current_room->texture()) {
1391 const float preview_scale =
1392 kConnectedCurrentRoomPreviewWidth /
1393 std::max(1.0f, static_cast<float>(current_room->width()));
1394 const ImVec2 preview_size(current_room->width() * preview_scale,
1395 current_room->height() * preview_scale);
1396 const ImVec2 preview_pos(
1397 using_connected_side_panel ? side_panel_screen_min.x
1398 : viewport_screen_max.x - preview_size.x -
1399 kConnectedCurrentRoomPreviewPadding,
1400 using_connected_side_panel
1401 ? side_panel_screen_max.y - kConnectedPreviewApproxHeight
1402 : viewport_screen_min.y + kConnectedCurrentRoomPreviewPadding);
1404 kConnectedCurrentRoomHudId, preview_pos, ImVec2(0, 0), [&]() {
1405 ImGui::Text(tr("Current Room"));
1406 ImGui::TextDisabled(
1407 "[%03X] %s", center_room_id,
1409 .c_str());
1410 ImGui::Image((ImTextureID)(intptr_t)current_room->texture(),
1411 preview_size);
1412 });
1413 }
1414 }
1415
1416 const bool side_panel_hovered = using_connected_side_panel &&
1417 mouse_pos.x >= side_panel_screen_min.x &&
1418 mouse_pos.x <= side_panel_screen_max.x &&
1419 mouse_pos.y >= side_panel_screen_min.y &&
1420 mouse_pos.y <= side_panel_screen_max.y;
1421 HandleConnectedCanvasPan((control_actions.hovered ||
1422 overview_actions.hovered || side_panel_hovered)
1423 ? center_room_id
1424 : hovered_room_id);
1425
1426 gui::EndCanvas(connected_canvas_, canvas_rt, frame_opts);
1427 return std::nullopt;
1428}
1429
1430} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:144
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
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_
gfx::Bitmap * PrepareRoomCompositeBitmap(int room_id)
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:401
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
auto global_scale() const
Definition canvas.h:397
void set_global_scale(float scale)
Definition canvas.cc:239
CanvasConfig & GetConfig()
Definition canvas.h:229
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:289
void BeginNoPadding()
Definition style.cc:285
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