using System; using System.Linq; using System.Threading; using AIImages.Helpers; using AIImages.Models; using AIImages.Services; using RimWorld; using UnityEngine; using Verse; #pragma warning disable IDE1006 // Naming Styles namespace AIImages { /// /// Окно для просмотра персонажа и генерации AI изображений /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Style", "IDE1006:Naming Styles", Justification = "RimWorld Window naming convention" )] [System.Diagnostics.CodeAnalysis.SuppressMessage( "Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "RimWorld Window naming convention" )] public class Window_AIImage : Window { private Pawn pawn; private PawnAppearanceData appearanceData; private StableDiffusionSettings generationSettings; private Texture2D generatedImage; private bool isGenerating = false; private string generationStatus = ""; private CancellationTokenSource cancellationTokenSource; // Сервисы (получаем через DI) private readonly IPawnDescriptionService pawnDescriptionService; private readonly IPromptGeneratorService promptGeneratorService; private readonly IStableDiffusionApiService apiService; public Window_AIImage(Pawn pawn) { this.pawn = pawn; this.doCloseX = true; this.doCloseButton = true; this.forcePause = false; this.absorbInputAroundWindow = false; this.draggable = true; this.preventCameraMotion = false; // Получаем сервисы через DI контейнер var services = AIImagesMod.Services; pawnDescriptionService = services.PawnDescriptionService; promptGeneratorService = services.PromptGeneratorService; apiService = services.ApiService; // Извлекаем данные персонажа RefreshPawnData(); } public override Vector2 InitialSize => new Vector2(900f, 800f); private Vector2 scrollPosition = Vector2.zero; private Vector2 promptScrollPosition = Vector2.zero; private float copiedMessageTime = 0f; /// /// Обновляет данные персонажа /// private void RefreshPawnData() { appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn); generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings(); } /// /// Освобождает ресурсы при закрытии окна /// public override void PreClose() { base.PreClose(); // Отменяем генерацию, если она идет if (isGenerating && cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } // Освобождаем CancellationTokenSource cancellationTokenSource?.Dispose(); } /// /// Обновляет текущую пешку в окне /// public void UpdatePawn(Pawn newPawn) { this.pawn = newPawn; RefreshPawnData(); } /// /// Получить текущую пешку /// public Pawn CurrentPawn => pawn; /// /// Вызывается каждый кадр для обновления окна /// public override void WindowUpdate() { base.WindowUpdate(); // Проверяем, изменилась ли выбранная пешка Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p => p.IsColonist && p.Spawned && p.Faction == Faction.OfPlayer ); // Если выбрана новая колонистская пешка, обновляем окно if (selectedPawn != null && selectedPawn != pawn) { UpdatePawn(selectedPawn); } // Уменьшаем таймер сообщения о копировании if (copiedMessageTime > 0f) { copiedMessageTime -= Time.deltaTime; } } /// /// Асинхронная генерация изображения с поддержкой отмены /// private async System.Threading.Tasks.Task GenerateImageAsync() { if (isGenerating) return; // Создаем новый CancellationTokenSource cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); isGenerating = true; generationStatus = "AIImages.Generation.InProgress".Translate(); try { // Генерируем промпты string positivePrompt = promptGeneratorService.GeneratePositivePrompt( appearanceData, generationSettings ); string negativePrompt = promptGeneratorService.GenerateNegativePrompt( generationSettings ); // Создаем запрос var request = new GenerationRequest { Prompt = positivePrompt, NegativePrompt = negativePrompt, Steps = generationSettings.Steps, CfgScale = generationSettings.CfgScale, Width = generationSettings.Width, Height = generationSettings.Height, Sampler = generationSettings.Sampler, Scheduler = generationSettings.Scheduler, Seed = generationSettings.Seed, Model = AIImagesMod.Settings.apiEndpoint, }; // Генерируем изображение с поддержкой отмены var result = await apiService.GenerateImageAsync( request, cancellationTokenSource.Token ); if (result.Success) { // Загружаем текстуру generatedImage = new Texture2D(2, 2); generatedImage.LoadImage(result.ImageData); generationStatus = "AIImages.Generation.Success".Translate(); Messages.Message( "AIImages.Generation.SavedTo".Translate(result.SavedPath), MessageTypeDefOf.PositiveEvent ); } else { generationStatus = $"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}"; Messages.Message(generationStatus, MessageTypeDefOf.RejectInput); } } catch (OperationCanceledException) { generationStatus = "AIImages.Generation.Cancelled".Translate(); Log.Message("[AI Images] Generation cancelled by user"); } catch (Exception ex) { generationStatus = $"Error: {ex.Message}"; Log.Error($"[AI Images] Generation error: {ex}"); Messages.Message( $"AIImages.Generation.Error".Translate() + ": {ex.Message}", MessageTypeDefOf.RejectInput ); } finally { isGenerating = false; } } /// /// Запускает генерацию изображения (обертка для безопасного fire-and-forget) /// private void StartGeneration() { AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation"); } /// /// Отменяет генерацию изображения /// private void CancelGeneration() { if (isGenerating && cancellationTokenSource != null) { cancellationTokenSource.Cancel(); generationStatus = "AIImages.Generation.Cancelling".Translate(); } } public override void DoWindowContents(Rect inRect) { float curY = 0f; // Заголовок Text.Font = GameFont.Medium; Widgets.Label( new Rect(0f, curY, inRect.width, 40f), "AIImages.Window.Title".Translate() ); curY += 45f; // Имя пешки Text.Font = GameFont.Small; Widgets.Label( new Rect(0f, curY, inRect.width, 30f), "AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve()) ); curY += 40f; // Разделитель Widgets.DrawLineHorizontal(0f, curY, inRect.width); curY += 10f; // Разделяем на две колонки: левая - информация, правая - изображение float leftColumnWidth = inRect.width * 0.55f; float rightColumnWidth = inRect.width * 0.42f; float columnGap = inRect.width * 0.03f; // Левая колонка - прокручиваемая информация Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, inRect.height - curY); DrawLeftColumn(leftColumnRect); // Правая колонка - превью и управление Rect rightColumnRect = new Rect( leftColumnWidth + columnGap, curY, rightColumnWidth, inRect.height - curY ); DrawRightColumn(rightColumnRect); } private void DrawLeftColumn(Rect rect) { Rect scrollViewRect = new Rect(0f, 0f, rect.width - 20f, CalculateContentHeight()); Widgets.BeginScrollView(rect, ref scrollPosition, scrollViewRect); float contentY = 0f; // Секция "Внешность" Text.Font = GameFont.Medium; Widgets.Label( new Rect(10f, contentY, scrollViewRect.width - 20f, 30f), "AIImages.Appearance.SectionTitle".Translate() ); contentY += 35f; Text.Font = GameFont.Small; string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn); float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight), appearanceText ); contentY += appearanceHeight + 20f; // Разделитель Widgets.DrawLineHorizontal(10f, contentY, scrollViewRect.width - 20f); contentY += 15f; // Секция "Одежда" Text.Font = GameFont.Medium; Widgets.Label( new Rect(10f, contentY, scrollViewRect.width - 20f, 30f), "AIImages.Apparel.SectionTitle".Translate() ); contentY += 35f; Text.Font = GameFont.Small; string apparelText = pawnDescriptionService.GetApparelDescription(pawn); float apparelHeight = Text.CalcHeight(apparelText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, apparelHeight), apparelText ); Widgets.EndScrollView(); } private void DrawRightColumn(Rect rect) { float curY = 0f; // Превью изображения if (generatedImage != null) { float imageSize = Mathf.Min(rect.width, 400f); Rect imageRect = new Rect( rect.x + (rect.width - imageSize) / 2f, rect.y + curY, imageSize, imageSize ); GUI.DrawTexture(imageRect, generatedImage); curY += imageSize + 10f; } else { // Placeholder для изображения float placeholderSize = Mathf.Min(rect.width, 300f); Rect placeholderRect = new Rect( rect.x + (rect.width - placeholderSize) / 2f, rect.y + curY, placeholderSize, placeholderSize ); Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f)); Text.Anchor = TextAnchor.MiddleCenter; Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate()); Text.Anchor = TextAnchor.UpperLeft; curY += placeholderSize + 10f; } // Статус генерации if (!string.IsNullOrEmpty(generationStatus)) { Text.Font = GameFont.Small; float statusHeight = Text.CalcHeight(generationStatus, rect.width); Widgets.Label( new Rect(rect.x, rect.y + curY, rect.width, statusHeight), generationStatus ); curY += statusHeight + 10f; } // Кнопка генерации/отмены Text.Font = GameFont.Small; if (isGenerating) { // Показываем кнопку отмены во время генерации if ( Widgets.ButtonText( new Rect(rect.x, rect.y + curY, rect.width, 35f), "AIImages.Generation.Cancel".Translate() ) ) { CancelGeneration(); } } else { // Показываем кнопку генерации if ( Widgets.ButtonText( new Rect(rect.x, rect.y + curY, rect.width, 35f), "AIImages.Generation.Generate".Translate() ) ) { StartGeneration(); } } curY += 40f; // Промпт секция Text.Font = GameFont.Medium; Widgets.Label( new Rect(rect.x, rect.y + curY, rect.width, 30f), "AIImages.Prompt.SectionTitle".Translate() ); curY += 35f; // Получаем промпт Text.Font = GameFont.Tiny; string promptText = promptGeneratorService.GeneratePositivePrompt( appearanceData, generationSettings ); // Фиксированная высота для области промпта float promptBoxHeight = 150f; float actualPromptHeight = Text.CalcHeight(promptText, rect.width - 20f); Rect promptOuterRect = new Rect(rect.x, rect.y + curY, rect.width, promptBoxHeight); Rect promptViewRect = new Rect(0f, 0f, rect.width - 20f, actualPromptHeight); // Рисуем фон Widgets.DrawBoxSolid(promptOuterRect, new Color(0.1f, 0.1f, 0.1f, 0.5f)); // Рисуем промпт с прокруткой Widgets.BeginScrollView( promptOuterRect.ContractedBy(5f), ref promptScrollPosition, promptViewRect ); Widgets.Label( new Rect(0f, 0f, promptViewRect.width, promptViewRect.height), promptText ); Widgets.EndScrollView(); curY += promptBoxHeight + 10f; // Кнопка копирования промпта if ( Widgets.ButtonText( new Rect(rect.x, rect.y + curY, rect.width / 2f - 5f, 30f), "AIImages.Prompt.CopyButton".Translate() ) ) { GUIUtility.systemCopyBuffer = promptText; copiedMessageTime = 2f; } // Кнопка обновления данных if ( Widgets.ButtonText( new Rect( rect.x + rect.width / 2f + 5f, rect.y + curY, rect.width / 2f - 5f, 30f ), "AIImages.Window.Refresh".Translate() ) ) { RefreshPawnData(); } // Сообщение о копировании if (copiedMessageTime > 0f) { curY += 35f; GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f); Widgets.Label( new Rect(rect.x, rect.y + curY, rect.width, 25f), "AIImages.Prompt.Copied".Translate() ); GUI.color = Color.white; } } private float CalculateContentHeight() { float height = 0f; // Заголовок "Внешность" height += 35f; // Текст внешности string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn); height += Text.CalcHeight(appearanceText, 400f) + 20f; // Разделитель height += 15f; // Заголовок "Одежда" height += 35f; // Текст одежды string apparelText = pawnDescriptionService.GetApparelDescription(pawn); height += Text.CalcHeight(apparelText, 400f) + 20f; // Дополнительный отступ height += 50f; return height; } } }