845 int center_room_id) {
848 ImGui::Text(tr(
"Invalid room ID: %d"), center_room_id);
852 ImGui::Text(tr(
"ROM not loaded"));
856 ImGui::TextDisabled(tr(
"Room data unavailable"));
871 kConnectedCanvasMaxScale));
878 const auto& connected_rooms = connected_graph.
room_mask;
879 const int connected_room_count = connected_graph.
room_count;
881 const ImVec2 content_size =
882 GetConnectedCanvasSize(connected_graph.
min_col, connected_graph.
max_col,
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));
919 ? kConnectedSidePanelApproxMinWidth -
920 kConnectedSidePanelHideHysteresis
921 : kConnectedSidePanelApproxMinWidth;
922 if (raw_side_width >= min_threshold) {
923 connected_side_panel_width = raw_side_width;
927 const float reserved_side_width =
928 connected_side_panel_width > 0.0f
929 ? connected_side_panel_width + kConnectedSidePanelGap
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;
947 auto center_room_scroll = [&](
float scale_value,
int room_id) {
948 const auto& placement =
950 const ImVec2 room_origin = GetConnectedRoomOrigin(
951 placement.col, placement.row, connected_graph.
min_col,
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);
959 ((room_origin.x + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
960 (map_viewport_size.x * 0.5f),
963 ((room_origin.y + (kDungeonRoomPixelSize * 0.5f)) * scale_value) -
964 (map_viewport_size.y * 0.5f),
965 0.0f, max_scroll_y));
968 auto center_content_scroll = [&](
float scale_value) {
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) *
976 auto set_scale_around_view_center = [&](
float new_scale) {
978 const float clamped_scale = std::clamp(new_scale, kConnectedCanvasMinScale,
979 kConnectedCanvasMaxScale);
980 if (std::abs(clamped_scale - old_scale) < 0.0001f) {
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;
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);
994 std::clamp((world_x * clamped_scale) - (map_viewport_size.x * 0.5f),
995 0.0f, max_scroll_x));
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;
1002 auto apply_fit = [&]() {
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;
1014 auto apply_reset = [&]() {
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;
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)) {
1031 }
else if (ImGui::IsKeyPressed(ImGuiKey_0,
false) ||
1032 ImGui::IsKeyPressed(ImGuiKey_Keypad0,
false)) {
1034 }
else if (ImGui::IsKeyPressed(ImGuiKey_Equal,
false) ||
1035 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd,
false)) {
1037 }
else if (ImGui::IsKeyPressed(ImGuiKey_Minus,
false) ||
1038 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract,
false)) {
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);
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)) +
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)) +
1081 std::vector<int> visible_rooms;
1082 visible_rooms.reserve(
static_cast<size_t>(connected_room_count));
1084 if (!connected_rooms[
static_cast<size_t>(room_id)]) {
1088 const auto& placement =
1090 if (!placement.placed) {
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) {
1101 visible_rooms.push_back(room_id);
1110 const ImU32 outline_color = ImGui::GetColorU32(
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);
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),
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) {
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,
1162 int hovered_room_id = -1;
1163 for (
int room_id : visible_rooms) {
1164 const auto& placement =
1166 const ImVec2 room_origin = GetConnectedRoomOrigin(
1167 placement.col, placement.row, connected_graph.
min_col,
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));
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));
1182 canvas_rt.draw_list->AddRectFilled(screen_min, screen_max,
1183 placeholder_fill, 6.0f);
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);
1192 const std::string& floor_label =
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);
1202 const bool hovered =
1203 mouse_in_map_viewport &&
1204 ImGui::IsMouseHoveringRect(screen_min, screen_max,
false) &&
1205 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
1207 hovered_room_id = room_id;
1210 const ImU32 border_color = room_id == center_room_id ? active_outline
1211 : hovered ? hover_outline
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,
1219 char room_label[192];
1220 if (floor_label.empty()) {
1222 room_label,
sizeof(room_label),
"[%03X] %s", room_id,
1226 room_label,
sizeof(room_label),
"[%03X] %s - %s", room_id,
1227 floor_label.c_str(),
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),
1241 for (
const auto& link : connected_graph.
links) {
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) {
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,
1264 canvas_rt.draw_list->AddCircleFilled(screen_end, transport_node_radius,
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);
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 =
1282 ? ImVec2(0.0f, 0.0f)
1283 : ImVec2(1.0f, 0.0f);
1284 control_actions = DrawConnectedCanvasControls(
1285 connected_controls_pos, connected_controls_pivot,
1288 if (!using_connected_side_panel &&
1289 control_actions.moved_window_pos.has_value()) {
1292 *control_actions.moved_window_pos;
1294 if (control_actions.reset_position) {
1300 if (control_actions.fit) {
1303 if (control_actions.reset) {
1308 ConnectedCanvasOverviewActions overview_actions;
1310 overview_actions = DrawConnectedCanvasOverview(
1311 connected_graph, content_size, map_viewport_size,
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 +
1319 viewport_screen_max.x - kConnectedOverviewMaxWidth - 12.0f,
1320 viewport_screen_max.y - kConnectedOverviewMaxHeight - 12.0f));
1322 if (overview_actions.target_scroll.has_value()) {
1323 ImGui::SetScrollX(overview_actions.target_scroll->x);
1324 ImGui::SetScrollY(overview_actions.target_scroll->y);
1326 if (overview_actions.activated_room_id.has_value()) {
1327 if (*overview_actions.activated_room_id != center_room_id) {
1331 return *overview_actions.activated_room_id;
1334 if (!control_actions.hovered && !overview_actions.hovered &&
1335 hovered_room_id >= 0) {
1337 std::string tooltip = absl::StrFormat(
1338 "[%03X] %s", hovered_room_id,
1341 const auto& hovered_placement =
1342 connected_graph.
room_positions[
static_cast<size_t>(hovered_room_id)];
1343 const std::string& floor_label =
1346 if (!floor_label.empty()) {
1347 absl::StrAppend(&tooltip,
"\nFloor: ", floor_label);
1349 if (!hovered_placement.connected_to_start) {
1352 "\nUnlinked: no resolved ROM path from the current room.");
1357 hovered_room_id, *hovered_room,
1361 if (!diagnostics.links.empty()) {
1362 absl::StrAppend(&tooltip,
"\nConnections:");
1363 for (
const auto& link : diagnostics.links) {
1364 absl::StrAppend(&tooltip,
"\n ",
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 ",
1376 absl::StrAppend(&tooltip,
"\nClick to open room view");
1377 ImGui::SetTooltip(
"%s", tooltip.c_str());
1379 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1380 if (hovered_room_id != center_room_id) {
1384 return hovered_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,
1410 ImGui::Image((ImTextureID)(intptr_t)current_room->texture(),
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)
1427 return std::nullopt;